本文将详细讲解如何在 Anchor 框架中实现 CPI,重点展示调用另一个 Anchor 程序的过程,并以 Alice 调用 Bob 的示例说明。
跨程序调用(CPI)是指在 Solana 中一个程序调用另一个程序的公共函数。本文将详细讲解如何在 Anchor 框架中实现 CPI,重点展示调用另一个 Anchor 程序的过程,并以 Alice 调用 Bob 的示例说明。注意,本教程适用于 Anchor 程序,不适用于纯 Rust 开发的程序。
CPI 的典型示例是向系统程序转账 SOL:
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 表示跨程序调用,调用其他程序的逻辑与之类似。
首先创建 Bob 程序:
anchor init bob
将以下代码写入 bob/lib.rs:
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("BN2aLyQ8UJozgebuDVdwhY47VZA2RbS3HDRDz5zyYQqw");
#[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;
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>,
}
Bob 提供两个函数:initialize 初始化存储账户,add_and_store 将两个 u64 相加并存储。
在 bob 项目中创建 Alice 程序:
anchor new alice
编辑 programs/alice/Cargo.toml,添加依赖:
[dependencies]
bob = { path = "../bob", features = ["cpi"] }
此依赖使 Alice 能访问 Bob 的函数和结构体,类似 Solidity 中的接口导入。
Alice 的完整代码如下:
use anchor_lang::prelude::*;
use bob::cpi::accounts::BobAddOp;
use bob::program::Bob;
use bob::BobData;
declare_id!("GaKz6s9cpFBNMTM56TE3uQLMJ3YefZp4VFiUnCkQT8fU");
#[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);
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>,
}
与转账 SOL 的代码对比:
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();
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 () => {
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!", async () => {
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);
});
});
通过在 AliceOp 上实现 impl,可简化 CPI:
use anchor_lang::prelude::*;
use bob::cpi::accounts::BobAddOp;
use bob::program::Bob;
use bob::BobData;
declare_id!("GaKz6s9cpFBNMTM56TE3uQLMJ3YefZp4VFiUnCkQT8fU");
#[program]
pub mod alice {
use super::*;
pub fn ask_bob_to_add(ctx: Context<AliceOp>, a: u64, b: u64) -> Result<()> {
let res = bob::cpi::add_and_store(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>> {
let cpi_program = self.bob_program.to_account_info();
let cpi_account = BobAddOp {
bob_data_account: self.bob_data_account.to_account_info(),
};
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>,
}
此方法将 CPI 封装为一行,避免重复代码,提升可维护性。
此机制适用于 Anchor 程序间的交互,确保高效的链上操作。
【笔记配套代码】 https://github.com/0xE1337/rareskills_evm_to_solana 【参考资料】 https://learnblockchain.cn/column/119 https://www.rareskills.io/solana-tutorial
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!