🔀 Anchor的CPIs - 让程序之间愉快地聊天!
🎯 欢迎来到程序社交网络!
嘿,Solana架构师们!👋 还记得程序之间不能说话的时代吗?今天我们要学习CPI(跨程序调用) —— 让你的程序成为社交达人!
想象一下:
- 🏝️ 孤岛程序 = 自己玩自己的
- 🌉 CPI程序 = 可以调用其他程序,组成强大的DApp生态!
🎯 今日任务: 掌握Anchor中的CPI,让你的程序能够调用其他程序!
🗺️ 今日学习路线
┌──────────────────────────────────────┐
│ 🎯 CPI技能树 │
├──────────────────────────────────────┤
│ │
│ 1️⃣ CpiContext基础 🎪 │
│ └─ 构建CPI的上下文 │
│ │
│ 2️⃣ Token程序CPI 🪙 │
│ └─ 调用SPL Token程序 │
│ │
│ 3️⃣ 错误处理 🚨 │
│ └─ 优雅地处理CPI错误 │
│ │
│ 4️⃣ 高级约束 🔐 │
│ └─ init_if_needed等高级功能 │
│ │
└──────────────────────────────────────┘
🌉 CPI基础 - 程序间的桥梁
🤔 什么是CPI?
CPI就像是程序之间的电话线!一个程序可以"打电话"给另一个程序,请求它执行某些操作。
普通调用 vs CPI
┌─────────────────┐ ┌─────────────────┐
│ 客户端 💻 │ │ 程序A 🎮 │
│ ↓ │ │ ↓ │
│ 程序A 🎮 │ │ 程序B 🎯 │
└─────────────────┘ └─────────────────┘
单程序调用 跨程序调用
🎯 传统方式 vs Anchor方式
// 😰 传统方式:手动构建一切
use solana_program::program::invoke_signed;
// 手动构建指令
let instruction = /* 复杂的指令构建 */;
// 手动准备账户
let account_infos = /* 账户列表 */;
// 手动准备签名种子
let signer_seeds = /* PDA种子 */;
// 执行CPI
invoke_signed(&instruction, &account_infos, &signer_seeds)?;
// 😎 Anchor方式:优雅简洁
token::mint_to(
CpiContext::new_with_signer(/* 简单参数 */),
amount
)?;
🎪 CpiContext - CPI的灵魂
📦 CpiContext结构详解
/// 🎯 CpiContext - 包含CPI所需的一切信息
pub struct CpiContext<'a, 'b, 'c, 'info, T>
where
T: ToAccountMetas + ToAccountInfos<'info>,
{
/// 📦 CPI需要的账户列表
pub accounts: T,
/// 📋 额外的账户(如果有)
pub remaining_accounts: Vec<AccountInfo<'info>>,
/// 🎮 要调用的程序
pub program: AccountInfo<'info>,
/// 🔑 PDA签名种子(如果需要)
pub signer_seeds: &'a [&'b [&'c [u8]]],
}
🛠️ 两种创建方式
1️⃣ 不需要PDA签名的CPI
/// 🎯 简单CPI - 不需要PDA签名
pub fn simple_cpi_example(ctx: Context<SimpleCpi>) -> Result<()> {
msg!("🚀 准备执行简单CPI...");
// 📦 准备CPI上下文
let cpi_context = CpiContext::new(
ctx.accounts.target_program.to_account_info(), // 🎮 目标程序
TargetAccounts { // 📦 需要的账户
account_one: ctx.accounts.account_one.to_account_info(),
account_two: ctx.accounts.account_two.to_account_info(),
}
);
// 🎯 执行CPI
target_program::some_instruction(cpi_context, param1, param2)?;
msg!("✅ CPI执行成功!");
Ok(())
}
2️⃣ 需要PDA签名的CPI
/// 🔐 带签名的CPI - 使用PDA作为签名者
pub fn signed_cpi_example(ctx: Context<SignedCpi>) -> Result<()> {
msg!("🔑 准备使用PDA签名的CPI...");
// 🎲 获取bump(Anchor自动计算的)
let auth_bump = *ctx.bumps.get("mint_authority").unwrap();
// 🌱 准备签名种子
let seeds = &[
b"mint".as_ref(), // 固定种子
&[auth_bump], // bump值
];
let signer = &[&seeds[..]]; // 转换格式
// 📦 准备CPI上下文(带签名)
let cpi_context = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(), // 🎮 Token程序
MintTo { // 📦 铸币账户
mint: ctx.accounts.token_mint.to_account_info(),
to: ctx.accounts.token_account.to_account_info(),
authority: ctx.accounts.mint_authority.to_account_info()
},
signer // 🔑 PDA签名种子
);
// 🪙 执行铸币CPI
token::mint_to(cpi_context, 100)?;
msg!("✅ 成功铸造100个代币!");
Ok(())
}
🪙 Token程序CPI实战
📚 使用anchor_spl库
anchor_spl
提供了预置的Token程序CPI功能,让操作变得超级简单!
// 📦 在Cargo.toml中添加
// [dependencies]
// anchor-spl = "0.29.0"
use anchor_spl::token::{self, Token, TokenAccount, Mint, MintTo, Transfer};
🎯 完整的铸币示例
// 🏗️ 程序模块
#[program]
pub mod token_cpi_example {
use super::*;
/// 🪙 铸造代币到用户账户
pub fn mint_tokens(
ctx: Context<MintTokens>,
amount: u64, // 铸造数量
) -> Result<()> {
msg!("🎯 开始铸造 {} 个代币...", amount);
// 🔑 获取PDA的bump值
let bump = *ctx.bumps.get("mint_authority").unwrap();
// 🌱 构建签名种子
let signer_seeds = &[
b"mint_authority".as_ref(),
&[bump],
];
// 📦 构建CPI上下文
let cpi_context = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.token_account.to_account_info(),
authority: ctx.accounts.mint_authority.to_account_info(),
},
&[signer_seeds], // PDA签名
);
// 🎯 执行铸币CPI
token::mint_to(cpi_context, amount)?;
msg!("✅ 成功铸造 {} 个代币!", amount);
Ok(())
}
}
// 📦 账户结构
#[derive(Accounts)]
pub struct MintTokens<'info> {
/// 🪙 代币铸币账户
#[account(mut)]
pub mint: Account<'info, Mint>,
/// 💳 接收代币的账户
#[account(mut)]
pub token_account: Account<'info, TokenAccount>,
/// 🔑 铸币权限PDA
/// CHECK: 这是一个PDA,将在CPI中验证
#[account(
seeds = [b"mint_authority"],
bump
)]
pub mint_authority: UncheckedAccount<'info>,
/// 🎮 Token程序
pub token_program: Program<'info, Token>,
}
💸 转账示例(更简洁的写法)
/// 💸 从金库转账到用户
pub fn transfer_from_vault(
ctx: Context<TransferFromVault>,
amount: u64,
) -> Result<()> {
msg!("💰 从金库转账 {} 个代币...", amount);
// 🎯 单行完成CPI!
token::transfer(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.vault.to_account_info(),
to: ctx.accounts.user_token.to_account_info(),
authority: ctx.accounts.vault_authority.to_account_info(),
},
&[&[
b"vault",
&[*ctx.bumps.get("vault_authority").unwrap()],
]],
),
amount,
)?;
msg!("✅ 转账成功!");
Ok(())
}
🚨 错误处理 - 优雅地失败
🎨 Anchor错误类型
错误类型分类
┌─────────────────────────────────────┐
│ 1️⃣ 内部错误 - Anchor框架自带 │
│ 2️⃣ 自定义错误 - 你定义的 │
│ 3️⃣ 约束错误 - 账户验证失败 │
└─────────────────────────────────────┘
💡 定义自定义错误
/// 🚨 自定义错误枚举
#[error_code]
pub enum MyError {
/// 数据超过限制
#[msg("数据必须小于100!当前值太大了 😱")]
DataTooLarge,
/// 权限不足
#[msg("只有管理员能执行此操作 🔐")]
Unauthorized,
/// 余额不足
#[msg("余额不足!需要充值 💸")]
InsufficientFunds,
/// 账户已存在
#[msg("账户已经初始化过了 🎯")]
AccountAlreadyInitialized,
}
🛠️ 使用错误的三种方式
#[program]
mod error_examples {
use super::*;
/// 🎯 方式1:使用 err! 宏
pub fn method_one(ctx: Context<MyContext>, value: u64) -> Result<()> {
if value >= 100 {
// ❌ 返回错误
return err!(MyError::DataTooLarge);
}
msg!("✅ 值合法: {}", value);
Ok(())
}
/// 🎯 方式2:使用 require! 宏(推荐)
pub fn method_two(ctx: Context<MyContext>, value: u64) -> Result<()> {
// 🔍 require! 让代码更简洁
require!(value < 100, MyError::DataTooLarge);
msg!("✅ 值合法: {}", value);
Ok(())
}
/// 🎯 方式3:使用 require_eq! 检查相等
pub fn method_three(ctx: Context<MyContext>, expected: u64, actual: u64) -> Result<()> {
// 🔍 检查两个值是否相等
require_eq!(expected, actual, MyError::ValueMismatch);
msg!("✅ 值匹配!");
Ok(())
}
}
🎨 错误处理最佳实践
/// 💡 组合多个验证
pub fn complex_validation(
ctx: Context<ComplexValidation>,
amount: u64,
user_level: u8,
) -> Result<()> {
msg!("🔍 开始验证...");
// 1️⃣ 检查金额范围
require!(
amount >= 10 && amount <= 1000,
MyError::InvalidAmount
);
// 2️⃣ 检查用户等级
require!(
user_level >= 2,
MyError::InsufficientLevel
);
// 3️⃣ 检查账户余额
let balance = ctx.accounts.user_account.balance;
require!(
balance >= amount,
MyError::InsufficientFunds
);
msg!("✅ 所有验证通过!");
// 执行业务逻辑...
Ok(())
}
🔐 高级约束 - init_if_needed
🎯 智能初始化
init_if_needed
让账户初始化变得更智能 —— 如果账户不存在就创建,存在就跳过!
/// 🎯 关联代币账户的智能初始化
#[derive(Accounts)]
pub struct SmartInitialize<'info> {
/// 🪙 智能初始化代币账户
/// 如果不存在,自动创建!
#[account(
init_if_needed, // 🎯 智能初始化
payer = payer, // 💰 付款人
associated_token::mint = mint, // 🪙 关联的铸币
associated_token::authority = payer // 🔑 账户权限
)]
pub token_account: Account<'info, TokenAccount>,
/// 🪙 代币铸币
pub mint: Account<'info, Mint>,
/// 👤 付款人
#[account(mut)]
pub payer: Signer<'info>,
/// ⚙️ 必需的程序
pub system_program: Program<'info, System>,
pub token_program: Program<'info, Token>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub rent: Sysvar<'info, Rent>,
}
⚠️ 重要提示: 使用
init_if_needed
需要在Cargo.toml
中启用特性:
[dependencies]
anchor-lang = { version = "0.29.0", features = ["init-if-needed"] }
💡 init_if_needed的工作原理
// 🔍 Anchor生成的代码(简化版)
// 这就是 init_if_needed 背后的魔法!
if account_not_initialized {
msg!("📝 账户不存在,创建新账户...");
// 自动创建关联代币账户
let cpi_context = CpiContext::new(
associated_token_program,
Create {
payer: payer,
associated_token: token_account,
authority: payer,
mint: mint,
system_program: system_program,
token_program: token_program,
rent: rent,
}
);
// 执行创建CPI
associated_token::create(cpi_context)?;
msg!("✅ 账户创建成功!");
} else {
msg!("✅ 账户已存在,跳过创建");
}
💎 实用技巧与最佳实践
🎯 技巧1:CPI账户转换助手
/// 🔧 创建账户转换trait,让代码更简洁
trait ToAccountInfo<'info> {
fn to_account_info(&self) -> AccountInfo<'info>;
}
// 为常用类型实现trait
impl<'info> ToAccountInfo<'info> for Account<'info, TokenAccount> {
fn to_account_info(&self) -> AccountInfo<'info> {
self.to_account_info()
}
}
🎯 技巧2:CPI错误处理包装器
/// 🛡️ 安全的CPI执行器
pub fn safe_cpi_execute<F, T>(
operation: &str,
cpi_fn: F,
) -> Result<T>
where
F: FnOnce() -> Result<T>,
{
msg!("🚀 执行CPI: {}", operation);
match cpi_fn() {
Ok(result) => {
msg!("✅ {} 成功!", operation);
Ok(result)
}
Err(e) => {
msg!("❌ {} 失败: {:?}", operation, e);
Err(e)
}
}
}
// 使用示例
safe_cpi_execute("铸造代币", || {
token::mint_to(cpi_ctx, amount)
})?;
🎯 技巧3:批量CPI操作
/// 🚀 批量转账示例
pub fn batch_transfer(
ctx: Context<BatchTransfer>,
recipients: Vec<Pubkey>,
amounts: Vec<u64>,
) -> Result<()> {
require_eq!(
recipients.len(),
amounts.len(),
MyError::LengthMismatch
);
msg!("📦 开始批量转账到 {} 个账户", recipients.len());
for (i, (recipient, amount)) in recipients.iter().zip(amounts.iter()).enumerate() {
msg!("💸 转账 {}: {} 个代币到 {}", i + 1, amount, recipient);
// 执行转账CPI
token::transfer(
CpiContext::new(
ctx.accounts.token_program.to_account_info(),
Transfer {
from: ctx.accounts.source.to_account_info(),
to: /* 获取接收者的代币账户 */,
authority: ctx.accounts.authority.to_account_info(),
}
),
*amount,
)?;
}
msg!("🎉 批量转账完成!");
Ok(())
}
🎓 知识总结
📚 CPI技能清单
┌────────────────────────────────────┐
│ 🏆 CPI大师技能解锁 │
├────────────────────────────────────┤
│ ✅ 理解CpiContext概念 │
│ ✅ 创建简单CPI │
│ ✅ 使用PDA签名的CPI │
│ ✅ Token程序CPI操作 │
│ ✅ 自定义错误处理 │
│ ✅ 使用高级约束 │
└────────────────────────────────────┘
🎯 记忆口诀
🎵 "CPI搭桥梁,Context装参数, PDA要签名,错误用require" 🎵
🚀 实战练习
挑战1:创建代币交换程序
// 🎯 实现一个简单的代币交换
// 用户存入代币A,获得代币B
挑战2:构建多重签名钱包
// 🎯 需要多个签名才能执行CPI
// 提示:使用多个signer验证
🎊 恭喜完成!
你已经掌握了Anchor CPI的核心技能!🎉 现在你可以:
- 🌉 让程序之间自由通信
- 🪙 调用Token程序进行代币操作
- 🛡️ 优雅地处理错误
- 🔐 使用高级约束简化开发
💬 记住: CPI让Solana生态系统变成了乐高积木,你可以自由组合创造无限可能!
准备好构建下一个DeFi乐高了吗?让我们继续前进! 🚀🔀✨