solana_business_card合约解析

  • Louis
  • 发布于 21小时前
  • 阅读 91

文章背景:上篇文章,我们使用Anchor工程化环境,从初始化项目、编译、测试、部署各个环节演示了一个真实的solana链上程序的开发流程。这篇文章,我们从语法和业务的角度来梳理下我们实现的合约的源码

文章背景:

上篇文章,我们使用 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 个字符。

<!---->

  • 将用户的 numbercolorhobbies 存储到 Favorites 账户中。
  • 输出日志,记录用户的公钥、程序 ID、设置的数字、颜色和爱好。

返回:成功执行返回 Ok(()),失败则抛出自定义错误。

账户结构(Accounts)

Favorites 账户:存储用户的最喜欢的数字、颜色和爱好。

使用 #[account]#[derive(InitSpace)] 宏定义,确保账户空间计算准确。

字段:

  • number: u64(8 字节)。
  • color: String(最大 50 字符,包含 4 字节长度前缀)。
  • hobbies: Vec&lt;String>(最多 5 个字符串,每个字符串最大 50 字符,包含向量长度前缀)。

空间计算:ANCHOR_DISCRIMINATOR_SIZE(8 字节)+ Favorites::INIT_SPACE

SetFavorites 账户上下文

user: Signer&lt;'info>:调用指令的签名者(用户),需要支付账户初始化费用。

favorites: Account&lt;'info, Favorites>

  • 使用 PDA,种子为 b"solana_business_card" 和用户公钥。
  • 如果账户不存在,自动初始化(init_if_needed)。
  • 空间大小为 ANCHOR_DISCRIMINATOR_SIZE + Favorites::INIT_SPACE

system_program: Program&lt;'info, System>:用于账户创建和初始化的系统程序。

错误处理

自定义错误类型 CustomError

  • ColorTooLong:颜色字符串超过 50 个字符。
  • TooManyHobbies:爱好数量超过 5 个。
  • HobbyTooLong:单个爱好字符串超过 50 个字符。

使用 require! 宏进行输入验证,失败时抛出相应错误。

工作流程

  1. 用户调用 set_favorites 指令,传入 numbercolorhobbies
  2. 程序验证:
    • 颜色字符串长度 ≤ 50。
    • 爱好数量 ≤ 5 且每个爱好长度 ≤ 50。
  1. 如果验证通过,程序将数据存储到用户的 Favorites PDA 中。
  2. 输出日志,记录用户的公钥、设置的数字、颜色和爱好。
  3. 返回成功或抛出错误。

struct Favorites 和 struct SetFavorites

在上面 Solana 程序中,struct Favoritesstruct SetFavorites 是两个不同用途的结构体,分别用于不同的场景。

主要区别

Favorites 结构体

定义

#[account]
#[derive(InitSpace)]
pub struct Favorites {
    pub number: u64,
    #[max_len(50)]
    pub color: String,
    #[max_len(5, 50)]
    pub hobbies: Vec&lt;String>,
}

用途

  • Favorites 是一个账户数据结构,定义了存储在链上账户(PDA)中的数据格式。
  • 它表示程序实际存储在 Solana 区块链上的数据内容,用于持久化用户的喜好信息(numbercolorhobbies)。
  • 使用 #[account] 宏标记,告诉 Anchor 这是一个账户结构体,Anchor 会自动处理其序列化和反序列化。
  • #[derive(InitSpace)] 宏用于自动计算账户所需的存储空间。

存储位置

  • 存储在链上的账户中(favorites PDA),每次调用 set_favorites 指令时会更新该账户的数据。

生命周期

  • 持久化存储,只要账户未被关闭,数据会一直保留在链上。

作用

  • 定义了数据的结构和约束(例如,colorhobbies 的最大长度)。
  • 用于存储和读取链上数据。

SetFavorites 结构体

定义

#[derive(Accounts)]
pub struct SetFavorites&lt;'info> {
    #[account(mut)]
    pub user: Signer&lt;'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&lt;'info, Favorites>,
    pub system_program: Program&lt;'info, System>,
}

用途

  • SetFavorites 是一个账户上下文结构体,定义了调用 set_favorites 指令时需要提供的账户列表及其约束。
  • 它指定了指令执行时涉及的账户(userfavoritessystem_program)以及它们的角色和验证规则。
  • 使用 #[derive(Accounts)] 宏,Anchor 会自动生成代码来验证这些账户是否符合约束(例如,user 必须是签名者,favorites 必须是有效的 PDA)。

存储位置

  • 仅存在于指令执行的上下文环境中,不会在链上存储。

生命周期

  • 仅在指令调用期间存在,执行完成后即销毁。

作用

  • 提供指令执行所需的账户信息,并通过 Anchor 的约束(例如 mutinit_if_neededseeds 等)确保账户的正确性和安全性。
  • 链接到 Favorites 结构体(通过 favorites: Account&lt;'info, Favorites>),将指令的输入数据存储到链上的 Favorites 账户。

总结对比

特性 Favorites SetFavorites
类型 账户数据结构(#[account] 账户上下文结构(#[derive(Accounts)]
用途 定义链上存储的数据格式 定义指令执行时需要的账户及其约束
存储位置 存储在链上账户(PDA) 仅存在于指令调用上下文,临时使用
生命周期 持久化,账户存在期间一直保留 临时,仅在指令执行期间有效
功能 存储用户数据(numbercolor 等) 验证和提供指令所需的账户(如签名者、PDA)
Anchor 宏 #[account], #[derive(InitSpace)] #[derive(Accounts)]
与链上交互 直接存储在链上 间接通过 favorites 字段操作链上数据

指令中context参数的含义

context 是 Anchor 程序中每个指令处理函数的第一个必需参数。它包含了执行这个指令所需的所有上下文信息,包括:

  • 账户信息 - 调用这个指令时传入的所有账户
  • 程序信息 - 当前程序的 ID 和相关信息
  • 其他执行上下文 - 如剩余账户、bumps 等

Context<SetFavorites> 的含义

这是一个泛型类型,其中:

  • Context&lt;T> 是 Anchor 提供的通用上下文类型
  • SetFavorites 是类型参数,定义了这个指令需要哪些账户

从代码中可以看到 SetFavorites 结构体定义了三个账户:

#[derive(Accounts)]
pub struct SetFavorites&lt;'info> {
    #[account(mut)]
    pub user: Signer&lt;'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&lt;'info, Favorites>,  // 存储收藏信息的 PDA 账户

    pub system_program: Program&lt;'info, System>, // 系统程序
}

使用方式

在函数中,你可以通过 context.accounts 来访问这些账户:

let user_public_key = context.accounts.user.key();  // 获取用户公钥
context.accounts.favorites.set_inner(Favorites { ... }); // 设置收藏数据

上面说的这个 context 是 Anchor 框架 的特性,不是 Rust 的特性

Rust 语言本身提供的:

  • 泛型语法 &lt;T> - 这是 Rust 的核心特性
  • 结构体和函数 - 基础语言特性

Anchor 框架提供的:

  • Context&lt;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&lt;SetFavorites>,  // Anchor 提供的上下文
    number: u64,
    color: String,
    hobbies: Vec&lt;String>,
) -> Result&lt;()> {
    // 直接使用,Anchor 已经验证了所有账户
    context.accounts.user.key();
}
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论