solana_business_card合约解析

文章背景:

上篇文章,我们使用 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 个字符。
  • 将用户的 numbercolorhobbies 存储到 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! 宏进行输入验证,失败时抛出相应错误。

工作流程

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

struct Favorites 和 struct SetFavorites

在上面 Solana 程序中,struct Favoritesstruct 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 区块链上的数据内容,用于持久化用户的喜好信息(numbercolorhobbies)。
  • 使用 #[account] 宏标记,告诉 Anchor 这是一个账户结构体,Anchor 会自动处理其序列化和反序列化。
  • #[derive(InitSpace)] 宏用于自动计算账户所需的存储空间。

存储位置

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

生命周期

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

作用

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

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 指令时需要提供的账户列表及其约束。
  • 它指定了指令执行时涉及的账户(userfavoritessystem_program)以及它们的角色和验证规则。
  • 使用 #[derive(Accounts)] 宏,Anchor 会自动生成代码来验证这些账户是否符合约束(例如,user 必须是签名者,favorites 必须是有效的 PDA)。

存储位置

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

生命周期

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

作用

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

总结对比

特性 Favorites SetFavorites
类型 账户数据结构(#[account] 账户上下文结构(#[derive(Accounts)]
用途 定义链上存储的数据格式 定义指令执行时需要的账户及其约束
存储位置 存储在链上账户(PDA) 仅存在于指令调用上下文,临时使用
生命周期 持久化,账户存在期间一直保留 临时,仅在指令执行期间有效
功能 存储用户数据(numbercolor 等) 验证和提供指令所需的账户(如签名者、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>,
}
相关推荐
DevOps0081 天前
在 Azure Linux 上安装 RustFS
rust·minio·分布式存储·s3·rustfs
hweiyu001 天前
Rust 简介
rust
萧曵 丶1 天前
Rust中Option和Result详解
开发语言·后端·rust·option·result
火柴就是我1 天前
每日见闻之Rust中 trait 的孤儿规则
android·rust
zstar-_2 天前
Hello, Tauri!
rust
我是前端小学生2 天前
Rust中的Vec数据结构介绍
rust
小马哥聊DevSecOps2 天前
RustFS:Rust 语言编写的分布式存储系统初探
rust
UestcXiye2 天前
Rust Web 全栈开发(八):添加功能并重构
rust·actix
UestcXiye2 天前
Rust Web 全栈开发(六):在 Web 项目中使用 MySQL 数据库
数据库·rust