指令与跨程序调用 (CPI)

指令与跨程序调用

指令结构

每个 Anchor 指令由三部分组成:

  1. Context:包含账户和程序信息
  2. 参数:指令的输入数据
  3. 返回值Result<()>Result<T>
rust
pub fn transfer( ctx: Context<Transfer>, // 账户上下文 amount: u64, // 指令参数 memo: String, // 可以有多个参数 ) -> Result<()> { // 指令逻辑 Ok(()) }

Context 结构

Context<T> 提供了丰富的信息访问:

rust
pub fn example(ctx: Context<Example>) -> Result<()> { // 访问账户 let account = &ctx.accounts.my_account; // 访问程序 ID let program_id = ctx.program_id; // 访问剩余账户 for remaining in ctx.remaining_accounts { // 处理... } // 访问 PDA bumps let bump = ctx.bumps.vault; Ok(()) }

指令鉴别器

Anchor 使用 8 字节鉴别器标识指令,默认由 sha256("global:<instruction_name>")[0..8] 计算:

rust
// 指令名使用 snake_case pub fn initialize_vault(ctx: Context<InitializeVault>) -> Result<()> { // sha256("global:initialize_vault")[0..8] Ok(()) }

自定义鉴别器:

rust
#[instruction(discriminator = [1, 2, 3, 4, 5, 6, 7, 8])] pub fn custom_instruction(ctx: Context<Custom>) -> Result<()> { Ok(()) }

自定义类型参数

使用自定义结构体作为参数:

rust
#[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct TransferParams { pub amount: u64, pub memo: String, } pub fn transfer_with_params( ctx: Context<Transfer>, params: TransferParams, ) -> Result<()> { msg!("转账 {} lamports,备注: {}", params.amount, params.memo); Ok(()) }

错误处理

定义清晰的自定义错误:

rust
#[error_code] pub enum VaultError { #[msg("金库余额不足")] InsufficientBalance, #[msg("金额必须大于零")] InvalidAmount, #[msg("未授权的操作")] Unauthorized, #[msg("金额超过最大限制: {0}")] AmountExceedsLimit(u64), }

在指令中使用:

rust
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> { let vault = &ctx.accounts.vault; require!(amount > 0, VaultError::InvalidAmount); require!(vault.balance >= amount, VaultError::InsufficientBalance); // 或者使用 if 语句 if amount > MAX_WITHDRAWAL { return Err(VaultError::AmountExceedsLimit(MAX_WITHDRAWAL).into()); } // 执行提款... Ok(()) }

跨程序调用(CPI)

CPI 允许一个程序调用另一个程序的指令,是 Solana 可组合性的基础。

基本 CPI

rust
use anchor_lang::system_program::{transfer, Transfer}; pub fn transfer_sol(ctx: Context<TransferSol>, amount: u64) -> Result<()> { let cpi_accounts = Transfer { from: ctx.accounts.sender.to_account_info(), to: ctx.accounts.recipient.to_account_info(), }; let cpi_program = ctx.accounts.system_program.to_account_info(); let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); transfer(cpi_ctx, amount)?; Ok(()) }

带 PDA 签名的 CPI

当 PDA 需要作为签名者时:

rust
pub fn transfer_from_vault(ctx: Context<TransferFromVault>, amount: u64) -> Result<()> { let vault = &ctx.accounts.vault; // 构造签名种子 let seeds = &[ b"vault".as_ref(), vault.authority.as_ref(), &[vault.bump], ]; let signer_seeds = &[&seeds[..]]; let cpi_accounts = Transfer { from: ctx.accounts.vault.to_account_info(), to: ctx.accounts.recipient.to_account_info(), }; let cpi_program = ctx.accounts.system_program.to_account_info(); let cpi_ctx = CpiContext::new_with_signer( cpi_program, cpi_accounts, signer_seeds ); transfer(cpi_ctx, amount)?; Ok(()) }

Token 转账 CPI

rust
use anchor_spl::token::{transfer, Transfer}; pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> { let cpi_accounts = Transfer { from: ctx.accounts.source.to_account_info(), to: ctx.accounts.destination.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }; let cpi_program = ctx.accounts.token_program.to_account_info(); let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); transfer(cpi_ctx, amount)?; Ok(()) }

调用其他 Anchor 程序

使用 declare_program! 宏实现类型安全的 CPI:

project/ ├── idls/ │ └── target_program.json # 目标程序的 IDL └── programs/ └── caller/ └── src/ └── lib.rs
rust
use anchor_lang::prelude::*; declare_id!("YourProgramID"); // 从 IDL 生成模块 declare_program!(target_program); use target_program::{ accounts::Counter, cpi::{self, accounts::Increment}, program::TargetProgram, }; #[program] pub mod caller { use super::*; pub fn call_increment(ctx: Context<CallIncrement>) -> Result<()> { let cpi_ctx = CpiContext::new( ctx.accounts.target_program.to_account_info(), Increment { counter: ctx.accounts.counter.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }, ); target_program::cpi::increment(cpi_ctx)?; Ok(()) } } #[derive(Accounts)] pub struct CallIncrement<'info> { #[account(mut)] pub counter: Account<'info, Counter>, pub authority: Signer<'info>, pub target_program: Program<'info, TargetProgram>, }

指令组织最佳实践

对于复杂程序,将指令逻辑分离到独立文件:

instructions/mod.rs:

rust
pub mod initialize; pub mod deposit; pub mod withdraw; pub use initialize::*; pub use deposit::*; pub use withdraw::*;

instructions/deposit.rs:

rust
use anchor_lang::prelude::*; use crate::state::Vault; use crate::errors::VaultError; #[derive(Accounts)] pub struct Deposit<'info> { #[account(mut)] pub vault: Account<'info, Vault>, #[account(mut)] pub depositor: Signer<'info>, pub system_program: Program<'info, System>, } impl<'info> Deposit<'info> { pub fn transfer_to_vault(&self, amount: u64) -> Result<()> { let cpi_accounts = anchor_lang::system_program::Transfer { from: self.depositor.to_account_info(), to: self.vault.to_account_info(), }; let cpi_ctx = CpiContext::new( self.system_program.to_account_info(), cpi_accounts ); anchor_lang::system_program::transfer(cpi_ctx, amount) } } pub fn handler(ctx: Context<Deposit>, amount: u64) -> Result<()> { require!(amount > 0, VaultError::InvalidAmount); ctx.accounts.transfer_to_vault(amount)?; let vault = &mut ctx.accounts.vault; vault.total_deposits = vault.total_deposits .checked_add(amount) .ok_or(VaultError::Overflow)?; msg!("存入 {} lamports", amount); Ok(()) }
RUSTPlayground
EDITOR ACTIVE
Initializing RUST Environment...

Anchor CPI 模拟器

调用方程序 (Vault)
let cpi_ctx = CpiContext::new_with_signer(
  cpi_program,
  cpi_accounts,
  signer_seeds
);
PDA 签名 (PDA Signature)
"vault"bump
Vault PDA
10 SOL
目标程序 (System Program)
等待调用 (Waiting for invocation)...
用户钱包 (User Wallet)
0 SOL
核心机制 (Key Mechanism): CPI (跨程序调用) 允许程序像搭积木一样组合。 当使用 new_with_signer 时,运行时会验证种子是否匹配 PDA,并且调用方是否是 PDA 的所有者,从而虚拟地代表没有私钥的地址“签署”交易。