项目初始化:
本小节,我们继续探索如何使用 anchor 这个框架来从 0 到 1 写一个 solana 程序。
- 找一个空的目录,使用 anchor init 命令进行项目初始化:
csharp
anchor init solana_business_card
执行上面命令之后,anchor会使用 yarn 命令初始化项目,项目初始化之后,我们用编辑器打开。
- 我们查看下项目中的配置文件:
- 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 上最流行的智能合约开发框架。
- 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 的链接:
执行测试操作:
我们可以使用 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
因为是本地部署,所以执行的速度非常快。看到上面的信息,说明我们的程序部署成功了。