Solana 开发系列 2 - 第一个 Solana 程序

  • Tiny熊
  • 更新于 14小时前
  • 阅读 644

通过简单的程序介绍: Anchor 、 Solana 在线 IDE solpg 、演示合约编译、部署、调用流程,以及 解读 Solana 程序代码。

Solana系列文章第二篇,在上一篇: 理解 Solana , 我们理解了 Solana 共识,账户、PDA、交易及费用、集群等。

今天这篇文章我们来编写一个最简单的程序,我们将介绍:

  1. Anchor 开发框架
  2. Solana 在线 IDE : https://beta.solpg.io/
  3. favorites 程序,演示合约编译、部署、调用流程
  4. 解读Solana 程序代码。

让我们开始吧。

Anchor 开发框架

Anchor 是 Solana 程序开发框架(类似 EVM 合约开发的 Foundry ), 因为原生 Solana 的开发需要理解很多底层原理(账户模型、分配空间、序列化等等)

对于刚接触 Solana 开发的开发者来说,使用 Anchor 是很好的选择,Anchor 提供了更友好的开发体验:

  1. 使用宏和属性(attribute)来简化代码:Anchor 帮你自动生成例如复杂的序列化、去序列化或安全验证逻辑等代码。
  2. 内置测试框架:内置脚手架和测试工具,让我们能够快速搭建本地测试环境并执行单元测试。
  3. 统一的合约结构:Anchor 约定了代码目录结构、合约入口函数、账户声明方式等,具有更好的可读性和可维护性。
  4. Anchor 命令行工具提供了从初始化项目到编译、部署、测试的所有功能,非常适合快速迭代。
    1. anchor init <project-name>:初始化 Anchor 项目。
    2. anchor build:编译合约代码,生成 IDL(合约接口描述文件)。
    3. anchor test:运行测试脚本。
    4. anchor deploy:将合约部署到指定的网络(本地网络、测试网或主网)。

Solana 在线 IDE

Solana Playground(类似于 EVM 的Remix)为想要快速体验 Anchor 的开发者提供了一个在线环境。相比在本地手动搭建开发环境,Playground 可以让我们免去安装 Rust、Solana CLI、Anchor CLI 等前期工作,在浏览器中便可进行编写、测试与模拟。

当我们想要快速验证某段代码逻辑时,或者学习简单程序代码,Playground 非常便捷。不过,如果你打算进行更大规模的开发或生产部署,仍然建议在本地搭建完备的开发环境。

编写第一个Solana 程序

下面我们在 Solana Playground 中开始编写第一个Solana 程序,我们将使用一个名为 favorites 的示例, 这个代码来源于 18 小时 Solana 教程 - 2024 训练营 的第一个项目。

打开 https://beta.solpg.io/ , 点击创建项目,输入项目名 favorites, 选择 Anchor, 点击 Create

solpg 会默认帮我们生成一些代码:

image-20250108212822139

src/lib.rs 是 Solana 合约代码,client.ts 是客户端代码,用用户端与合约交互的代码。 anchor.test.ts 是测试代码。今天我们仅关注合约部分。

我们删除自动生成的代码,贴上favorites的代码,代码在这里

稍后我们来一行行解读代码,我们先体验一下代码的编译、部署以及合约调用的完整流程。

编译合约

在 solpg 中只要点击,如下的“Build” ,就可以完成编译。

编译完成,在控制台有类似的输出:

Building...
Build successful. Completed in 5.81s.

还有会生成用户部署的二进制文件以及 IDL , 这里我们暂时不管。如果有兴趣查看它,可以在上图左侧切换到第二个扳手图标,导出查看相应的文件。

部署合约

部署合约需要链接到一个网络,以及一个有余额的账号。

如果是第一次使用 solpg ,点击 Deploy 的时候,会出现如下创建 Playground Wallet 的提示:

点击“Continue”即可,刚开始创建的账号,我们需要从水龙头获取一个测试 Sol,默认情况下,solpg 应该链接的是 devnet 集群(如果没有链接devnet,可以通过下图剪头指向的设置修改)。我们可以在底部看到链接的集群信息:

我们可以从 https://faucet.solana.com/ 获取一些 devnet 测试Sol, 或者通过 Solpg 右上角 wallet 直接点 Airdrop:

准备工作做好之后,就可以进行部署:

点击 “Deploy” 后, 可以看到右侧 declare_id!() 中的程序 id 会自动修改,修改为链上的程序账户公钥。

我们可以在浏览器查看该程序账户信息

调用函数

favorites 合约程序里 ,定义了一个函数 set_favorites ,称之为指令处理函数。上一篇我们知道了交易 会包含一个或多个指令。

一条指令(Instruction)本质上是“要执行的代码 + 所需账户 + 参数数据”。在 anchor build 时会分析你的 #[program] 下的方法签名和参数,然后自动生成一个对应的 .json IDL文件,会列出方法名、参数类型、需要的账户等。solpg IDE 会帮我们把这些信息解析出来, 来看看来 solpg IDE如何交互。

先就切换到 test 页面,可以看到已经解析出程序的指令与账户:

提示:在 Anchor 项目中,Rust 端(合约端)使用的是 snake_case (蛇形命名法)命名, 而客户前端 JavaScript / TypeScript 通常使用 camelCase (驼峰命名法)命令, Anchor 会为了与前端习惯一致,会做一个自动的命令转换。

我们展开 setFavorites 指令,可以看到调用该指令需要的参数和需要提供的账户 :

set_favorites 做的事情是:保存自己喜欢的号码(number)、颜色(color)及习惯(hobbies),这些信息保存在自己地址关联的 PDA 账户里。

因此号码(number)、颜色(color)及习惯(hobbies)是传递给函数的参数,而关联的账户有:

user 用来支付该交易的费用, favorites 是保存这些信息的 PDA 账户。systemProgram 系统程序账户,例如在创建新账户分配租金转账 SOL,都需要系统程序账户。

参数填写比较简单,user 账户在下拉框选择current wallet 即可。favorites 选择通过 From seed 创建:

PDA 账户创建的种子是在 程序中定义的:

seeds=[b"favorites", user.key().as_ref()],

种子的第一部分是 favorites , 第二部分是用户的公钥, 这样每个用户都有一个对应的 PDA 账户了。

第一个种子写字符串"favorites",第二个种子(需要先点 Add Seed ), 写当前的用户公钥,点击Generate 创建出 PDA 账户。然后就可以点击"Test" 调用了。

我的调用记录可以在这里 可以查看到,随后我们就可以通过 Fetch All 查询到所有的数据:

现在我们已经知道了如何惊醒合约程序的编译、部署和调用,我们继续看一下使用 Anchor 框架如何编写 Solana 合约。

程序代码解析

Solana 合约是使用 Rust 语言编写的, 学习起来门槛有一点高,不过你需要完全学会了 Rust 才动手写 Solana 合约,

我们可以把Anchor Rust 当成一门独立的语言学习,把一些用法当成是一个固定搭配,先不要深究背后的实现及原理。

当然,我自己是一个 Rust 新手,仅供参考。

favorites 代码

use anchor_lang::prelude::*;
// 声明程序账户地址,在部署后,被 anchor 自动更新
declare_id!("ww9C83noARSQVBnqmCUmaVdbJjmiwcV9j2LkXYMoUCV");

pub const ANCHOR_DISCRIMINATOR_SIZE: usize = 8;

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

    // 函数, 处理指令
    pub fn set_favorites(context: Context<SetFavorites>, number: u64, color: String, hobbies: Vec<String>) -> Result<()> {
        let user_public_key = context.accounts.user.key();
        msg!("Greetings from {}", context.program_id);
        msg!("User {user_public_key}'s favorite number is {number}, favorite color is: {color}");
        msg!("User's hobbies are: {:?}",hobbies); 

        context.accounts.favorites.set_inner(Favorites {
            number,
            color,
            hobbies
        });
        Ok(())
    }
}

// 定义放在 Favorites PDA 里的数据
#[account]
#[derive(InitSpace)]
pub struct Favorites {
    pub number: u64,

    #[max_len(50)]
    pub color: String,

    #[max_len(5, 50)]
    pub hobbies: Vec<String>
}
// 定义调用set_favorites指令时,需要提供要修改的帐户。 
#[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>,
}

逐行解析

备注,我使用 AI 的加持,帮我解读代码

use anchor_lang::prelude::*;

在 Rust 中,use xxx::* 会把对应模块内可见的所有内容都导入到当前命名空间, 这里导入 Anchor 框架的 prelude,它包含了使用 Anchor 时大部分常用的结构和宏,例如 ContextAccountSigner 等,可以查看 Anchor 文档

declare_id!("ww9C83noARSQVBnqmCUmaVdbJjmiwcV9j2LkXYMoUCV");

declare_id! 宏声明此程序(Program)的唯一地址(公钥),在部署后,被 anchor 自动更新这个地址。

pub const ANCHOR_DISCRIMINATOR_SIZE: usize = 8;

在 Anchor 中,每个账户(Account)都有一个8 字节的“discriminator”(鉴别符),用来标识该账户的数据结构类型。 定义常量,用来在后续计算账号所需空间(space)时加上这 8 字节大小。

#[program]
pub mod favorites {
    use super::*;

这里开始定义一个 Anchor 的程序模块 favorites#[program] 是 Anchor 提供的宏,用来表明这是一个 Solana 上的可执行程序。 use super::*; 则是把父模块(本文件最上层)的内容导入到该模块下,从而可以使用 FavoritesSetFavorites 等定义。

    pub fn set_favorites(context: Context<SetFavorites>, number: u64, color: String, hobbies: Vec<String>) -> Result<()> {
        let user_public_key = context.accounts.user.key();
        msg!("Greetings from {}", context.program_id);
        msg!("User {user_public_key}'s favorite number is {number}, favorite color is: {color}");

        msg!("User's hobbies are: {:?}", hobbies ); 

        context.accounts.favorites.set_inner(Favorites {
            number,
            color,
            hobbies
        });
        Ok(())
    }
  1. 函数签名:

    • set_favorites 是一个上链的指令(instruction)函数。
    • context: Context<SetFavorites> 表示这个指令需要携带哪些账户信息,在后面会从 SetFavorites 里看到具体定义。
    • 还接受若干参数:number, color, hobbies
    • 返回类型 Result<()> 表示要么成功(Ok(())),要么返回一个错误(Err),这是在 Anchor / Rust 编程中常见的模式。
    1. 函数体:
      • let user_public_key = context.accounts.user.key(); 从传入的 Context<SetFavorites>accounts.user 中,.key()获取当前调用这条指令的用户的公钥(Pubkey)。
      • msg!("Greetings from {}", context.program_id); 调用 Anchor 提供的 msg! 宏,用于在程序日志中打印一段信息(类似 EVM 中 emit 一个事件)。
      • context.accounts.favorites.set_inner(Favorites { number, color, hobbies });Favorites 账户中的数据更新为用户传进来的新值。这会将该账户持久化到链上,字段包括:number, color, hobbies
      • Ok(()) 指令成功执行完毕,返回一个空的成功结果。
#[account]
#[derive(InitSpace)]
pub struct Favorites {
    pub number: u64,

    #[max_len(50)]
    pub color: String,

    #[max_len(5, 50)]
    pub hobbies: Vec<String>
}
  1. #[account]:表示此结构体是一个可存储到 Solana 上账户数据(Account Data)中的数据类型,Anchor 会为其自动管理序列化/反序列化。

  2. #[derive(InitSpace)]:这是 Anchor 的一个属性宏,用来自动计算并提供在初始化时所需的账户空间大小(稍后会看到 Favorites::INIT_SPACE 用到)。

  3. pub struct Favorites { ... } :该结构体用于存储相关信息:

    • pub number: u64:用户喜欢的数字。
    • #[max_len(50)] pub color: String:用户喜欢的颜色,限制最大长度 50。
    • #[max_len(5, 50)] pub hobbies: Vec<String>:用户的兴趣爱好列表,这里限制了向量中最多 5 项,且每个字符串长度最多 50。
#[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>,
}
  1. #[derive(Accounts)]:将该结构体标注为 Anchor 的账户验证(Accounts)结构体。执行指令时,Runtime 会根据这里的定义校验和初始化所需账户。

  2. pub struct SetFavorites<'info> { ... } :存放 set_favorites 指令里需要的账户信息(包含 3 个账号):

    • #[account(mut)] pub user: Signer<'info> : 表示 user 是一个可写账户(mut),而且是 Signer,即调用该指令时需要使用这个账户签名。
     #[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>,
  • init_if_needed:如果这个账户尚未初始化,就会自动帮我们初始化。
  • payer = user:初始化账户时,需要使用 user 来支付租金(Solana 上存储数据的开销)。
  • space = ANCHOR_DISCRIMINATOR_SIZE + Favorites::INIT_SPACE:给新账户分配的空间大小,包含 8 字节的 discriminator 和 Favorites 结构体本身的空间。
  • seeds=[b"favorites", user.key().as_ref()]:确定存储数据的 PDA 的种子,根据 b"favorites" + user 公钥来推导出来的PDA地址, 同时 anchor 会约束只有 user 能操作这个 PDA。
  • bump:用来处理 PDA 生成时的碰撞因子,由 Anchor 在运行时自动生成并校验。
  • pub favorites: Account<'info, Favorites> 这个 favorites 就是我们要读写的账户数据,里面存储用户喜爱信息。
    • pub system_program: Program<'info, System>, : 表示系统程序账户。因为我们要初始化一个新账户,需要使用系统程序来完成创建、分配空间、支付租金等操作。

总结

我们借助简单的 favorites 项目,演示了如何 Solpg 来进行 solana 程序的编译、部署和交互。 也详细解读了 favorites 合约程序如何编写的,几个关键点:

  1. 导入 use anchor_lang::prelude::*; 以便使用anchor定义的宏和模块。

  2. 通过 declare_id! 指定了该程序在 Solana 上的公钥地址。

  3. 使用 #[program] 宏将rust 模块转换为 Solana 程序,模块类的函数,都是指令处理函数

  4. 通过 #[account] 定义PDA 数据存储

  5. 通过 #[derive(Accounts)] 描述调用该指令需要提供的账户。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Tiny熊
Tiny熊
0xD682...E8AB
登链社区发起人 通过区块链技术让世界变得更好而尽一份力。