Solana 中的合约间调用通常称为 CPI(Cross-Program Invocation),它允许一个 Solana 程序调用另一个 Solana 程序的功能。这种调用是通过向 Solana 区块链发送事务来实现的,其中包含了被调用程序的地址以及调用所需的数据。
CPI 是 Solana 上构建去中心化金融(DeFi)和其他复杂应用程序的基础。通过 CPI,不同的智能合约可以相互协作,执行复杂的逻辑,并且可以实现更加灵活和强大的应用程序。
1.通过一个案例认识CPI
假设我们已经创建了代币 TokenA,并需要给某个人进行空投。按照之前的做法,我们首先为该用户创建 TokenA 的 ATA 账户,例如命名为 TokenA_ATA_Account。然后进行代币的发放操作。
按照之前的步骤,我们首先创建 TokenA_ATA_Account 地址,并将其 ower_pubkey 设置为该用户的公钥。接着,我们需要再创建一个交易来为 TokenA_ATA_Account 进行代币的发放操作。这两个交易存在先后顺序关系,第二个交易必须在第一个交易执行完成后才能执行。同时,这样做会增加两次交易的 gas 费用。那么,有没有办法将这两次交易合并呢?
答案是使用一个合约来实现这两个步骤,然后发送一个交易即可。在这个合约中,我们可以实现账户的创建,并调用 SPL-Token 合约来执行代币的发放操作。这个合约需要与 ATA 合约进行交互,以调用其创建账户指令,然后再与 Token 合约进行交互,执行其代币发放的指令。通过这种方式,我们可以将两次交易合并为一次,减少 gas 费用并提高效率。
合约之间调用分成两类,一类是无需校验签名的invoke,另一类是目标合约需要校验操作权限的invoke_signed 方法,前者类似 router 路由到另外一个合约执行,后者则目标程序对相关的账号有操作权限,也就是我们前面说的 PDA 账号。
2.无需校验签名的invoke
use solana_program::{
account_info::{
next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
instruction, msg,
program::invoke,
pubkey::Pubkey,
};
// Declare and export the program's entrypoint
entrypoint!(process_instruction);
// Program entrypoint's implementation
pub fn process_instruction(
_program_id: &Pubkey, // Public key of the account the hello world program was loaded into
accounts: &[AccountInfo], // The account to say hello to
_instruction_data: &[u8], // Ignored, all helloworld instructions are hellos
) -> ProgramResult {
// Iterating accounts is safer than indexing
let accounts_iter = &mut accounts.iter();
// Get the account to say hello to
let account = next_account_info(accounts_iter)?;
let helloworld = next_account_info(accounts_iter)?;
msg!("invoke program entrypoint from {}", account.key);
let account_metas = vec![instruction::AccountMeta::new(*account.key, true)];
let instruction = instruction::Instruction::new_with_bytes(
*helloworld.key,
"hello".as_bytes(),
account_metas,
);
let account_infos = [account.clone()];
invoke(&instruction, &account_infos[..])
}
客户端调用:
use std::str::FromStr;
use solana_sdk::signature::Signer;
use solana_rpc_client::rpc_client;
use solana_sdk::signer::keypair;
use solana_sdk::transaction;
use solana_program::instruction;
use solana_program::pubkey;
const RPC_ADDR: &str = "https://api.devnet.solana.com";
fn main() {
let helloworld = pubkey::Pubkey::from_str("BiEjWMFBfRCup21wQMcC7TxioLQZcnFxBXWKSxZoGV1Q").unwrap();
let invoke= pubkey::Pubkey::from_str("DpB75eadx9QruoaJbGJj4JKjHwmFej2uk9dGAdHiMYF1").unwrap();
let private_key_bytes: [u8; 64] = [26,...,89];
// 将私钥字节数组转换为 Base58 编码的字符串
let private_key_bs58_string = bs58::encode(private_key_bytes).into_string();
println!("Solana Private Key (Base58): {}", private_key_bs58_string);
let me = keypair::Keypair::from_base58_string(&private_key_bs58_string);
println!("me is {}", me.pubkey());
let client = rpc_client::RpcClient::new(RPC_ADDR);
let account_metas = vec![
instruction::AccountMeta::new(me.pubkey(), true),
instruction::AccountMeta::new_readonly(helloworld, false),
];
let instruction = instruction::Instruction::new_with_bytes(
invoke,
"hello".as_bytes(),
account_metas,
);
let ixs = vec![instruction];
let latest_blockhash = client.get_latest_blockhash().unwrap();
let sig = client.send_and_confirm_transaction(&transaction::Transaction::new_signed_with_payer(
&ixs,
Some(&me.pubkey()),
&[&me],
latest_blockhash,
)).unwrap();
println!("tx:{}", sig);
}
运行结果:
me is E3NAyseUZrgwSbYe2g47TuFJr5CxAeneK63mR4Ufbqhh
tx:2s3CsHB9o3bb1q2pNf4j85csEjSsRJHm36CBXP2Dfbkxu6yWPz5z4No4YdbukbJGxk6nYKyXSF83itmMnjVhCjZd
查看浏览器:
https://solscan.io/tx/2s3CsHB9o3bb1q2pNf4j85csEjSsRJHm36CBXP2Dfbkxu6yWPz5z4No4YdbukbJGxk6nYKyXSF83itmMnjVhCjZd?cluster=devnet
先调用了invoke 合约,然后调用了helloworld 合约。
3.校验签名的invoke_signed
程序可以使用程序派生地址发出包含原始交易中未签名的签名帐户的指令 。要使用程序派生地址签署帐户,程序可以使用 invoke_signed().
invoke_signed(
&instruction,
accounts,
&[&["First addresses seed"],
&["Second addresses first seed", "Second addresses second seed"]],
)?;
前面我们有介绍合约内生成 PDA 账号的方式,包括一个地址,一个seed。通过 find_program_address 可以得到 PDA 地址,以及一个 Bump 种子。其中这里传入的地址,将有权限校验该 PDA 地址的签名,可以认为这个合约地址相当于 PDA 账号的私钥。
pub fn find_program_address(seeds: &[&[u8]], program_id: &Pubkey) -> (Pubkey, u8) {
Self::try_find_program_address(seeds, program_id)
.unwrap_or_else(|| panic!("Unable to find a viable program address bump seed"))
}
use std::str::FromStr;
use solana_program::pubkey::Pubkey;
fn main() {
// Sol钱包地址
let sol_addr: Pubkey = Pubkey::from_str("E3NAyseUZrgwSbYe2g47TuFJr5CxAeneK63mR4Ufbqhh").unwrap();
// SPL Token官方地址
let spl_token_addr : Pubkey= Pubkey::from_str("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA").unwrap();
// 创建好的SPl Token代币Mint地址
let token_addr: Pubkey = Pubkey::from_str("vJELotX5KpAoPpCVPXB6QXpUkqMdA14me9JqSWEjJ1r").unwrap();
let ata_program_addr: Pubkey = Pubkey::from_str("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL").unwrap();
let seeds = [
&sol_addr.to_bytes()[..],
&spl_token_addr.to_bytes()[..],
&token_addr.to_bytes()[..],
];
let (ata_addr ,_seed )= Pubkey::find_program_address(&seeds[..], &ata_program_addr);
println!("ata_addr is {}", ata_addr);
println!("_seed is {}", _seed);
}