本文介绍了如何使用 Anchor 框架在 Solana 上通过跨程序调用(CPI)实现 SOL 转账,并在此基础上扩展手续费收取功能,包括完整的代码示例和测试流程。
本文将带领你从零开始,使用 Anchor 框架构建一个简单的 Solana 程序。我们将通过跨程序调用(CPI)的方式实现 Sol 的转账功能,并在此基础上扩展一个手续费收取的示例。通过本文,你将学会:
我们将从基础的环境搭建开始,逐步讲解代码的实现,并提供完整的命令操作步骤。只需按照文中的顺序执行命令,即可完成整个示例的部署与测试。
笔者使用的版本如下:
rustc 1.75.0 (82e1608df 2023-12-21)
solana-cli 1.18.17 (src:c027cfc3; feat:4215500110, client:Agave)
anchor-cli 0.30.1
首先,使用 Anchor CLI 创建一个新的项目。运行以下命令:
anchor init transfer-sol
此命令会生成一个名为 transfer-sol 的 Anchor 项目,包含基本的项目结构和配置文件。
接下来,将 lib.rs 文件中的代码替换为以下内容。这段代码实现了通过 CPI 调用 System Program 进行 Sol 转账的功能。为了帮助新手更好地理解,代码中附带了详细的注释:
#![allow(clippy::result_large_err)] // 允许编译器忽略关于大错误类型的警告
use anchor_lang::prelude::*; // 引入 Anchor 框架的预导入模块,包含常用的类型和宏
use anchor_lang::system_program; // 引入 Anchor 框架的 System Program 模块,用于处理系统级别的操作
declare_id!("Akde2QFpSYkE8Ejc8X4XnxZQb4cyJdN43uzA59gstgNP"); // 定义程序的唯一 ID
#[program] // 标记下面的模块为 Anchor 程序的入口
pub mod transfer_sol { // 定义名为 transfer_sol 的模块
use super::*; // 引入父模块的所有内容
// CPI 方式:调用 System Program 的 transfer 方法进行转账。
pub fn transfer_sol_with_cpi(ctx: Context<TransferSolWithCpi>, amount: u64) -> Result<()> {
// 使用 system_program::transfer 方法进行跨程序调用 (CPI) 转账
system_program::transfer(
// 创建一个新的 CPI 上下文
CpiContext::new(
// 传入 System Program 的账户信息
ctx.accounts.system_program.to_account_info(),
// 传入转账的账户信息
system_program::Transfer {
from: ctx.accounts.payer.to_account_info(), // 转账的发起方
to: ctx.accounts.recipient.to_account_info(), // 转账的接收方
},
),
amount, // 转账的金额
)?; // 如果转账失败,返回错误
Ok(()) // 如果转账成功,返回 Ok
}
}
#[derive(Accounts)] // 标记下面的结构体为账户集合,用于定义指令所需的账户
pub struct TransferSolWithCpi<'info> {
#[account(mut)] // 标记 payer 账户为可变,因为转账会改变其余额
payer: Signer<'info>, // 转账的发起方,必须是一个签名者
#[account(mut)] // 标记 recipient 账户为可变,因为转账会改变其余额
recipient: SystemAccount<'info>, // 转账的接收方,必须是一个系统账户
system_program: Program<'info, System>, // System Program 的账户信息,用于执行转账操作
}
anchor keys sync
这条命令用于同步程序ID。当你使用 anchor init
初始化项目时,系统会自动生成一个与程序对应的密钥对,并保存在项目目录中。刚刚粘贴的代码与项目中的程序ID不匹配,可以通过运行 anchor keys sync
命令来确保两者保持一致。
使用以下命令编译程序:
anchor build
该命令会将你的 Solana 程序编译为字节码,并生成相应的 IDL(接口描述语言)文件,以便后续与程序交互时使用。
将以下代码粘贴到 tests/transfer-sol.ts
文件中:
import * as anchor from '@coral-xyz/anchor';
import { Keypair, LAMPORTS_PER_SOL, type PublicKey, SystemProgram, Transaction, sendAndConfirmTransaction } from '@solana/web3.js';
import type { TransferSol } from '../target/types/transfer_sol';
describe('transfer-sol', () => {
// 使用 AnchorProvider.env() 读取本地环境变量(ANCHOR_WALLET 和 ANCHOR_PROVIDER_URL)来初始化 Solana 提供者。
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const payer = provider.wallet as anchor.Wallet;
// 通过 Anchor 加载 transfer_sol 合约,并转换成 泛型类型 Program<TransferSol>,这样可以正确推断方法和账户结构。
const program = anchor.workspace.TransferSol as anchor.Program<TransferSol>;
// 设定转账金额为 1 SOL(1 SOL = 10^9 Lamports)
const transferAmount = 1 * LAMPORTS_PER_SOL;
// 生成一个新的 Keypair,用于接收转账。
const recipient = new Keypair();
// 使用 System Program(CPI)转账 SOL, 付款人可以是任意账户(只要签名交易)。
it('Transfer SOL with CPI', async () => {
// 查询并打印转账前的余额(getBalances)
await getBalances(payer.publicKey, recipient.publicKey, 'Beginning');
await program.methods
.transferSolWithCpi(new anchor.BN(transferAmount)) // 将 transferAmount(1 SOL)转换为 BN 格式(Anchor 需要)
.accounts({ // 指定付款人和收款人
payer: payer.publicKey,
recipient: recipient.publicKey,
})
.rpc(); // 发送交易并等待执行完成
// 查询并打印转账后的余额
await getBalances(payer.publicKey, recipient.publicKey, 'Resulting');
});
async function getBalances(payerPubkey: PublicKey, recipientPubkey: PublicKey, timeframe: string) {
const payerBalance = await provider.connection.getBalance(payerPubkey);
const recipientBalance = await provider.connection.getBalance(recipientPubkey);
console.log(`${timeframe} balances:`);
console.log(` Payer: ${payerBalance / LAMPORTS_PER_SOL}`);
console.log(` Recipient: ${recipientBalance / LAMPORTS_PER_SOL}`);
}
});
运行本地测试:
anchor test
测试结果为:
transfer-sol
Beginning balances:
Payer: 500000000
Recipient: 0
Resulting balances:
Payer: 499999998.999995
Recipient: 1
✔ Transfer SOL with CPI (573ms)
修改 transferSolWithCpi,增加一个 fee_amount 参数,并在转账时扣除手续费。
#![allow(clippy::result_large_err)]
use anchor_lang::prelude::*;
use anchor_lang::system_program;
declare_id!("Akde2QFpSYkE8Ejc8X4XnxZQb4cyJdN43uzA59gstgNP");
#[program]
pub mod transfer_sol {
use super::*;
pub fn transfer_sol_with_fee(
ctx: Context<TransferSolWithFee>,
amount: u64,
fee_amount: u64,
) -> Result<()> {
// 执行用户的转账(实际转账金额 = amount)
system_program::transfer(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.recipient.to_account_info(),
},
),
amount,
)?;
// 扣除手续费并发送给 fee_receiver
system_program::transfer(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.fee_receiver.to_account_info(),
},
),
fee_amount,
)?;
Ok(())
}
}
#[derive(Accounts)]
pub struct TransferSolWithFee<'info> {
#[account(mut)]
payer: Signer<'info>, // 付款人(调用者)
#[account(mut)]
recipient: SystemAccount<'info>, // 实际收款人
#[account(mut)]
fee_receiver: SystemAccount<'info>, // 手续费接收者(合约所有者或预定义地址)
system_program: Program<'info, System>,
}
测试脚本如下:
import * as anchor from '@coral-xyz/anchor';
import { Keypair, LAMPORTS_PER_SOL, type PublicKey, SystemProgram, Transaction, sendAndConfirmTransaction } from '@solana/web3.js';
import type { TransferSol } from '../target/types/transfer_sol';
describe('transfer-sol', () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const payer = provider.wallet as anchor.Wallet;
const program = anchor.workspace.TransferSol as anchor.Program<TransferSol>;
// 设定转账金额(1 SOL)和手续费(0.01 SOL)。
const transferAmount = 1 * LAMPORTS_PER_SOL;
const feeAmount = 0.01 * LAMPORTS_PER_SOL;
const recipient = new Keypair();
// 生成手续费接收者 Keypair(也可以使用一个固定公钥)
const feeReceiver = new Keypair();
it('Transfer SOL with fee', async () => {
await getBalances(payer.publicKey, recipient.publicKey, feeReceiver.publicKey, 'Beginning');
await program.methods
.transferSolWithFee(new anchor.BN(transferAmount), new anchor.BN(feeAmount))
.accounts({
payer: payer.publicKey,
recipient: recipient.publicKey,
feeReceiver: feeReceiver.publicKey,
})
.rpc();
await getBalances(payer.publicKey, recipient.publicKey, feeReceiver.publicKey, 'Resulting');
});
async function getBalances(
payerPubkey: PublicKey,
recipientPubkey: PublicKey,
feeReceiverPubkey: PublicKey,
timeframe: string
) {
const payerBalance = await provider.connection.getBalance(payerPubkey);
const recipientBalance = await provider.connection.getBalance(recipientPubkey);
const feeReceiverBalance = await provider.connection.getBalance(feeReceiverPubkey);
console.log(`${timeframe} balances:`);
console.log(` Payer: ${payerBalance / LAMPORTS_PER_SOL}`);
console.log(` Recipient: ${recipientBalance / LAMPORTS_PER_SOL}`);
console.log(` Fee Receiver: ${feeReceiverBalance / LAMPORTS_PER_SOL}`);
}
});
测试结果:
Beginning balances:
Payer: 500000000
Recipient: 0
Fee Receiver: 0
Resulting balances:
Payer: 499999998.989995
Recipient: 1
Fee Receiver: 0.01
✔ Transfer SOL with fee (258ms)
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!