在上一节这次一定好好学Solana(3):交易和费用中,我们学习了Solana交易的结构,本质上是通过拼装参数来调用智能合约。今天我们将深入探讨如何基于此开发一个简单的Solana合约。我们会从交易的JSON表示开始,逐步过渡到合约的伪代码模板,最后通过Solana官方P
在上一节 这次一定好好学 Solana (3) : 交易和费用中,我们学习了 Solana 交易的结构,本质上是通过拼装参数来调用智能合约。今天我们将深入探讨如何基于此开发一个简单的 Solana 合约。我们会从交易的 JSON 表示开始,逐步过渡到合约的伪代码模板,最后通过 Solana 官方 Playground 实现一个功能:让每个 PDA 账户存储用户喜欢的数字和颜色。
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 合约模板如下:
引入依赖
定义常量
#[program]
pub mod 模块名 {
定义指令方法1
定义账户1 struct, 哪些签名, 哪些付 gas, 哪些是 PDA
定义 PDA 账户的存储结构1 struct
}
注意:如果合约有多个指令,且涉及不同账户,则需定义多组指令、账户和 PDA 存储结构。
我们将开发一个简单的合约:每个 PDA 账户存储用户的喜欢的数字(number
)和喜欢的颜色(color
)。
我们使用 Solana 官方的在线 IDE:Solana Playground。
步骤:
以下是基于 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>, // 系统程序
}
为什么 context.accounts.favorites
可以链式调用?
Context
是一个泛型结构体,其 accounts
字段的类型是 SetFavorites
。因此可以通过 .favorites
访问定义的 PDA 账户。伪代码如下:
pub struct Context<T> {
accounts: T // T 是 SetFavorites
}
为什么引入 system_program
但没直接使用?
在 #[account(init_if_needed)]
中,Anchor 底层会调用 system_program
检查账户是否存在(不存在则创建),并处理租金支付和空间分配。
付费账户(payer
)的值可以随便写吗?
不是。payer = user
中的 user
是上面定义的 Signer<'info>
变量名,表示签名者支付费用。
为什么 PDA 账户要指定空间大小?
Solana 不支持动态大小的账户数据。创建账户时,system_program
需要 space
参数并收取租金。空间包括 2 部分:
#[derive(InitSpace)]
宏自动计算,生成 INIT_SPACE
属性。PDA 账户如何创建?
使用以下属性定义:
#[account(
init_if_needed,
payer = user,
space = ANCHOR_DISCRIMINATOR_SIZE + Favorites::INIT_SPACE,
seeds = [b"favorites", user.key().as_ref()],
bump
)]
b"favorites"
是固定种子,user.key().as_ref()
是用户公钥,确保每个用户有唯一 PDA。
注意:
PDA 账户要加2个种子, 一个是 favorites 字符串, 一个是账户 public key
部署并调用后,可在 Solana Explorer 查看交易示例:
Devnet 交易
交易说明:
通过这个简单的合约,我们学习了:
希望这篇笔记能帮助你快速上手 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>,
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!