Solana笔记 08.案例:写一个SOL转账程序

跟我一起从0开始学习Solana合约开发,一起实操,一起做项目。这是一个系列文章,系统地记录了我的学习笔记。

在之前的文章中,我们探讨了 Solana 账户的基础理论并进行了实操。然而,为了进一步加深理解,我们需要结合实际案例来应用这些知识。今天,我们将用两种方法实现账户之间的 SOL 转账。相信通过这个真实场景的学习,你一定能够更加透彻地掌握 Solana 的账户模型和转账机制。

在开始之前,请先思考一个问题:我写的程序可以直接从我的钱包里转出 SOL 吗? 带着这个问题,让我们开始今天的学习。

如何转出 SOL

回顾一下 Solana 的账户模型:每个账户都有一个 owner 字段,标明该账户受哪个程序管理。只有账户的 owner 程序可以对账户执行操作,比如减少余额或修改数据。

这意味着,如果我们希望从自己的钱包里转出 SOL,只有系统程序(SystemProgram)能够直接操作我们的钱包账户。我们自己编写的程序无法直接控制钱包余额。

那么问题来了:如果我们想编写一个程序来转账 SOL,该如何实现呢? 有两种方法:一种是创建受程序管理的账户,另一种是使用 CPI(跨程序调用)

创建受程序管理的账户

我们可以创建一个新账户,并将其 owner 设置为我们自己编写的程序。通过这种方式,我们的程序就可以完全控制该账户,执行各种操作,实现转账等功能。

实现过程概述

  1. 创建一个新账户,命名为 programOwnedAccount。

  2. 设置账户的 owner 为程序,将账户的权限交给我们自定义的程序。

  3. 向新账户存入 SOL,将其作为一个“中转账户”。

  4. 实现转账逻辑,从 programOwnedAccount 转出 SOL 到接收者的钱包。

这种方式有效避免了程序直接操作用户钱包的限制,同时保证账户的安全和程序的可控性。

创建受程序管理的账户

理清了这个过程,接下来我们就可以写代码了。

打开 tests/{your-program}.ts 文件,我们将通过调用 Solana 的 SystemProgram.createAccount 方法来实现账户的创建。以下是具体代码:

it('Create and fund account owned by our program', async () => {
    const instruction = SystemProgram.createAccount({
      fromPubkey: payer.publicKey, // 我的钱包地址
      newAccountPubkey: programOwnedAccount.publicKey, // 新账户地址
      space: 0, // 新账户的存储空间 (0 表示不存储额外数据)
      lamports: 1 * LAMPORTS_PER_SOL, // 存入新账户的初始余额 (1 SOL)
      programId: program.programId, // 设置 owner 为自定义程序的 ID
    });

    const transaction = new Transaction().add(instruction);

    // 发送交易并确认结果
    await sendAndConfirmTransaction(provider.connection, transaction, [payer.payer, programOwnedAccount]);
});

通过这段代码,我们创建了一个受程序管理的账户,并存入了 1 SOL 用作转账。需要注意的是在最后发送交易时,参数 [payer.payer, programOwnedAccount] 指明了签名账户,用户的钱包签名表示授权转账,programOwnedAccount 签名则激活新账户

转账逻辑的实现

完成账户创建后,我们可以实现从 programOwnedAccount 到接收者钱包的转账逻辑。

打开 src/lib.rs 文件,编写以下代码:

注意:以下代码示例默认你已经看过了之前的文章,具备了账户的实操能力。如果你感到不理解下面的代码,请回过头阅读我之前的文章。

  1. 定义账户上下文
#[derive(Accounts)]
pub struct TransferSolWithProgram<'info> {
    #[account(mut, owner = id())]
    payer: UncheckedAccount<'info>, // 支付账户
    #[account(mut)]
    recipient: SystemAccount<'info>, // 接收账户
}

在账户上下文中,我们可以看到 payerrecipient 两个字段的定义。分别使用了 UncheckedAccountSystemAccount 两种类型,分别对应不同的账户类型和用途。

payer 字段

#[account(mut, owner = id())] 的含义如下:

  • mut:表示这个账户的余额会被修改。

  • owner = id():确保 payer 是由当前程序(通过 id() 获取的程序 ID)管理的账户,即一个受程序控制的账户。

  • UncheckedAccount:表明这个账户不需要额外的数据验证。因为 payer 是我们之前创建并初始化的中转账户 (programOwnedAccount),已经明确由程序控制,所以无需额外检查。

需要注意的是,这里没有使用 init,是因为 payer 是一个已存在的账户,并且在创建时已完成了初始化。因此,不需要重复初始化。

recipient 字段

#[account(mut)] 的含义如下:

  • mut:表示该账户的余额会被修改,即接收 SOL。

  • SystemAccount:标明这个账户的所有者是系统程序(SystemProgram),它是普通用户钱包的默认类型。

  1. 编写转账函数
#[program]
pub mod transfer_sol {
    use super::*;

    pub fn transfer_sol_with_program(ctx: Context<TransferSolWithProgram>, amount: u64) -> Result<()> {
        **ctx.accounts.payer.try_borrow_mut_lamports()? -= amount;
        **ctx.accounts.recipient.try_borrow_mut_lamports()? += amount;
        Ok(())
    }
}

代码解析

  • try_borrow_mut_lamports:获取账户余额的可变引用,允许我们直接操作账户的余额。

  • ** 运算符:解引用以访问账户的 lamports 字段,从而操作账户余额。错误通过 ? 运算符向上传递,要求调用者处理错误。

测试代码示例

我们通过以下测试代码验证程序的正确性:

it('Transfer SOL with Program', async () => {
    // 打印转账前的余额

    await program.methods
      .transferSolWithProgram(new anchor.BN(transferAmount))
      .accounts({
        payer: programOwnedAccount.publicKey,
        recipient: payer.publicKey,
      })
      .rpc();

    // 打印转账后的余额
});

使用 CPI(跨程序调用)

Solana 提供了一个强大的机制,称为 CPI(Cross-Program Invocation,跨程序调用)。通过 CPI,我们的程序可以调用其他程序的功能,包括系统程序。利用这一机制,我们可以在自己的程序中调用系统程序,间接操作钱包余额,实现更复杂的功能。

具体实现过程

  1. 编写转账程序,通过 CPI 调用系统程序的 transfer 方法。

  2. 执行转账逻辑,从钱包直接转出 SOL 到接收者的钱包。

编写转账程序

在上面的 src/lib.rs 文件里,我们定义一个函数,调用系统程序的 transfer 方法来实现转账功能。代码如下:

pub fn transfer_sol_with_cpi(ctx: Context<TransferSolWithCpi>, amount: u64) -> Result<()> {
    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,
    )?;

    Ok(())
}

在以上代码中,我们使用 system_program::transfer 通过 CPI 机制从 payer 账户向 recipient 账户转账。CpiContext::new 用于构造一个新的上下文,它包含了系统程序的账户信息和两个账户的地址(payer 和 recipient)。

另外,在代码中使用系统程序时,需要引入它。你可以通过以下方式来导入 system_program

use anchor_lang::system_program;

为了让程序能够与系统程序交互,我们需要在账户上下文中添加一个字段来引用 system_program。这是因为我们需要告诉 Solana 哪个程序提供了 transfer 方法。以下是账户上下文代码:

#[derive(Accounts)]
pub struct TransferSolWithCpi<'info> {
    #[account(mut)]
    payer: Signer<'info>,
    #[account(mut)]
    recipient: SystemAccount<'info>,
    system_program: Program<'info, System>,
}

编写测试代码

为了验证转账功能,我们需要编写相应的测试代码。以下是一个简单的测试示例,展示如何在程序中调用 transfer_sol_with_cpi 方法进行转账:

it('Transfer SOL with CPI', async () => {

    // 打印转账前的信息: payer余额、recipient

    await program.methods
      .transferSolWithCpi(new anchor.BN(transferAmount))
      .accounts({
        payer: payer.publicKey,
        recipient: recipient.publicKey,
      })
      .rpc();

    // 打印转账后的信息: payer余额、recipient
});

总结

第一种方法:创建受程序管理的账户灵活性高,程序可以完全控制 programOwnedAccount,自由设计逻辑。这个在合约自动化转账领域非常有用。缺点是中转账户需要额外的存储费用。

第二种方法:使用 CPI简洁、高效,不需要额外创建中转账户,直接操作钱包账户,节省存储费用。缺点是程序必须经过审慎设计,避免误操作或安全漏洞。

两种方法对比

特性 创建受程序管理的账户 使用 CPI
灵活性 程序完全控制账户 程序受系统程序的限制
存储成本 需要支付新账户的存储费用 无额外存储费用
开发复杂度 需要管理多个账户 较为简单,直接调用系统程序
安全性 程序外部无法直接操作中转账户 钱包账户操作依赖程序逻辑,存在潜在风险

现在回答本文开头的问题:“程序可以直接操作我的钱包转出 SOL 吗?” 答案是:通过 CPI 调用系统程序,可以间接实现操作钱包的功能。下一篇文章我将详细介绍 IDL。如果你想提前看到我的更新,可以扫描下方二维码关注我的公众号:认知那些事

认知那些事.png

  • 原创
  • 学分: 11
  • 分类: Solana
  • 标签:
点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
认知那些事
认知那些事
0x2b62...95a0
人立于天地之间,必然有我们的出路。