通过简单的程序介绍: Anchor 、 Solana 在线 IDE solpg 、演示合约编译、部署、调用流程,以及 解读 Solana 程序代码。
Solana系列文章第二篇,在上一篇: 理解 Solana , 我们理解了 Solana 共识,账户、PDA、交易及费用、集群等。
今天这篇文章我们来编写一个最简单的程序,我们将介绍:
favorites
程序,演示合约编译、部署、调用流程让我们开始吧。
Anchor 是 Solana 程序开发框架(类似 EVM 合约开发的 Foundry ), 因为原生 Solana 的开发需要理解很多底层原理(账户模型、分配空间、序列化等等)
对于刚接触 Solana 开发的开发者来说,使用 Anchor 是很好的选择,Anchor 提供了更友好的开发体验:
anchor init <project-name>
:初始化 Anchor 项目。anchor build
:编译合约代码,生成 IDL(合约接口描述文件)。anchor test
:运行测试脚本。anchor deploy
:将合约部署到指定的网络(本地网络、测试网或主网)。Solana Playground(类似于 EVM 的Remix)为想要快速体验 Anchor 的开发者提供了一个在线环境。相比在本地手动搭建开发环境,Playground 可以让我们免去安装 Rust、Solana CLI、Anchor CLI 等前期工作,在浏览器中便可进行编写、测试与模拟。
当我们想要快速验证某段代码逻辑时,或者学习简单程序代码,Playground 非常便捷。不过,如果你打算进行更大规模的开发或生产部署,仍然建议在本地搭建完备的开发环境。
下面我们在 Solana Playground 中开始编写第一个Solana 程序,我们将使用一个名为 favorites
的示例, 这个代码来源于 18 小时 Solana 教程 - 2024 训练营 的第一个项目。
打开 https://beta.solpg.io/ , 点击创建项目,输入项目名 favorites
, 选择 Anchor, 点击 Create
solpg 会默认帮我们生成一些代码:
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 新手,仅供参考。
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 时大部分常用的结构和宏,例如 Context
,Account
,Signer
等,可以查看 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::*;
则是把父模块(本文件最上层)的内容导入到该模块下,从而可以使用 Favorites
、SetFavorites
等定义。
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(())
}
函数签名:
set_favorites
是一个上链的指令(instruction)函数。context: Context<SetFavorites>
表示这个指令需要携带哪些账户信息,在后面会从 SetFavorites
里看到具体定义。number
, color
, hobbies
Result<()>
表示要么成功(Ok(())
),要么返回一个错误(Err
),这是在 Anchor / Rust 编程中常见的模式。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>
}
#[account]
:表示此结构体是一个可存储到 Solana 上账户数据(Account Data)中的数据类型,Anchor 会为其自动管理序列化/反序列化。
#[derive(InitSpace)]
:这是 Anchor 的一个属性宏,用来自动计算并提供在初始化时所需的账户空间大小(稍后会看到 Favorites::INIT_SPACE
用到)。
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>,
}
#[derive(Accounts)]
:将该结构体标注为 Anchor 的账户验证(Accounts)结构体。执行指令时,Runtime 会根据这里的定义校验和初始化所需账户。
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
合约程序如何编写的,几个关键点:
导入 use anchor_lang::prelude::*;
以便使用anchor
定义的宏和模块。
通过 declare_id!
指定了该程序在 Solana 上的公钥地址。
使用 #[program]
宏将rust 模块转换为 Solana 程序,模块类的函数,都是指令处理函数
通过 #[account]
定义PDA 数据存储
通过 #[derive(Accounts)]
描述调用该指令需要提供的账户。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!