手摸手带你用anchor框架写一个Bank链上程序

  • Louis
  • 发布于 3天前
  • 阅读 309

这篇文章,我们从工程化的角度来带领大家使用 anchor 框架来实现一个 Bank 链上程序。

大家好,感谢你们点开这篇文章!如果有兴趣,欢迎关注我的 GitHub,里面有一些我的小项目和代码,水平有限,还请多多指教!

相关背景:

手把手带你写一个solana程序:计数器合约 这篇文章中,我们实现了一个简单的计数器合约,并从源码的层面对其中涉及到的一些概念做了梳理,想必大家已经掌握了基本的概念。深入理解账户的概念能帮助我们更好的掌握开发套路。

这篇文章,我们从工程化的角度来带领大家使用 anchor 框架来实现一个 Bank 链上程序。

环境准备:

一文说透,如何在solana上铸造spl-token 这篇文章中,我已经详细的介绍了开发环境的相关配置,这里不再赘述,有需要的朋友可以移步这里查看。

1、我们在本地查看环境配置:

➜  ~ 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

从上面的输出可以看到,我目前连接的是本地环境,这篇文章,我们就使用这个环境来给大家做演示。

项目初始化:

1、找一个空的目录,使用 anchor init 命令进行项目初始化:

➜ anchor init solana_bank_demo

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

2、查看一些常用的配置文件:

programs/solana_bank_demo/Cargo.toml:

[package]
name = "solana_bank_demo"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"

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

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

[dependencies]
anchor-lang = "0.31.0"

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

### [package] 部分
- `name`: 定义包的名称为 "solana_bank_demo"
- `version`: 包的版本号,遵循语义化版本规范
- `description`: 包的描述信息
- `edition`: Rust 版本,使用 2021 版本

### [lib] 部分
- `crate-type`: 指定生成的库类型
  - `cdylib`: 生成动态链接库,用于与其他语言交互
  - `lib`: 生成 Rust 标准库
- `name`: 指定库的名称

### [features] 部分(Solana/Anchor 特定功能)
- `default`: 默认启用的特性列表
- `cpi`: Cross-Program Invocation 特性,用于程序间调用
- `no-entrypoint`: 禁用程序入口点,通常在作为依赖被其他程序调用时使用
- `no-idl`: 禁用 IDL(Interface Description Language)生成
- `no-log-ix-name`: 禁用指令名称日志记录
- `idl-build`: 启用 Anchor IDL 构建功能

### [dependencies] 部分
- `anchor-lang`: 依赖 Anchor 框架,版本 0.31.0,这是 Solana 智能合约开发的主要框架

/solana_bank_demo/Anchor.toml:

[toolchain]
package_manager = "yarn"

[features]
resolution = true
skip-lint = false

[programs.localnet]
solana_bank_demo = "G3ABM8zJtyFrJF5e2D5CwvRxh7jDqHxFd9Rw6MF7uMpX"

[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"

<!---->

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

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

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

### [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_bank_demo-keypair.json

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

执行构建操作:

我们迫不及待想执行 build 操作,来构建我们的程序。

anchor build

<!---->

➜  solana_bank_demo git:(master) ✗ anchor build                
error: rustc 1.79.0-dev is not supported by the following package:
                 Note that this is the rustc version that ships with Solana tools and not your system's rustc version. Use `solana-install update` or head over to https://docs.solanalabs.com/cli/install to install a newer version.
bytemuck_derive@1.9.2 requires rustc 1.84
Either upgrade rustc or select compatible dependency versions with
`cargo update &lt;name>@&lt;current-ver> --precise &lt;compatible-ver>`
where `&lt;compatible-ver>` is the latest version supporting rustc 1.79.0-dev

不出意外的话,肯定会出意外,直接报错,其实这个报错的原因就是 solana cli 中带有的 rustc 工具版本和我本地的安装的一个包不太兼容。bytemuck_derive\@1.9.2 requires rustc 1.84,我本地安装的 rust 版本比较新:

➜  solana_bank_demo git:(master) ✗ rustup --version
rustup 1.28.1 (f9edccde0 2025-03-05)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: The currently active `rustc` version is `rustc 1.85.0 (4d91de4e4 2025-02-17)`

如何解决呢?我尝试了一些方法,最终的解决方法是 对这个包进行降级:

➜  solana_bank_demo git:(master) ✗ cargo update bytemuck_derive@1.9.2 --precise 1.8.1
    Updating crates.io index
 Downgrading bytemuck_derive v1.9.2 -> v1.8.1

然后再次执行 anchor build,经过漫长的编译之后,就构建成功了。

执行测试操作:

在 tests 目录下,有一个测试文件,当我们指定 anchor test 的时候,就会执行这个测试文件。

➜  solana_bank_demo git:(master) ✗ anchor test                       

<!---->

solana_bank_demo
Your transaction signature jmSFrCk1LvaeB9P9CtZbLsQaSnvFn9VXrTbpVDKyN3Ta9yUfoCfyvAaQevYvebQqoebosNdqSKsXLTcuWaoV1uc
    ✔ Is initialized! (265ms)

  1 passing (266ms)
✨  Done in 2.25s.

如果显示出上面的打印内容,说明测试的脚本成功执行了。

执行部署操作:

到这里,我们还没有看,脚手架给我们默认生成的程序结构呢:

use anchor_lang::prelude::*;

declare_id!("G3ABM8zJtyFrJF5e2D5CwvRxh7jDqHxFd9Rw6MF7uMpX");

#[program]
pub mod solana_bank_demo {
    use super::*;

    pub fn initialize(ctx: Context&lt;Initialize>) -> Result&lt;()> {
        msg!("Greetings from: {:?}", ctx.program_id);
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize {}

其实很简单对吧,我们也并不需要关心它的内容,因为我们会把这部分内容全部删掉,然后写我们自己的逻辑。

在此之前,我们先测试一下部署到本地的测试环境:

➜  solana_bank_demo git:(master) ✗ solana-test-validator

Ledger location: test-ledger
Log: test-ledger/validator.log
⠈ Initializing...                                                                                                                                                                                             Waiting for fees to stabilize 1...
Identity: FNR8SCwvQwdcjdGhzZdnoEK77RRrMDGsG4XJJpUPht19
Genesis Hash: 2c8WcTPfKsSNTsxdae3h2tkWU3uQWTRVi3dnVF6j1VcV
Version: 2.1.17
Shred Version: 47570
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:07 | Processed Slot: 16 | Confirmed Slot: 15 | Finalized Slot: 0 | Full Snapshot Slot: - | Incremental Snapshot Slot: - | Transactions: 15 | ◎499.999930000 

我使用了 solana-test-validator 命令启动了测试环境,怎么验证这个环境有没有启动成功呢?我们可以打开浏览器链接本地的端口查看,看到出块信息说明我们的本地环境已经搭建好了。

执行 anchor deploy 命令,部署我们的程序:

➜  solana_bank_demo git:(master) ✗ anchor deploy
Deploying cluster: http://127.0.0.1:8899
Upgrade authority: /Users/louis/.config/solana/id.json
Deploying program "solana_bank_demo"...
Program path: /Users/louis/Documents/myProject/Solana_project/solana_bank_demo/target/deploy/solana_bank_demo.so...
Program Id: G3ABM8zJtyFrJF5e2D5CwvRxh7jDqHxFd9Rw6MF7uMpX

Signature: 4cygxMjzD18vuv5iZTDQYd6vMuvM3FmxPHLtPBKuipPiT6Lb2XyNS5AdEns7SMWyjAPFM3uXVMYpUw9YH4xPwFAT

Deploy success

浏览器能够看到相应的部署信息,说明我们的程序部署完成。

编写 Bank 合约:

bank 合约的功能比较简单:

  • 1、bank 合约目前只支持原生代币 sol 的存款和取款的功能;
  • 2、任何人都可以向程序中存入不小于0.01个sol代币;
  • 3、用户任何时候可以从中提取自己的资金;
  • 4、程序中需要维护每个人的在bank中的余额,可以供外部查看每个人资金的情况;
  • 5、需要维护bank智能合约中总的余额;
  • 6、合约的部署人员也没有权限动用别人的资金;

好了,上面就是我们合约的全部需求了。我这里贴上提前写好的程序:


use anchor_lang::prelude::*;
use anchor_lang::solana_program::system_instruction;

declare_id!("98Frhx1UD56goz5ns2vTR6WaqHLPsJE7Jux8iWgJr3Fv");

#[program]
pub mod solana_bank_demo {
    use super::*;

    pub fn initialize(ctx: Context&lt;Initialize>) -> Result&lt;()> {
        msg!("Initializing bank contract");
        let bank = &mut ctx.accounts.bank;
        bank.owner = ctx.accounts.owner.key();
        bank.total_balance = 0;
        Ok(())
    }

    pub fn deposit(ctx: Context&lt;Deposit>, amount: u64) -> Result&lt;()> {
        const MIN_DEPOSIT: u64 = 10_000_000; // 0.01 SOL
        msg!(
            "Processing deposit of {} lamports from user: {}",
            amount,
            ctx.accounts.user.key()
        );
        require!(amount >= MIN_DEPOSIT, BankError::DepositTooSmall);

        let transfer_instruction = system_instruction::transfer(
            &ctx.accounts.user.key(),
            &ctx.accounts.bank.key(),
            amount,
        );

        anchor_lang::solana_program::program::invoke(
            &transfer_instruction,
            &[
                ctx.accounts.user.to_account_info(),
                ctx.accounts.bank.to_account_info(),
                ctx.accounts.system_program.to_account_info(),
            ],
        )?;

        let user_account = &mut ctx.accounts.user_account;
        let old_balance = user_account.balance;
        user_account.balance = user_account.balance.checked_add(amount).unwrap();

        let bank =...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Louis
Louis
web3 developer,技术交流或者有工作机会可加VX: magicalLouis