从0到1,手把手教你写一个solana_business_card链上程序

项目初始化:

本小节,我们继续探索如何使用 anchor 这个框架来从 0 到 1 写一个 solana 程序。

  1. 找一个空的目录,使用 anchor init 命令进行项目初始化:
csharp 复制代码
 anchor init solana_business_card

执行上面命令之后,anchor会使用 yarn 命令初始化项目,项目初始化之后,我们用编辑器打开。

  1. 我们查看下项目中的配置文件:
  • programs/solana_business_card/Cargo.toml
toml 复制代码
[package]
name = "solana_business_card"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"

[lib]
crate-type = ["cdylib", "lib"]
name = "solana_business_card"

[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build"]

[dependencies]
anchor-lang = "0.31.1"

我们来梳理下每个配置的作用,这里有一个坑点,我们稍后回来解决:

perl 复制代码
[package]
  ● name:包的名称,这里是 solana_business_card,即你的 Solana 程序的名字。
  ● version:版本号,0.1.0,用于标识当前包的版本。
  ● description:包的描述,这里是 "Created with Anchor",说明是用 Anchor 框架创建的。
  ● edition:Rust 语言的版本,这里是 2021,表示使用 Rust 2021 版的语法和特性。
[lib]
  ● crate-type:指定生成的库类型。  
    ○ "cdylib":生成 C 语言兼容的动态库,Solana 程序部署时需要这个类型。
    ○ "lib":生成 Rust 的普通库,方便本地开发和测试。
  ● name:库的名称,和 package name 一致。
[features]
Rust 的可选功能模块(feature),可以通过编译参数选择性启用。
  ● default:默认启用的 feature,这里是空数组,表示没有默认 feature。
  ● cpi:包含 "no-entrypoint",用于构建 CPI(Cross-Program Invocation)时,不包含入口函数(entrypoint)。
  ● no-entrypoint:不包含入口函数,通常用于 CPI 场景。
  ● no-idl:不生成 IDL(Interface Definition Language),有些场景下不需要 IDL 文件。
  ● no-log-ix-name:不记录指令名称到日志,减少日志输出。
  ● idl-build:启用时会启用 anchor-lang 的 idl-build feature,用于生成 IDL。
[dependencies]
  ● anchor-lang = "0.31.1"
  依赖 Anchor 框架的核心库,版本为 0.31.1。Anchor 是 Solana 上最流行的智能合约开发框架。
  1. solana_business_card/Anchor.toml
toml 复制代码
[toolchain]
package_manager = "yarn"

[features]
resolution = true
skip-lint = false

[programs.localnet]
solana_business_card = "3u7HtoiWaiqsPj551oM6Hh1sqooek3FenXuXzqFvnq7u"

[registry]
url = "https://api.apr.dev"

[provider]
cluster = "Localnet"
wallet = "~/.config/solana/id.json"

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

我们来解析下这个配置文件:

markdown 复制代码
### [toolchain] 部分
- `package_manager`: 指定使用 yarn 作为包管理器

### [features] 部分
- `resolution`: 启用依赖解析功能
- `skip-lint`: 设置为 false,表示不跳过代码检查

### [programs.localnet] 部分
- `solana_bank_demo`: 定义程序 ID,这是程序在 Solana 网络上的唯一标识符
- `"3u7HtoiWaiqsPj551oM6Hh1sqooek3FenXuXzqFvnq7u"` 是程序的公钥地址

### [registry] 部分
- `url`: 指定 Anchor 包注册表的 URL,用于发布和下载 Anchor 包

### [provider] 部分
- `cluster`: 设置为 "localnet",表示使用本地测试网络
- `wallet`: 指定钱包密钥对的路径,这里使用的是默认的 Solana CLI 钱包路径

### [scripts] 部分
- `test`: 定义测试命令
- 使用 ts-mocha 运行测试
- `-p ./tsconfig.json`: 指定 TypeScript 配置文件
- `-t 1000000`: 设置测试超时时间为 1000000 毫秒
- `tests/**/*.ts`: 运行 tests 目录下所有的 TypeScript 测试文件

我们需要重点关注的是 programs.localnet 这个部分,这里显示的是程序的公钥地址。

3、程序的公钥地址是如何生成的呢?

我们在初始化项目的时候,anchor 框架自动的帮我们生成了这个公钥地址,并且在 target/deploy 目录中,还生成了一个 json 文件,格式就是 program_name-keypair.json,所以我这个项目当前的 文件名称就是:solana_business_card-keypair.json

需要注意的是,程序 ID 是确定性的,由程序的密钥对唯一决定,在本地开发时,每次重新生成密钥对都会得到新的程序 ID,在生产环境中,应该妥善保管程序的密钥对文件,因为它关系到程序的所有权和更新权限。

执行构建操作:

我们可以使用 anchor build 执行程序的构建操作,在构建操作之前,我们先将代码准备好:

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,
}

当我把这个代码贴到 lib.rs 中的时候,程序就直接报错了,提示我们当前我们的配置中不支持 init_if_needed 这个特性,还记得上面我们说的一个坑点吗,就是这里,我们修改下 Cargo.toml 相关配置:

programs/solana_bank_demo/Cargo.toml

ini 复制代码
[dependencies]
anchor-lang = { version = "0.31.1", features = ["init-if-needed"] }

添加完毕之后,发现我们的程序不报错了。

每次在执行编译之前,我们可以先执行 anchor clean,避免出现问题。

scss 复制代码
   Compiling anchor-lang v0.31.1
   Compiling solana_business_card v0.1.0 (/Users/louis/code/solana_program/solana_business_card/programs/solana_business_card)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 14.07s
     Running unittests src/lib.rs (/Users/louis/code/solana_program/solana_business_card/target/debug/deps/solana_business_card-c0b87f8b49943c78)

如果没有报错,说明,我们的构建成功了。

编写测试用例:

anchor 帮助我们生成了一个文件:tests/solana_business_card.ts,我们可以在里面编写测试用例。

我这里准备好了代码,因为测试用例代码太长,就不贴代码了,直接贴上 github 的链接:

github.com/MagicalBrid...

执行测试操作:

我们可以使用 anchor test 命令来帮住我们执行测试用例:

bash 复制代码
Found a 'test' script in the Anchor.toml. Running it as a test suite!

Running test suite: "/Users/louis/code/solana_program/solana_business_card/Anchor.toml"

yarn run v1.22.22
$ /Users/louis/code/solana_program/solana_business_card/node_modules/.bin/ts-mocha -p ./tsconfig.json -t 1000000 'tests/**/*.ts'

  solana_business_card
    基本功能测试
交易签名: 5VEXXm7z1haDWY7VV4bUSHufbhbitzceXkZEW1FP1TTKsNym5Y7U9nzBUbTrQr7CSYaiUA3SSzD3XZXnqmm8q5Jh
      ✔ 应该能够成功设置用户偏好 (463ms)
      ✔ 应该能够更新已存在的用户偏好 (461ms)
      ✔ 不同用户应该有独立的偏好存储 (474ms)
    边界条件测试
      ✔ 应该能够处理最大长度的颜色字符串 (936ms)
      ✔ 应该能够处理最大数量和长度的爱好 (937ms)
      ✔ 应该能够处理空爱好数组 (930ms)
      ✔ 应该能够处理最大u64数值 (919ms)
    安全性测试
      ✔ 应该拒绝未签名的交易 (468ms)
      ✔ 应该拒绝用错误的用户修改他人的偏好 (1401ms)
      ✔ 应该验证PDA地址的正确性 (473ms)
    错误处理测试
      ✔ 应该拒绝超过长度限制的颜色字符串 (468ms)
      ✔ 应该拒绝超过数量限制的爱好 (451ms)
      ✔ 应该拒绝超过长度限制的单个爱好 (464ms)
    状态一致性测试
      ✔ 应该在多次调用后保持数据一致性 (1854ms)

  14 passing (12s)

✨  Done in 12.89s.

如果显示上面打印的信息,说明我们的测试用例全部通过了。

部署程序:

我们先看下本地的环境信息:

arduino 复制代码
➜  solana_business_card git:(main) solana config get                                            
Config File: /Users/louis/.config/solana/cli/config.yml
RPC URL: http://localhost:8899 
WebSocket URL: ws://localhost:8900/ (computed)
Keypair Path: /Users/louis/.config/solana/id.json 
Commitment: confirmed 

从打印的信息可以看到,目前我们设置的是本地环境,此时我们可以启动本地验证器:

yaml 复制代码
➜  solana_business_card git:(main) solana-test-validator
Ledger location: test-ledger
Log: test-ledger/validator.log
⠄ Initializing...                                                                                                                                                                                                           Waiting for fees to stabilize 1...
Identity: Cs2Zcp7YyvEPhuSQQzSna1mBgfGsyhbzvufBwBuXEaNG
Genesis Hash: 48K4PqE3fN57UyUGKKq6YLfJRq2jFgmSX8Zr1UDViKi1
Version: 2.2.20
Shred Version: 15409
Gossip Address: 127.0.0.1:1024
TPU Address: 127.0.0.1:1027
JSON RPC URL: http://127.0.0.1:8899
WebSocket PubSub URL: ws://127.0.0.1:8900
⠁ 00:00:13 | Processed Slot: 27 | Confirmed Slot: 27 | Finalized Slot: 0 | Full Snapshot Slot: - | Incremental Snapshot Slot: - | Transactions: 26 | ◎499.999870000  

我们可以使用 anchor deploy 这个命令来部署程序:

javascript 复制代码
➜  solana_business_card git:(main) anchor deploy
Deploying cluster: http://127.0.0.1:8899
Upgrade authority: /Users/louis/.config/solana/id.json
Deploying program "solana_business_card"...
Program path: /Users/louis/code/solana_program/solana_business_card/target/deploy/solana_business_card.so...
Program Id: BYBFmxjHn48LVAjKfo7dX6kPTw62HNPTktMqnpNeeiHu

Signature: QMKzHh2eA7Rzs3wC3rQY3NovRztsd2LifSGbvpXD9udPh8wpQt6J7h2E2E97gh7dvQd2tLNxkJ6EzoHpKZdgD5V

Deploy success

因为是本地部署,所以执行的速度非常快。看到上面的信息,说明我们的程序部署成功了。

代码仓库链接:

github.com/MagicalBrid...

相关推荐
许强0xq1 天前
Solidity 的十年与重生:从 Classic 到 Core
web3·区块链·智能合约·solidity·以太坊
小攻城狮长成ing1 天前
从0开始学区块链第12天—如何使用可见性标识符
web3·区块链·智能合约·solidity·以太坊
阿登林3 天前
区块链技术在生产数据管理中的应用:Hyperledger Fabric与蚂蚁链智能合约设计
区块链·智能合约·fabric
野老杂谈5 天前
如何快速学习智能合约开发语言 Solidity
开发语言·学习·智能合约·solidity·以太坊·区块链开发
老程序员刘飞6 天前
hardhat 搭建智能合约
开发语言·php·智能合约
许强0xq13 天前
Gas优化大师目录
web3·区块链·智能合约·solidity·foundry·ethernaut·gas优化
Joy T15 天前
Solidity智能合约开发入门攻略
web3·区块链·智能合约·solidity·以太坊·共识算法
Joy T15 天前
Solidity智能合约存储与数据结构精要
数据结构·区块链·密码学·智能合约·solidity·合约function
友莘居士18 天前
Java基于Web3j调用智能智能合约案例
java·web3·智能合约
安当加密21 天前
智能合约在分布式密钥管理系统中的应用
分布式·智能合约