文章背景:
上篇文章,我们使用 Anchor 工程化环境,从初始化项目、编译、测试、部署各个环节演示了一个真实的 solana 链上程序的开发流程。这篇文章,我们从语法和业务的角度来梳理下我们实现的合约的源码。
solana_business_card 合约源码:
rust
use anchor_lang::prelude::*;
// Our program's address!
// This matches the key in the target/deploy directory
declare_id!("BYBFmxjHn48LVAjKfo7dX6kPTw62HNPTktMqnpNeeiHu");
// Anchor programs always use 8 bits for the discriminator
pub const ANCHOR_DISCRIMINATOR_SIZE: usize = 8;
// Our Solana program!
#[program]
pub mod solana_business_card {
use super::*;
// Our instruction handler! It sets the user's favorite number and color
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}",);
// 验证颜色长度限制
require!(color.len() <= 50, CustomError::ColorTooLong);
// 验证爱好数量和每个爱好的长度限制
require!(hobbies.len() <= 5, CustomError::TooManyHobbies);
for hobby in &hobbies {
require!(hobby.len() <= 50, CustomError::HobbyTooLong);
}
msg!("User's hobbies are: {:?}", hobbies);
context.accounts.favorites.set_inner(Favorites {
number,
color,
hobbies,
});
Ok(())
}
// We can also add a get_favorites instruction handler to return the user's favorite number and color
}
// What we will put inside the 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>,
}
// When people call the set_favorites instruction, they will need to provide the accounts that will be modifed. This keeps Solana fast!
#[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"solana_business_card", user.key().as_ref()],
bump)]
pub favorites: Account<'info, Favorites>,
pub system_program: Program<'info, System>,
}
#[error_code]
pub enum CustomError {
#[msg("Color string is too long (max 50 characters)")]
ColorTooLong,
#[msg("Too many hobbies (max 5)")]
TooManyHobbies,
#[msg("Hobby string is too long (max 50 characters)")]
HobbyTooLong,
}
核心功能概述
程序的主要功能是通过 set_favorites
指令允许用户在区块链上存储和更新以下信息:
- 最喜欢的数字 (
number: u64
):一个无符号 64 位整数。 - 最喜欢的颜色 (
color: String
):一个字符串,长度限制为最多 50 个字符。 - 爱好列表 (
hobbies: Vec<String>
):一个字符串向量,最多包含 5 个爱好,每个爱好的长度限制为 50 个字符。
这些信息存储在一个 PDA(Program Derived Address) 中,PDA 的种子基于字符串 "solana_business_card"
和用户的公钥,确保每个用户有唯一的存储空间。
程序结构与关键组件
指令(Instruction)set_favorites
:
输入参数:
number: u64
:用户设置的最喜欢的数字。color: String
:用户设置的最喜欢的颜色。hobbies: Vec<String>
:用户设置的爱好列表。
功能:
- 验证输入:
-
- 颜色字符串长度不超过 50 个字符。
- 爱好列表不超过 5 个,且每个爱好字符串长度不超过 50 个字符。
- 将用户的
number
、color
和hobbies
存储到Favorites
账户中。 - 输出日志,记录用户的公钥、程序 ID、设置的数字、颜色和爱好。
返回:成功执行返回 Ok(())
,失败则抛出自定义错误。
账户结构(Accounts)
Favorites
账户:存储用户的最喜欢的数字、颜色和爱好。
使用 #[account]
和 #[derive(InitSpace)]
宏定义,确保账户空间计算准确。
字段:
number: u64
(8 字节)。color: String
(最大 50 字符,包含 4 字节长度前缀)。hobbies: Vec<String>
(最多 5 个字符串,每个字符串最大 50 字符,包含向量长度前缀)。
空间计算:ANCHOR_DISCRIMINATOR_SIZE
(8 字节)+ Favorites::INIT_SPACE
。
SetFavorites
账户上下文:
user: Signer<'info>
:调用指令的签名者(用户),需要支付账户初始化费用。
favorites: Account<'info, Favorites>
:
- 使用 PDA,种子为
b"solana_business_card"
和用户公钥。 - 如果账户不存在,自动初始化(
init_if_needed
)。 - 空间大小为
ANCHOR_DISCRIMINATOR_SIZE + Favorites::INIT_SPACE
。
system_program: Program<'info, System>
:用于账户创建和初始化的系统程序。
错误处理
自定义错误类型 CustomError
:
ColorTooLong
:颜色字符串超过 50 个字符。TooManyHobbies
:爱好数量超过 5 个。HobbyTooLong
:单个爱好字符串超过 50 个字符。
使用 require!
宏进行输入验证,失败时抛出相应错误。
工作流程
- 用户调用
set_favorites
指令,传入number
、color
和hobbies
。 - 程序验证:
-
- 颜色字符串长度 ≤ 50。
- 爱好数量 ≤ 5 且每个爱好长度 ≤ 50。
- 如果验证通过,程序将数据存储到用户的
Favorites
PDA 中。 - 输出日志,记录用户的公钥、设置的数字、颜色和爱好。
- 返回成功或抛出错误。
struct Favorites 和 struct SetFavorites
在上面 Solana 程序中,struct Favorites
和 struct SetFavorites
是两个不同用途的结构体,分别用于不同的场景。
主要区别
Favorites 结构体
定义:
rust
#[account]
#[derive(InitSpace)]
pub struct Favorites {
pub number: u64,
#[max_len(50)]
pub color: String,
#[max_len(5, 50)]
pub hobbies: Vec<String>,
}
用途:
Favorites
是一个账户数据结构,定义了存储在链上账户(PDA)中的数据格式。- 它表示程序实际存储在 Solana 区块链上的数据内容,用于持久化用户的喜好信息(
number
、color
和hobbies
)。 - 使用
#[account]
宏标记,告诉 Anchor 这是一个账户结构体,Anchor 会自动处理其序列化和反序列化。 #[derive(InitSpace)]
宏用于自动计算账户所需的存储空间。
存储位置:
- 存储在链上的账户中(
favorites
PDA),每次调用set_favorites
指令时会更新该账户的数据。
生命周期:
- 持久化存储,只要账户未被关闭,数据会一直保留在链上。
作用:
- 定义了数据的结构和约束(例如,
color
和hobbies
的最大长度)。 - 用于存储和读取链上数据。
SetFavorites 结构体
定义:
rust
#[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"solana_business_card", user.key().as_ref()],
bump
)]
pub favorites: Account<'info, Favorites>,
pub system_program: Program<'info, System>,
}
用途:
SetFavorites
是一个账户上下文结构体 ,定义了调用set_favorites
指令时需要提供的账户列表及其约束。- 它指定了指令执行时涉及的账户(
user
、favorites
和system_program
)以及它们的角色和验证规则。 - 使用
#[derive(Accounts)]
宏,Anchor 会自动生成代码来验证这些账户是否符合约束(例如,user
必须是签名者,favorites
必须是有效的 PDA)。
存储位置:
- 仅存在于指令执行的上下文环境中,不会在链上存储。
生命周期:
- 仅在指令调用期间存在,执行完成后即销毁。
作用:
- 提供指令执行所需的账户信息,并通过 Anchor 的约束(例如
mut
、init_if_needed
、seeds
等)确保账户的正确性和安全性。 - 链接到
Favorites
结构体(通过favorites: Account<'info, Favorites>
),将指令的输入数据存储到链上的Favorites
账户。
总结对比
特性 | Favorites | SetFavorites |
---|---|---|
类型 | 账户数据结构(#[account] ) |
账户上下文结构(#[derive(Accounts)] ) |
用途 | 定义链上存储的数据格式 | 定义指令执行时需要的账户及其约束 |
存储位置 | 存储在链上账户(PDA) | 仅存在于指令调用上下文,临时使用 |
生命周期 | 持久化,账户存在期间一直保留 | 临时,仅在指令执行期间有效 |
功能 | 存储用户数据(number 、color 等) |
验证和提供指令所需的账户(如签名者、PDA) |
Anchor 宏 | #[account] , #[derive(InitSpace)] |
#[derive(Accounts)] |
与链上交互 | 直接存储在链上 | 间接通过 favorites 字段操作链上数据 |
实际应用中的取舍示例
假设你要扩展程序,添加以下功能:
- 读取喜好 (
get_favorites
指令)。 - 更新部分喜好 (
update_favorites
指令,仅更新color
)。
设计步骤:
定义数据结构(Favorites):
- 保持当前的
Favorites
结构体不变,因为它已经满足需求。 - 如果需要新字段(例如,
last_updated: u64
记录更新时间),可以添加:
rust
#[account]
#[derive(InitSpace)]
pub struct Favorites {
pub number: u64,
#[max_len(50)]
pub color: String,
#[max_len(5, 50)]
pub hobbies: Vec<String>,
pub last_updated: u64,
}
定义指令上下文(SetFavorites、GetFavorites 等):
- GetFavorites(只读):
rust
#[derive(Accounts)]
pub struct GetFavorites<'info> {
pub user: Signer<'info>,
#[account(
seeds=[b"solana_business_card", user.key().as_ref()],
bump
)]
pub favorites: Account<'info, Favorites>,
}
- UpdateFavorites (只更新
color
):
rust
#[derive(Accounts)]
pub struct UpdateFavorites<'info> {
#[account(mut)]
pub user: Signer<'info>,
#[account(
mut,
seeds=[b"solana_business_card", user.key().as_ref()],
bump
)]
pub favorites: Account<'info, Favorites>,
}