跨程序调用 (Cross Program Invocation, 简称 CPI) 是 Solana 中的术语,指的是一个程序调用另一个程序的公开函数。
之前在发送 向 System Program 转账 SOL 的交易 时,我们就已经使用过 CPI 了。为了唤起你的记忆,这里提供相关的代码片段:
pub fn send_sol(ctx: Context<SendSol>, amount: u64) -> Result<()> {
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.signer.to_account_info(),
to: ctx.accounts.recipient.to_account_info(),
}
);
let res = system_program::transfer(cpi_context, amount);
if res.is_ok() {
return Ok(());
} else {
return err!(Errors::TransferFailed);
}
}
CpiContext 中的 Cpi 字面意思就是“跨程序调用”。
调用除 System Program 之外的其他程序的公开函数的工作流程并没有太大区别——我们将在本教程中教授这一点。
本教程仅重点介绍如何调用另一个使用 Anchor 构建的 Solana 程序。如果另一个程序是使用原生 Rust 开发的,那么以下指南将不适用。
在我们的贯穿示例中,Alice 程序将调用 Bob 程序上的一个函数。
Bob 程序
我们首先使用 Anchor 的 CLI 创建一个新项目:
anchor init bob
然后将下面的代码复制粘贴到 bob/lib.rs 中。该程序有两个函数,一个是初始化一个用于保存 u64 数据的存储账户,另一个是 add_and_store 函数,它接收两个 u64 变量,将它们相加,并将结果存储在由 BobData 结构体定义的账户中。
use anchor_lang::prelude::*;
use std::mem::size_of;
// REPLACE WITH YOUR <PROGRAM_ID>declare_id!("8GYu5JYsvAYoinbFTvW4AACYB5GxGstz21FmZe3MNFn4");
#[program]
pub mod bob {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
msg!("Data Account Initialized: {}", ctx.accounts.bob_data_account.key());
Ok(())
}
pub fn add_and_store(ctx: Context<BobAddOp>, a: u64, b: u64) -> Result<()> {
let result = a + b;
// MODIFY/UPDATE THE DATA ACCOUNT
ctx.accounts.bob_data_account.result = result;
Ok(())
}
}
#[account]
pub struct BobData {
pub result: u64,
}
#[derive(Accounts)]
pub struct BobAddOp<'info> {
#[account(mut)]
pub bob_data_account: Account<'info, BobData>,
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = signer, space = size_of::<BobData>() + 8)]
pub bob_data_account: Account<'info, BobData>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
本教程的目标是创建另一个程序 alice,用来调用 bob.add_and_store。
在当前项目 (bob) 目录下,使用 anchor new 命令创建一个新程序:
anchor new alice
命令行应该会打印出 Created new program。
在开始编写 Alice 程序之前,必须将下面的代码片段添加到 Alice 的 Cargo.toml 文件 (programs/alice/Cargo.toml) 的 [dependencies] 部分中。
[dependencies]
bob = {path = "../bob", features = ["cpi"]}
在这里,Anchor 在后台做了大量工作。Alice 现在可以访问 Bob 的公开函数和结构体的定义。你可以把它类比为在 Solidity 中导入接口,这样我们就能知道如何与另一个合约进行交互了。
下面我们展示 Alice 程序。在顶部,Alice 程序导入了承载 BobAddOp 账户的结构体(用于 add_and_store)。请注意代码中的注释:
use anchor_lang::prelude::*;
// account struct for add_and_store
use bob::cpi::accounts::BobAddOp;
// The program definition for Bob
use bob::program::Bob;
// the account where Bob is storing the sum
use bob::BobData;
declare_id!("6wZDNWprmb9TAZYMAPpT23kHDPABvBLT8jbWQKLHEmBy");
#[program]
pub mod alice {
use super::*;
pub fn ask_bob_to_add(ctx: Context<AliceOp>, a: u64, b: u64) -> Result<()> {
let cpi_ctx = CpiContext::new(
ctx.accounts.bob_program.to_account_info(),
BobAddOp {
bob_data_account: ctx.accounts.bob_data_account.to_account_info(),
}
);
let res = bob::cpi::add_and_store(cpi_ctx, a, b);
// return an error if the CPI failed
if res.is_ok() {
return Ok(());
} else {
return err!(Errors::CPIToBobFailed);
}
}
}
#[error_code]
pub enum Errors {
#[msg("cpi to bob failed")]
CPIToBobFailed,
}
#[derive(Accounts)]
pub struct AliceOp<'info> {
#[account(mut)]
pub bob_data_account: Account<'info, BobData>,
pub bob_program: Program<'info, Bob>,
}
如果我们将 ask_bob_to_add 与本文开头展示如何转账 SOL 的代码片段进行比较,会发现它们有很多相似之处。

要执行 CPI,需要满足以下条件:
- 目标程序的引用(作为
AccountInfo)(红框) - 目标程序上运行该函数所需的账户列表(包含所有账户的
ctx结构体)(绿框) - 传递给该函数的参数(橙框)
测试 CPI
可以使用以下 Typescript 代码来测试该 CPI:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Bob } from "../target/types/bob";
import { Alice } from "../target/types/alice";
import { expect } from "chai";
describe("CPI from Alice to Bob", () => {
const provider = anchor.AnchorProvider.env();
// Configure the client to use the local cluster.
anchor.setProvider(provider);
const bobProgram = anchor.workspace.Bob as Program<Bob>;
const aliceProgram = anchor.workspace.Alice as Program<Alice>;
const dataAccountKeypair = anchor.web3.Keypair.generate();
it("Is initialized!", async () => {
// Add your test here.
const tx = await bobProgram.methods
.initialize()
.accounts({
bobDataAccount: dataAccountKeypair.publicKey,
signer: provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([dataAccountKeypair])
.rpc();
});
it("Can add numbers then double!", async () => {
// Add your test here.
const tx = await aliceProgram.methods
.askBobToAdd(new anchor.BN(4), new anchor.BN(2))
.accounts({
bobDataAccount: dataAccountKeypair.publicKey,
bobProgram: bobProgram.programId,
})
.rpc();
});
it("Can assert value in Bob's data account equals 4 + 2", async () => {
const BobAccountValue = (
await bobProgram.account.bobData
.fetch(dataAccountKeypair.publicKey)
).result.toNumber();
expect(BobAccountValue).to.equal(6);
});
});
单行代码实现 CPI
因为传递给 Alice 的 ctx 账户包含了我们执行交易所需的所有账户的引用,我们可以为该结构体在一个 impl 块中创建一个函数来完成 CPI。记住,所有 impl 都会将函数“附加”到一个结构体上,从而可以使用该结构体中的数据。既然 ctx 结构体 AliceOp 已经持有了 Bob 执行该交易所需要的所有账户,我们可以将所有 CPI 代码:
let cpi_ctx = CpiContext::new(
ctx.accounts.bob_program.to_account_info(),
BobAddOp {
bob_data_account: ctx.accounts.bob_data_account.to_account_info(),
}
);
像这样移入一个 impl 中:
let cpi_ctx = CpiContext::new(
ctx.accounts.bob_program.to_account_info(),
BobAddOp {
bob_data_account: ctx.accounts.bob_data_account.to_account_info(),
}
);
use anchor_lang::prelude::*;
use bob::cpi::accounts::BobAddOp;
use bob::program::Bob;
use bob::BobData;
// REPLACE WITTH YOUR <PROGRAM_ID>declare_id!("B2BNs2GecG8Ux5EchDDFZakRWX2NDfy1RDhPCTJuJtr5");
#[program]
pub mod alice {
use super::*;
pub fn ask_bob_to_add(ctx: Context<AliceOp>, a: u64, b: u64) -> Result<()> {
// Calls the `bob_add_operation` function in bob program
let res = bob::cpi::bob_add_operation(ctx.accounts.add_function_ctx(), a, b);
if res.is_ok() {
return Ok(());
} else {
return err!(Errors::CPIToBobFailed);
}
}
}
impl<'info> AliceOp<'info> {
pub fn add_function_ctx(&self) -> CpiContext<'_, '_, '_, 'info, BobAddOp<'info>> {
// The bob program we are interacting with
let cpi_program = self.bob_program.to_account_info();
// Passing the necessary account(s) to the `BobAddOp` account struct in Bob program
let cpi_account = BobAddOp {
bob_data_account: self.bob_data_account.to_account_info(),
};
// Creates a `CpiContext` object using the new method
CpiContext::new(cpi_program, cpi_account)
}
}
#[error_code]
pub enum Errors {
#[msg("cpi to bob failed")]
CPIToBobFailed,
}
#[derive(Accounts)]
pub struct AliceOp<'info> {
#[account(mut)]
pub bob_data_account: Account<'info, BobData>,
pub bob_program: Program<'info, Bob>,
}
我们现在能够在“单行代码”中对 Bob 发起 CPI 调用。如果 Alice 程序的其他部分也需要对 Bob 发起 CPI,这将会非常方便——将代码移到 impl 中可以避免我们到处复制粘贴创建 CpiContext 的代码。
通过 RareSkills 了解更多
本教程是 学习 Solana 开发 系列的一部分。
初版发布于 2024 年 5 月 17 日