如何创建一个系统程序PDA

  • QuickNode
  • 发布于 2025-01-30 11:59
  • 阅读 16

本文详细介绍了如何在Solana平台上创建一个由系统程序(System Program)拥有的程序派生地址(PDA),并展示了如何向PDA转账以及从PDA取款的实现过程。文章提供了对相关概念的深入解读,并通过示例代码和测试用例,指导读者掌握该操作的步骤和注意事项。

概述

程序派生账户(Program-derived Accounts)是 Solana 中的一个独特工具,它允许程序拥有并对这些账户上的状态更改进行签名。这种所有权模型对各种应用程序至关重要,从去中心化金融(DeFi)到非同质化代币(NFT),因为它允许更复杂和安全的资产与数据处理。通常,PDAs 由该程序创建和拥有(例如,Token程序创建一个Token账户)。然而,有一些情况使得由系统程序拥有 PDA 变得有用。特别是,如果你正在为新账户支付租金豁免或与其他程序交互,期望系统程序账户参与交易(例如,向系统账户转移 SOL 的 CPI)。请查看该 Marinande Claim 函数 中的示例:

    #[account(\
        mut,\
        address = ticket_account.beneficiary @ MarinadeError::WrongBeneficiary\
    )]
    pub transfer_sol_to: SystemAccount<'info>,

在上述账户上下文中,Anchor 约束 SystemAccount 将限制交互仅限于由系统程序拥有的账户。本指南将向你展示如何在你的 Anchor 项目中创建一个由系统程序拥有的 PDA。

你将要做的事情

编写一个 Anchor 程序和测试:

  • 创建一个由系统程序拥有的 PDA
  • “初始化” PDA
  • 从 PDA 中提取 SOL

你将需要的东西

本指南中使用的依赖项

依赖项 版本
anchor-lang 0.29.0
anchor-spl 0.29.0
solana-program 1.16.24
spl-token 4.0.0
solana-cli 1.17.14

启动你的项目

通过访问 https://beta.solpg.io/ 创建一个新的 Solana Playground 项目。Solana Playground 是一个基于浏览器的 Solana 代码编辑器,使我们能够快速启动并完成此项目。你可以在自己的代码编辑器中跟随,但本指南将针对 Solana Playground 的必要步骤进行调整。首先,点击“创建新项目”:

创建新项目

输入项目名称“system-pda”,并选择“Anchor (Rust)”:

命名项目

创建并连接钱包

由于这个项目仅用于演示目的,我们可以使用一个“临时”钱包。Solana Playground 使它变得简单。你应该在浏览器窗口左下角看到一个红点“未连接”。点击它:

钱包设置按钮

Solana Playground 将为你生成一个钱包(或者你可以导入自己的钱包)。请随意保存以备后用,并在准备好后点击继续。一个新的钱包将被初始化并连接到 Solana devnet。

你准备好了!让我们开始构建吧!

创建程序

现在,让我们打开 lib.rs 并删除起始代码。一旦你拥有一个空白的页面,我们可以开始构建我们的程序。首先,让我们导入一些依赖并框定我们的程序。将以下内容添加到文件顶部:

use anchor_lang::prelude::*;
use anchor_lang::system_program::{transfer, Transfer};

declare_id!("11111111111111111111111111111111");

#[program]
mod sys_pda_example {
    use super::*;
    pub fn transfer_to_pda(ctx: Context<Example>, fund_lamports: u64) -> Result<()> {
        // 在这里添加代码
        Ok(())
    }
    pub fn transfer_from_pda(ctx: Context<Example>, return_lamports: u64) -> Result<()> {
        // 在这里添加代码
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Example<'info> {
    #[account(\
        mut,\
        seeds = [b"vault".as_ref()],\
        bump\
    )]
    pub pda: SystemAccount<'info>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}

我们在这里做的是框定我们的程序。我们从 Anchor 的预导入和系统程序中导入。我们还有一个 declare_id! 宏,它将定义我们程序构建后的程序 ID。我们在 sys_pda_example 程序中定义了两个指令:transfer_to_pdatransfer_from_pda。它们将用于“初始化” PDA 和从 PDA 转移资金。两个指令都将使用相同的 Example 账户上下文。Example 上下文被定义为包含三种账户:pda(一个 SystemAccount)、signersystem_programpda 账户是我们将要创建的 PDA,signer 账户是将签署交易并支付交易费用的账户,而 system_program 账户是将拥有 PDA 并允许我们转移 SOL 的账户。

“初始化”系统 PDA

你可能已经注意到我们在整个指南中提到系统 PDA 的初始化是用引号标记的。我们还将第一个函数命名为 transfer_to_pda 而非 initialize_pda。这是因为我们实际上并没有初始化 PDA,而只是将资金转移到它。所有账户默认为系统账户,且通过将 SOL 转移到我们从种子派生的账户,我们实际上是在“初始化”它。我们只需要确保转移足够的资金来覆盖 0 字节账户的租金。

将你的 transfer_to_pda 函数更新为通过添加以下代码将资金转移到我们的 PDA:

    pub fn transfer_to_pda(ctx: Context<Example>, fund_lamports: u64) -> Result<()> {
        let pda = &mut ctx.accounts.pda;
        let signer = &mut ctx.accounts.signer;
        let system_program = &ctx.accounts.system_program;

        let pda_balance_before = pda.get_lamports();

        transfer(
            CpiContext::new(
                system_program.to_account_info(),
                Transfer {
                    from: signer.to_account_info(),
                    to: pda.to_account_info(),
                },
            ),
            fund_lamports,
        )?;

        let pda_balance_after = pda.get_lamports();

        require_eq!(pda_balance_after, pda_balance_before + fund_lamports);

        Ok(())
    }

这个函数实际上做了两件事情:

  1. 向系统程序发起跨程序调用(CPI),从 signer 账户到 pda 账户转移资金。
  2. 通过比较我们转账后的余额和预期余额来检查资金是否成功转移。

注意:正如我们之前讨论的,这并不是真正的初始化函数。这个函数可以在任何时候被调用并向 PDA 转移 SOL。

从系统 PDA 转移资金

既然我们有了在 SystemAccount PDA 中的 SOL,我们需要知道如何转移这些资金。由于它是一个 PDA,我们将不得不使用带有签名种子的 CPI。现在让我们实现这个功能。

更新你的 transfer_from_pda 函数,通过添加以下代码从我们的 PDA 转移资金:

    pub fn transfer_from_pda(ctx: Context<Example>, return_lamports: u64) -> Result<()> {
        let pda = &mut ctx.accounts.pda;
        let signer = &mut ctx.accounts.signer;
        let system_program = &ctx.accounts.system_program;

        let pda_balance_before = pda.get_lamports();

        let bump = &[ctx.bumps.pda];
        let seeds: &[&[u8]] = &[b"vault".as_ref(), bump];
        let signer_seeds = &[&seeds[..]];

        transfer(
            CpiContext::new(
                system_program.to_account_info(),
                Transfer {
                    from: pda.to_account_info(),
                    to: signer.to_account_info(),
                },
            ).with_signer(signer_seeds),
            return_lamports,
        )?;

        let pda_balance_after = pda.get_lamports();

        require_eq!(pda_balance_after, pda_balance_before - return_lamports);

        Ok(())
    }

这个指令与我们之前的几乎完全相同,但有一个重要的不同之处。我们现在必须向 CpiContext 提供一个 signer_seeds 数组来签署交易。这个数组是一个用于派生 PDA 地址的种子列表。在本例中,我们使用与之前相同的种子“vault”以及在创建 PDA 时生成的 bump。

构建

在编写测试之前,请先构建你的程序以检查错误。你可以通过点击 Solana Playground 窗口左上角的“构建”按钮或者在 playground 终端输入 build 来完成此操作。如果你有任何错误,你将在下面的控制台中看到它们。控制台应该提供一些明确的指导,以更正任何问题或错误。修复后,让我们编写我们的测试。

测试

随时可以尝试编写自己的测试。由于这不是本指南的重点,我们已经为你编写了测试。用以下代码替换 anchor.test.ts 文件中的所有现有代码:

describe("测试", () => {
  const [pda] = web3.PublicKey.findProgramAddressSync(
    [Buffer.from("vault")],
    pg.PROGRAM_ID
  );

  it("向 PDA 转移资金", async () => {
    const lamports = web3.LAMPORTS_PER_SOL;
    const data = new BN(lamports);
    const initialBalance = await pg.connection.getBalance(pda);

    try {
      const tx = await pg.program.methods
        .transferToPda(data)
        .accounts({
          pda,
          signer: pg.wallet.publicKey,
        })
        .transaction();
      const txHash = await web3.sendAndConfirmTransaction(pg.connection, tx, [pg.wallet.keypair]);

      const balance = await pg.connection.getBalance(pda);
      assert.strictEqual(
        balance,
        initialBalance + lamports,
        "余额不正确"
      );
    } catch (error) {
      assert.fail(`交易错误:${error}`);
    }
  });
  it("从 PDA 转移资金", async () => {
    const lamports = 0.5 * web3.LAMPORTS_PER_SOL;
    const data = new BN(lamports);
    const initialBalance = await pg.connection.getBalance(pda);
    try {
      const tx = await pg.program.methods
        .transferFromPda(data)
        .accounts({
          pda,
          signer: pg.wallet.publicKey,
        })
        .transaction();
      const txHash = await web3.sendAndConfirmTransaction(pg.connection, tx, [pg.wallet.keypair]);

      const balance = await pg.connection.getBalance(pda);

      assert.strictEqual(
        balance,
        initialBalance - lamports,
        "余额不正确"
      );
    } catch (error) {
      assert.fail(`交易错误:${error}`);
    }
  });
});

这个文件做了三件事:

  1. 定义 PDA 地址。请注意,我们正在使用我们程序的 ID。即使系统程序将拥有该账户,我们的程序 ID 也需要派生账户并最终签署交易。
  2. 测试我们是否可以向 PDA 转移资金。
  3. 测试我们是否可以从 PDA 转移资金。

我们的测试依赖于对 pda 账户的前后余额检查,以确保资金按预期被贷记或扣除。

运行测试

打开一个新的终端窗口,然后运行以下命令以启动本地验证器:

solana-test-validator

验证器运行后,你必须确保 Solana Playground 连接到本地主机。你可以通过点击浏览器窗口左下角的“⚙️”并在端点下拉菜单中选择“Localhost”来做到这一点。你的浏览器现在应已连接到本地验证器。

通过在 playground 终端中输入以下命令为你的钱包分发一些本地 SOL:

solana airdrop 100

确认后,请在你的 playground 终端中输入 deploy 或点击工具“🛠️”菜单中的“部署”按钮。一分钟左右,你的程序应已部署到集群中。

最后,通过在 playground 终端中输入 test 或点击主浏览器屏幕中的“测试”按钮来运行你的测试。你应该看到两个成功的测试:

正在运行测试...
  anchor.test.ts:
  测试
    ✔ 向 PDA 转移资金 (552ms)
    ✔ 从 PDA 转移资金 (546ms)
  2 个通过 (1s)

随时浏览 Solana Explorer 中的交易(确保选择自定义/本地集群)以查看已执行的交易并浏览新创建的 PDA。

恭喜你!你已成功创建一个由系统程序拥有的 PDA,并向其转移了资金,也从中提取了资金。

总结

你现在拥有一个能够管理由系统程序拥有的 PDA 的工作程序。这可以帮助你在通过 CPI 与其他程序交互或从你的程序为新账户支付租金豁免时使用。

如果你遇到问题、想问问题或者只是想讨论你正在构建的内容,请通过 DiscordTwitter 联系我们!

我们 ❤️ 反馈!

告诉我们 如果你有任何反馈或希望讨论的新主题。我们很乐意听取你的想法。

  • 原文链接: quicknode.com/guides/sol...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。