【Solana】Anchor 示例:通过 CPI 实现 Sol 转账与手续费收取

  • 0xE
  • 发布于 1天前
  • 阅读 255

本文介绍了如何使用 Anchor 框架在 Solana 上通过跨程序调用(CPI)实现 SOL 转账,并在此基础上扩展手续费收取功能,包括完整的代码示例和测试流程。

本文将带领你从零开始,使用 Anchor 框架构建一个简单的 Solana 程序。我们将通过跨程序调用(CPI)的方式实现 Sol 的转账功能,并在此基础上扩展一个手续费收取的示例。通过本文,你将学会:

  1. 如何使用 Anchor 框架编写 Solana 程序
  2. 如何通过 CPI 调用 System Program 实现 Sol 转账
  3. 如何在转账过程中收取手续费

我们将从基础的环境搭建开始,逐步讲解代码的实现,并提供完整的命令操作步骤。只需按照文中的顺序执行命令,即可完成整个示例的部署与测试。

笔者使用的版本如下:

rustc 1.75.0 (82e1608df 2023-12-21)
solana-cli 1.18.17 (src:c027cfc3; feat:4215500110, client:Agave)
anchor-cli 0.30.1

Sol 转账示例

1. 初始化项目:

首先,使用 Anchor CLI 创建一个新的项目。运行以下命令:

anchor init transfer-sol

此命令会生成一个名为 transfer-sol 的 Anchor 项目,包含基本的项目结构和配置文件。

2. 替换代码

接下来,将 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 的账户信息,用于执行转账操作
}

3. 替换程序ID

anchor keys sync

这条命令用于同步程序ID。当你使用 anchor init 初始化项目时,系统会自动生成一个与程序对应的密钥对,并保存在项目目录中。刚刚粘贴的代码与项目中的程序ID不匹配,可以通过运行 anchor keys sync 命令来确保两者保持一致。

4. 编译

使用以下命令编译程序:

anchor build

该命令会将你的 Solana 程序编译为字节码,并生成相应的 IDL(接口描述语言)文件,以便后续与程序交互时使用。

5. 测试

将以下代码粘贴到 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)

Sol 转账并收取一定的手续费

修改 transferSolWithCpi,增加一个 fee_amount 参数,并在转账时扣除手续费。

1. 程序代码

#![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>,
}

2. 测试

测试脚本如下:

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)
  • 原创
  • 学分: 8
  • 分类: Solana
  • 标签:
点赞 1
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

1 条评论

请先 登录 后评论
0xE
0xE
0x59f6...a17e
17年进入币圈,Web3 开发者。刨根问底探链上真相,品味坎坷悟 Web3 人生。有工作机会可加v:__0xE__