这次一定好好学 Solana (5) : 开发一个有PDA的简单合约

  • dgu
  • 发布于 3天前
  • 阅读 272

在上一节这次一定好好学Solana(3):交易和费用中,我们学习了Solana交易的结构,本质上是通过拼装参数来调用智能合约。今天我们将深入探讨如何基于此开发一个简单的Solana合约。我们会从交易的JSON表示开始,逐步过渡到合约的伪代码模板,最后通过Solana官方P

在上一节 这次一定好好学 Solana (3) : 交易和费用中,我们学习了 Solana 交易的结构,本质上是通过拼装参数来调用智能合约。今天我们将深入探讨如何基于此开发一个简单的 Solana 合约。我们会从交易的 JSON 表示开始,逐步过渡到合约的伪代码模板,最后通过 Solana 官方 Playground 实现一个功能:让每个 PDA 账户存储用户喜欢的数字和颜色。

回顾Solana 交易的 JSON 表示

Solana 交易的核心是通过参数调用程序(Program)。一个典型的交易可以用 JSON 表示如下:

{
  "transaction": {
    "message": {
      "accountKeys": ["3z9...", "3z1...", "111..."], // 定义涉及的账户,包括程序和付 gas 的账户
      "header": {
        "numReadonlySignedAccounts": 0, // 只读签名账户数
        "numReadonlyUnsignedAccounts": 1, // 只读未签名账户数
        "numRequiredSignatures": 1 // 所需签名数
      },
      "recentBlockhash": "Dzf...", // 防止重复提交
      "instructions": [
        {
          "accounts": [0, 1], // 指令涉及的账户索引
          "programIdIndex": 2, // 程序 ID 的索引
          "data": "3Bx..." // 指令数据
        }
      ]
    },
    "signatures": [
      "5LrcE2f6uvydKRquEJ8xp19heGxSvqsVbcqUeFoiWbXe8JNip7ftPQNTAVPyTK7ijVdpkzmKKaAQR7MWMmujAhXD"
    ]
  }
}

如果你有 Web2 开发经验,可以通过这些参数推断接口定义。Solana 的合约开发与之类似,但有其独特之处。接下来,我们看看合约的伪代码模板。

Solana 合约伪代码模板

一个基本的 Solana 合约模板如下:

引入依赖

定义常量

#[program]
pub mod 模块名 {
    定义指令方法1

    定义账户1 struct, 哪些签名, 哪些付 gas, 哪些是 PDA

    定义 PDA 账户的存储结构1 struct
}

注意:如果合约有多个指令,且涉及不同账户,则需定义多组指令、账户和 PDA 存储结构。

开发一个简单合约

需求

我们将开发一个简单的合约:每个 PDA 账户存储用户的喜欢的数字number)和喜欢的颜色color)。

工具

我们使用 Solana 官方的在线 IDE:Solana Playground
步骤:

  1. 进入 Playground,创建钱包。
  2. 领取测试网代币,用于部署合约和调用指令。

solpg.jpg

核心代码(带注释)

以下是基于 Anchor 框架的实现代码:

// 引入依赖
use anchor_lang::prelude::*;

// 定义 programId,部署后自动更新
declare_id!("4Pm9xVzVsQJMmodRdANm28UapsES4Ffy13AKeSPuEtqy");

// 定义常量,账户类型鉴别器占 8 字节
pub const ANCHOR_DISCRIMINATOR_SIZE: usize = 8;

#[program] // 定义程序
pub mod favorites {
    use super::*;

    // 定义指令:设置喜欢的数字和颜色
    pub fn set_favorites(context: Context<SetFavorites>, number: u64, color: String) -> Result<()> {
        msg!("Greetings from {}", context.program_id);
        let user_public_key = context.accounts.user.key();
        msg!("User {user_public_key}'s favorite number is {number}, favorite color is: {color}");

        // 修改 PDA 账户数据
        context.accounts.favorites.set_inner(Favorites { number, color });
        Ok(())
    }
}

// 定义 PDA 账户存储的数据结构
#[account]
#[derive(InitSpace)] // 自动计算存储空间
pub struct Favorites {
    pub number: u64,
    #[max_len(50)] // 限制颜色字符串长度
    pub color: String,
}

// 定义账户集合
#[derive(Accounts)]
pub struct SetFavorites<'info> {
    #[account(mut)] // 可变账户
    pub user: Signer<'info>, // 签名者账户

    // 定义 PDA 账户
    #[account(
        init_if_needed, // 不存在则创建
        payer = user, // 付费者
        space = ANCHOR_DISCRIMINATOR_SIZE + Favorites::INIT_SPACE, // 空间大小
        seeds = [b"favorites", user.key().as_ref()], // PDA 种子
        bump // 碰撞参数
    )]
    pub favorites: Account<'info, Favorites>, // PDA 账户存储 Favorites 类型

    pub system_program: Program<'info, System>, // 系统程序
}

代码解析

  1. 为什么 context.accounts.favorites 可以链式调用?
    Context 是一个泛型结构体,其 accounts 字段的类型是 SetFavorites。因此可以通过 .favorites 访问定义的 PDA 账户。伪代码如下:

    pub struct Context<T> {
       accounts: T // T 是 SetFavorites
    }
  2. 为什么引入 system_program 但没直接使用?
    #[account(init_if_needed)] 中,Anchor 底层会调用 system_program 检查账户是否存在(不存在则创建),并处理租金支付和空间分配。

  3. 付费账户(payer)的值可以随便写吗?
    不是。payer = user 中的 user 是上面定义的 Signer<'info> 变量名,表示签名者支付费用。

  4. 为什么 PDA 账户要指定空间大小?
    Solana 不支持动态大小的账户数据。创建账户时,system_program 需要 space 参数并收取租金。空间包括 2 部分:

    • 鉴别器(Discriminator):8 字节,由 Anchor 自动添加,用于区分账户类型。
    • 实际数据:通过 #[derive(InitSpace)] 宏自动计算,生成 INIT_SPACE 属性。
  5. PDA 账户如何创建?
    使用以下属性定义:

    #[account(
       init_if_needed,
       payer = user,
       space = ANCHOR_DISCRIMINATOR_SIZE + Favorites::INIT_SPACE,
       seeds = [b"favorites", user.key().as_ref()],
       bump
    )]
    • 种子(Seeds)b"favorites" 是固定种子,user.key().as_ref() 是用户公钥,确保每个用户有唯一 PDA。
    • Bump:碰撞参数,由 Anchor 自动计算(从 255 递减),确保地址唯一且符合签名要求。

调用合约

test.jpg 注意: PDA 账户要加2个种子, 一个是 favorites 字符串, 一个是账户 public key

部署并调用后,可在 Solana Explorer 查看交易示例:
Devnet 交易

交易说明:

1743046137747.jpg

1743046202792.jpg

1743046263220.jpg

总结

通过这个简单的合约,我们学习了:

  • Solana 交易的结构和参数拼装。
  • 如何用 Anchor 框架定义指令、账户和 PDA。
  • PDA 账户的创建逻辑和空间计算。

希望这篇笔记能帮助你快速上手 Solana 合约开发!后续我会分享更复杂的合约案例,敬请期待。

无注释版本代码, 方便运行

use anchor_lang::prelude::*;
declare_id!("4Pm9xVzVsQJMmodRdANm28UapsES4Ffy13AKeSPuEtqy");
pub const ANCHOR_DISCRIMINATOR_SIZE: usize = 8;
#[program]
pub mod favorites {
    use super::*;
    pub fn set_favorites(context: Context<SetFavorites>, number: u64, color: String) -> Result<()> {
        msg!("Greetings from {}", context.program_id);
        let user_public_key = context.accounts.user.key();
        msg!("User {user_public_key}'s favorite number is {number}, favorite color is: {color}",);

        context
            .accounts
            .favorites
            .set_inner(Favorites { number, color });
        Ok(())
    }
}
// What we will put inside the Favorites PDA
#[account]
#[derive(InitSpace)]
pub struct Favorites {
    pub number: u64,
    #[max_len(50)]
    pub color: String,
}
#[derive(Accounts)]
pub struct SetFavorites<'info> {
    #[account(mut)]
    pub user: Signer<'info>,
    #[account(
        init_if_needed,
        payer = user,
        space = ANCHOR_DISCRIMINATOR_SIZE + Favorites::INIT_SPACE,
        seeds=[b"favorites", user.key().as_ref()],
        bump
    )]
    pub favorites: Account<'info, Favorites>,
    pub system_program: Program<'info, System>,
}
  • 原创
  • 学分: 9
  • 分类: Solana
  • 标签:
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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