通过示例学习 Anchor 框架:轻松上手 Solana 开发
Anchor CLI
1. Anchor CLI 简介
- Anchor 是一个基于 Rust 的 Solana 开发框架,支持智能合约的开发,并提供了完整的工具链来简化开发流程。
- 依赖安装:需要安装 Rust、Solana、Yarn 等依赖,然后安装 Anchor 版本管理工具
avm
(类似于 Node.js 的nvm
)。 - 安装指南:Anchor 安装指南
我安装的相关版本如下:
rustc 1.75.0 (82e1608df 2023-12-21)
solana-cli 1.18.17 (src:c027cfc3; feat:4215500110, client:Agave)
anchor-cli 0.30.1
2. anchor help
帮助指令
- 打印所有可用命令:
anchor help
- 获取子命令帮助信息:
anchor help [subcommand]
- 例如:
anchor help deploy
显示deploy
命令的用法和选项。
- 例如:
3. 常用指令
-
创建新项目:
anchor init my_project
- 创建一个包含示例代码的新 Anchor 项目。
- 注意项目名需要使用蛇形命名。
-
构建程序:
anchor build
- 在
target/deploy
目录下生成编译后的合约二进制文件。 - 常用选项:
- --skip-lint: 跳过代码静态检查。
- --features <features>: 指定编译特性。
- 在
-
测试程序:
anchor test
- 常用选项:
- --skip-build: 跳过构建步骤,仅运行测试。
- --bpf: 使用 BPF 测试,更接近实际部署环境。
- --features <features>: 指定编译特性。
- 常用选项:
-
部署程序:
anchor deploy
- 将程序部署到指定的 Solana 网络。
- 在
Anchor.toml
的[provider]
配置项中修改cluster
,例如:cluster = "Devnet"
-
创建新的程序:
anchor new <program-name>
- 在工作空间中创建一个新的程序(智能合约)。
- 通常先使用
anchor init
初始化工作空间,然后使用anchor new
创建程序。
-
升级程序
anchor upgrade <target/deploy/my_project.so> --program-id <program-id>
- 升级步骤:
- 部署最新版本的程序到 Solana 区块链。
- 初始化接口定义(IDL),使客户端能够正确解析合约数据结构和方法。
- 执行所有必要的迁移脚本,确保链上合约状态的平滑过渡。
- 账户空间不足错误: 如果升级的时候遇到
account data too small for instruction
报错,则需要调整空间后再升级solana program extend PROGRAM_ID 60000 -u d -k KEYPAIR_FILE_PATH # 例如调整到 60000 (60 KB) solana program extend FwtCTkoxTtWRYZZMpeTfsvHwHwU6YmH5KfvknA2bdqfL 60000 -u d -k ~/.config/solana/id.json
- 升级步骤:
4. 项目目录结构
Anchor.toml
:项目的配置文件。programs/
:包含程序的目录,每个程序有自己的子目录。Cargo.toml
:程序的 Rust 项目配置文件。src/
:包含程序代码文件,通常是lib.rs
。tests/
:包含测试代码文件。target/
:包含构建和编译生成的文件。
示例:计数器合约
- 创建新项目
执行以下命令初始化项目:
anchor init counter_anchor
- 获取程序 ID
创建项目后,Anchor 会自动生成一个程序 ID,可以在 Anchor.toml 和 lib.rs 的宏中找到。
我们可以使用 solana-keygen 指令查看当前的程序 ID:
solana-keygen pubkey target/deploy/counter_anchor-keypair.json
如果需要生成新的程序 ID,可以执行以下命令:
solana-keygen new -o target/deploy/counter_anchor-keypair.json --force
- 编写计数器合约
在 programs/counter_anchor/src/lib.rs 中替换为以下代码:
#![allow(clippy::result_large_err)]
use anchor_lang::prelude::*;
declare_id!("BmDHboaj1kBUoinJKKSRqKfMeRKJqQqEbUj1VgzeQe4A");
#[program]
pub mod counter_anchor {
use super::*;
pub fn initialize_counter(_ctx: Context<InitializeCounter>) -> Result<()> {
Ok(())
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
ctx.accounts.counter.count = ctx.accounts.counter.count.checked_add(1).unwrap();
Ok(())
}
}
#[derive(Accounts)]
pub struct InitializeCounter<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
space = 8 + Counter::INIT_SPACE,
payer = payer
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut)]
pub counter: Account<'info, Counter>,
}
#[account]
#[derive(InitSpace)]
pub struct Counter {
count: u64,
}
然后同步程序 ID:
anchor keys sync
- 编写测试脚本
同样,我们把测试脚本也替换成以下代码:
import * as anchor from '@coral-xyz/anchor';
import type { Program } from '@coral-xyz/anchor';
import { Keypair } from '@solana/web3.js';
import { assert } from 'chai';
import type { CounterAnchor } from '../target/types/counter_anchor';
describe('counter_anchor', () => {
// Configure the client to use the local cluster.
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const payer = provider.wallet as anchor.Wallet;
const program = anchor.workspace.CounterAnchor as Program<CounterAnchor>;
// Generate a new keypair for the counter account
const counterKeypair = new Keypair();
it('Initialize Counter', async () => {
await program.methods
.initializeCounter()
.accounts({
counter: counterKeypair.publicKey,
payer: payer.publicKey,
})
.signers([counterKeypair])
.rpc();
const currentCount = await program.account.counter.fetch(counterKeypair.publicKey);
assert(currentCount.count.toNumber() === 0, 'Expected initialized count to be 0');
});
it('Increment Counter', async () => {
await program.methods.increment().accounts({ counter: counterKeypair.publicKey }).rpc();
const currentCount = await program.account.counter.fetch(counterKeypair.publicKey);
assert(currentCount.count.toNumber() === 1, 'Expected count to be 1');
});
it('Increment Counter Again', async () => {
await program.methods.increment().accounts({ counter: counterKeypair.publicKey }).rpc();
const currentCount = await program.account.counter.fetch(counterKeypair.publicKey);
assert(currentCount.count.toNumber() === 2, 'Expected count to be 2');
});
});
直接运行 anchor test
,会先编译再测试。因为我们使用的默认 Anchor.toml
,配置为:
[provider]
cluster = "Localnet"
所以,在测试时,Anchor 会使用本地 solana-test-validator,这个本地网络只在测试运行期间有效,测试完成后会被销毁。
正常情况下,可以看到测试通过。
counter_anchor
Initialize Counter (601ms)
Increment Counter (465ms)
Increment Counter Again (465ms)
在本地部署并且使用 CLI 查询
与 EVM 不同,Solana 采用程序与数据分离的设计。在我们的程序中,我们会新建一个密钥对,并将其交给程序来初始化账户数据。因此,想要查询计数器中的具体数据时,我们不能直接通过程序 ID 进行查询,而是需要使用账户的公钥。
通过以下示例,你将更直观地理解这个过程!
首先,打开一个新的终端,手动启动本地 Solana 节点:
solana-test-validator
然后,在测试脚本中添加一行代码,打印出输入给程序的账户地址:
console.log('Counter Keypair:', counterKeypair.publicKey.toBase58());
接着,运行测试,同时使用 --skip-local-validator 标志,避免自动启动本地 Solana 验证节点:
anchor test --skip-local-validator
测试运行后,程序将部署到本地节点,并在终端输出随机生成的公钥,例如:
Counter Keypair: 714hf7qcXREvtvWEKYSn4w9ujxD3c5JR5oUCJoQ7NGha
这个地址就是用于初始化计数器的账户地址。测试完成后,该账户的 count 值应为 2。
此时,我们可以使用 anchor account 命令来查询该账户的信息:
anchor account counter_anchor.Counter 714hf7qcXREvtvWEKYSn4w9ujxD3c5JR5oUCJoQ7NGha
返回的结果如下:
{
"count": 2
}
这里,counter_anchor.Counter 读取了 IDL 文件,而后面的账户地址用于查询数据。可以看到,在 Solana 中,查询账户数据并不依赖合约地址,而是直接通过账户公钥来访问。
在 Dev 网络上部署测试
修改 Anchor.toml
配置文件,将 Localnet 改为 Devnet。或者 "Devnet" 直接放私有的 RPC 链接。
[provider]
cluster = "Devnet"
wallet = "~/.config/solana/id.json"
确保 "~/.config/solana/id.json" 对应的账户在 Dev 网络上有测试代币。然后运行 anchor test
。
如果遇到网络问题,比如:
Error: AccountNotFound: pubkey=4Ak8RNHDqWQCyjxVMk1W95DDDW4ntaM92Qq6kEBKAHvV: error decoding response body: unexpected end of file
There was a problem deploying: Output { status: ExitStatus(unix_wait_status(256)), stdout: "", stderr: "" }.
那么就像之前文章说过的,可以试试 proxychains4
代理以及 Helius
申请私有 RPC。
结果如下:
Counter Keypair: 6NFQPBVMnoHDc3ruiw52sxPDAZAziAoqGBSnWcJr35kH
counter_anchor
Initialize Counter (5579ms)
Increment Counter (4108ms)
Increment Counter Again (3290ms)
然后我们来试试 anchor account
指令来在 Dev 网络上查询,能得到正确结果:
anchor account counter_anchor.Counter 6NFQPBVMnoHDc3ruiw52sxPDAZAziAoqGBSnWcJr35kH
{
"count": 2
}
关闭 Solana 程序
首先,我们需要配置 Solana 命令行工具(solana
)的网络:
solana config set --url https://api.devnet.solana.com
为了全额退回已支付的租金,可以使用 solana program close <PROGRAM_ID>
命令来关闭 Solana 程序。例如:
solana program close 4HAbxhZ8twJfwbbGc8jpQUqLSr63cwScm8RsQv1wYkTy --bypass-warning
执行该命令时,Solana 可能会提示警告信息。若要跳过这些警告,可以加上 --bypass-warning
选项。
注意:一旦程序被关闭,其 Program ID 将无法再用于部署新的程序,请谨慎操作。
成功关闭程序后,终端将返回类似以下的消息,表示已回收租金:
Closed Program Id 4HAbxhZ8twJfwbbGc8jpQUqLSr63cwScm8RsQv1wYkTy, 1.4170908 SOL reclaimed
这样,程序占用的 SOL 资源将被释放并退回到你的账户。
版权声明
本文仅代表作者观点,不代表区块链技术网立场。
本文系作者授权本站发表,未经许可,不得转载。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。