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>,
}
相关推荐
m0_4805026417 小时前
Rust 登堂 之 类型转换(三)
开发语言·后端·rust
ftpeak19 小时前
Rust Web开发指南 第六章(动态网页模板技术-MiniJinja速成教程)
开发语言·前端·后端·rust·web
编码浪子19 小时前
趣味学Rust基础篇(数据类型)
开发语言·后端·rust
编码浪子1 天前
趣味学习Rust基础篇(用Rust做一个猜数字游戏)
学习·rust
love530love2 天前
怎么更新 cargo.exe ?(Rust 工具链)
人工智能·windows·python·rust·r语言
Source.Liu2 天前
【typenum】 23 倒序存储的无符号整数(private.rs片段)
rust
咸甜适中2 天前
rust语言(1.88.0)sqlite数据库rusqlite库(0.37.0)学习笔记
数据库·rust·sqlite·rusqlite
jinlei20092 天前
在python 代码中调用rust 源码库操作步骤
开发语言·python·rust
m0_480502643 天前
Rust 登堂 之 函数式编程(三)
开发语言·后端·rust
小喷友3 天前
阶段一:入门(理解 Rust 的基本概念)
前端·rust