文章背景:
上篇文章,我们使用 Anchor 工程化环境,从初始化项目、编译、测试、部署各个环节演示了一个真实的 solana 链上程序的开发流程。这篇文章,我们从语法和业务的角度来梳理下我们实现的合约的源码。
solana_business_card 合约源码:
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 结构体
定义:
#[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 结构体
定义:
#[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 字段操作链上数据 |
指令中context参数的含义
context
是 Anchor 程序中每个指令处理函数的第一个必需参数。它包含了执行这个指令所需的所有上下文信息,包括:
- 账户信息 - 调用这个指令时传入的所有账户
- 程序信息 - 当前程序的 ID 和相关信息
- 其他执行上下文 - 如剩余账户、bumps 等
Context<SetFavorites
> 的含义
这是一个泛型类型,其中:
Context<T>
是 Anchor 提供的通用上下文类型SetFavorites
是类型参数,定义了这个指令需要哪些账户
从代码中可以看到 SetFavorites
结构体定义了三个账户:
#[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>, // 存储收藏信息的 PDA 账户
pub system_program: Program<'info, System>, // 系统程序
}
使用方式
在函数中,你可以通过 context.accounts
来访问这些账户:
let user_public_key = context.accounts.user.key(); // 获取用户公钥
context.accounts.favorites.set_inner(Favorites { ... }); // 设置收藏数据
上面说的这个 context 是 Anchor 框架 的特性,不是 Rust 的特性
Rust 语言本身提供的:
- 泛型语法
<T>
- 这是 Rust 的核心特性 - 结构体和函数 - 基础语言特性
Anchor 框架提供的:
Context<T>
类型 - 这是 Anchor 专门为 Solana 程序开发设计的- 账户验证系统 - 自动验证账户权限、类型等
#[derive(Accounts)]
宏 - 自动生成账户验证代码
为什么需要 Anchor 的 Context?
在原生 Solana 程序中,你需要手动处理所有账户:
// 原生 Solana 写法(复杂且容易出错)
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// 手动解析和验证每个账户
let accounts_iter = &mut accounts.iter();
let user_account = next_account_info(accounts_iter)?;
let favorites_account = next_account_info(accounts_iter)?;
// ... 大量手动验证代码
}
而 Anchor 把这些复杂性抽象化了:
// Anchor 写法(简洁且类型安全)
pub fn set_favorites(
context: Context<SetFavorites>, // Anchor 提供的上下文
number: u64,
color: String,
hobbies: Vec<String>,
) -> Result<()> {
// 直接使用,Anchor 已经验证了所有账户
context.accounts.user.key();
}