不使用 Anchor 开发 solana program

初始化工程

使用 Cargo 初始化工程

我们可以使用 cargo 来初始化工程。

cargo init hello_world --lib

编写代码

程序入口 entrypoint

下面利用 entrypoint 来编写程序入口。

entrypoint macro 需要一个函数参数,作为 solana program 的入口函数。

#![allow(unused)]
fn main() {
pub fn process_instruction() -> ProgramResult {
    msg!("Hello, world!");
    Ok(())
}
}

如果传递给 entrypoint macro 的函数签名不符合要求,编译时会报错:

    Checking hello_world v0.1.0 (/Users/dylan/Code/solana/projects/hello_world)
error[E0061]: this function takes 0 arguments but 3 arguments were supplied
 --> src/lib.rs:6:1
  |
6 | entrypoint!(process_instruction);
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  | |
  | unexpected argument #1 of type `&Pubkey`
  | unexpected argument #2 of type `&Vec<AccountInfo<'_>>`
  | unexpected argument #3 of type `&[u8]`
  |
note: function defined here
 --> src/lib.rs:8:8
  |
8 | pub fn process_instruction() -> ProgramResult {
  |        ^^^^^^^^^^^^^^^^^^^
  = note: this error originates in the macro `entrypoint` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0061`.
error: could not compile `hello_world` (lib) due to 1 previous error

修改 process_instruction 函数的签名

process_instruction 函数添加三个参数:

  • program_id: &Pubkey 类型,表示当前程序的公钥地址
  • accounts: &[AccountInfo] 类型,是一个 AccountInfo 数组的引用,包含了交易涉及的所有账户信息
  • instruction_data: &[u8] 类型,是指令的输入数据,以字节数组的形式传入

这三个参数是 Solana 程序执行时的基本要素:

  • program_id 用于验证程序身份和权限
  • accounts 包含了程序需要读取或修改的所有账户数据
  • instruction_data 携带了调用程序时传入的具体指令数据
#![allow(unused)]
fn main() {
pub fn process_instruction(
    _program_id: &Pubkey,
    _accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    msg!("Hello, world!");
    Ok(())
}
}

注意这里参数名前加了下划线前缀(_),是因为在这个简单的示例中我们暂时没有使用这些参数,这样可以避免编译器的未使用变量警告。在实际开发中,这些参数都是非常重要的,我们会在后续的示例中详细介绍如何使用它们。

关于函数签名,我们也可以参考 solana_program_entrypoint 这个 crate 的文档:

#![allow(unused)]
fn main() {
/// fn process_instruction(
///     program_id: &Pubkey,      // Public key of the account the program was loaded into
///     accounts: &[AccountInfo], // All accounts required to process the instruction
///     instruction_data: &[u8],  // Serialized instruction-specific data
/// ) -> ProgramResult;
}

构建程序

使用 cargo build-sbf 构建程序

为了构建 solana program,我们需要使用 cargo build-sbf 程序。

cargo build-sbf

构建失败了,以下是报错信息。

dylan@smalltown ~/Code/solana/projects/hello_world (master)> cargo build-sbf
error: package `solana-program v2.1.4` cannot be built because it requires rustc 1.79.0 or newer, while the currently active rustc version is 1.75.0-dev
Either upgrade to rustc 1.79.0 or newer, or use
cargo update solana-program@2.1.4 --precise ver
where `ver` is the latest version of `solana-program` supporting rustc 1.75.0-dev

我们可以通过 --version 参数来查看 rustc 的版本信息。

cargo-build-sbf --version

输出:

solana-cargo-build-sbf 1.18.25
platform-tools v1.41
rustc 1.75.0

关于系统版本的 rust compiler 和 build-sbf 使用的 rust compiler 不对应的问题,可以参考这个 issue。 https://github.com/solana-labs/solana/issues/34987

解决 build-sbf 编译失败问题

一种方式是使用旧版本的 solana-program,如 =1.17.0 版本。

[package]
name = "hello_world"
version = "0.1.0"
edition = "2021"

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

[dependencies]
solana-program = "=1.17.0"
# solana-program = "=1.18.0"

但是运行 cargo build-sbf 之后,出现了另外的错误。

error: failed to parse lock file at: /Users/dylan/Code/solana/projects/hello_world/Cargo.lock

Caused by:
  lock file version 4 requires `-Znext-lockfile-bump`

猜测可能是 build-sbf 使用的 cargo 版本不支持 version = 4 版本的 Cargo.lock 文件,而这个是编辑器(vscode/cursor)打开的状态下,rust-analyser 自动生成的。

安装 stable 版本的 solana cli 工具链: sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)",发现还是无法编译,报错如下:

dylan@smalltown ~/Code/solana/projects/hello_world (master)> sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
downloading stable installer
  ✨ stable commit 7104d71 initialized
dylan@smalltown ~/Code/solana/projects/hello_world (master)> cargo build-sbf --version
solana-cargo-build-sbf 2.0.17
platform-tools v1.42

dylan@smalltown ~/Code/solana/projects/hello_world (master)> cargo build-sbf
[2024-12-04T11:14:48.052020000Z ERROR cargo_build_sbf] Failed to install platform-tools: HTTP status client error (404 Not Found) for url (https://github.com/anza-xyz/platform-tools/releases/download/v1.42/platform-tools-osx-x86_64.tar.bz2)

在进行 cargo build-sbf 编译的时候,需要下载对应版本的 platform-tools,因为未发布针对 Mac(Intel) 的 v1.42 版本 的 platform-tools,所以上述命令运行失败。

dylan@smalltown ~/Code/solana/projects/hello_world (master)> cargo build-sbf
   Compiling cc v1.2.2
   Compiling serde v1.0.215
   Compiling solana-frozen-abi-macro v1.17.0
   Compiling ahash v0.7.8
   Compiling solana-frozen-abi v1.17.0
   Compiling either v1.13.0
   Compiling bs58 v0.4.0
   Compiling log v0.4.22
   Compiling hashbrown v0.11.2
   Compiling itertools v0.10.5
   Compiling solana-sdk-macro v1.17.0
   Compiling bytemuck v1.20.0
   Compiling borsh v0.9.3
   Compiling num-derive v0.3.3
   Compiling blake3 v1.5.5
   Compiling solana-program v1.17.0
   Compiling bv v0.11.1
   Compiling serde_json v1.0.133
   Compiling serde_bytes v0.11.15
   Compiling bincode v1.3.3
Error: Function _ZN112_$LT$solana_program..instruction..InstructionError$u20$as$u20$solana_frozen_abi..abi_example..AbiEnumVisitor$GT$13visit_for_abi17hc69c00f4c61717f8E Stack offset of 6640 exceeded max offset of 4096 by 2544 bytes, please minimize large stack variables. Estimated function frame size: 6680 bytes. Exceeding the maximum stack offset may cause undefined behavior during execution.

   Compiling hello_world v0.1.0 (/Users/dylan/Code/solana/projects/hello_world)
    Finished `release` profile [optimized] target(s) in 25.19s
+ ./platform-tools/rust/bin/rustc --version
+ ./platform-tools/rust/bin/rustc --print sysroot
+ set +e
+ rustup toolchain uninstall solana
info: uninstalling toolchain 'solana'
info: toolchain 'solana' uninstalled
+ set -e
+ rustup toolchain link solana platform-tools/rust
+ exit 0
⏎

dylan@smalltown ~/Code/solana/projects/hello_world (master)> ls target/deploy/
hello_world-keypair.json  hello_world.so
dylan@smalltown ~/Code/solana/projects/hello_world (master)> cargo build-sbf --version
solana-cargo-build-sbf 2.1.4
platform-tools v1.43
rustc 1.79.0

dylan@smalltown ~/Code/solana/projects/hello_world (master) [1]> sh -c "$(curl -sSfL https://release.anza.xyz/beta/install)"
downloading beta installer
  ✨ beta commit 024d047 initialized
dylan@smalltown ~/Code/solana/projects/hello_world (master)> cargo build-sbf --version
solana-cargo-build-sbf 2.1.4
platform-tools v1.43
rustc 1.79.0
dylan@smalltown ~/Code/solana/projects/hello_world (master)> cargo build-sbf
Error: Function _ZN112_$LT$solana_program..instruction..InstructionError$u20$as$u20$solana_frozen_abi..abi_example..AbiEnumVisitor$GT$13visit_for_abi17hc69c00f4c61717f8E Stack offset of 6640 exceeded max offset of 4096 by 2544 bytes, please minimize large stack variables. Estimated function frame size: 6680 bytes. Exceeding the maximum stack offset may cause undefined behavior during execution.

    Finished `release` profile [optimized] target(s) in 0.23s

使用 beta 版本的 solana cli tool suites 虽然能够编译,但是遇到了这个错误:

Exceeding the maximum stack offset may cause undefined behavior during execution.

   Compiling bincode v1.3.3
Error: Function _ZN112_$LT$solana_program..instruction..InstructionError$u20$as$u20$solana_frozen_abi..abi_example..AbiEnumVisitor$GT$13visit_for_abi17hc69c00f4c61717f8E Stack offset of 6640 exceeded max offset of 4096 by 2544 bytes, please minimize large stack variables. Estimated function frame size: 6680 bytes. Exceeding the maximum stack offset may cause undefined behavior during execution.

具体原因依旧是老生常谈的版本问题,原因分析可以参考: https://solana.stackexchange.com/questions/16443/error-function-stack-offset-of-7256-exceeded-max-offset-of-4096-by-3160-bytes

尝试更新 solana-program 的版本到 2.1.4 之后(运行 sh -c "$(curl -sSfL https://release.anza.xyz/v2.1.4/install)"),用以下版本的工具链进行编译:

> cargo build-sbf --version
solana-cargo-build-sbf 2.1.4
platform-tools v1.43
rustc 1.79.0

# solana-cargo-build-sbf 2.2.0
# platform-tools v1.43
# rustc 1.79.0

运行 cargo build-sbf:

> cargo build-sbf
   Compiling serde v1.0.215
   Compiling equivalent v1.0.1
   Compiling hashbrown v0.15.2
   Compiling toml_datetime v0.6.8
   Compiling syn v2.0.90
   Compiling winnow v0.6.20
   Compiling cfg_aliases v0.2.1
   Compiling once_cell v1.20.2
   Compiling borsh v1.5.3
   Compiling solana-define-syscall v2.1.4
   Compiling solana-sanitize v2.1.4
   Compiling solana-atomic-u64 v2.1.4
   Compiling bs58 v0.5.1
   Compiling bytemuck v1.20.0
   Compiling five8_core v0.1.1
   Compiling five8_const v0.1.3
   Compiling solana-decode-error v2.1.4
   Compiling solana-msg v2.1.4
   Compiling cc v1.2.2
   Compiling solana-program-memory v2.1.4
   Compiling log v0.4.22
   Compiling solana-native-token v2.1.4
   Compiling solana-program-option v2.1.4
   Compiling indexmap v2.7.0
   Compiling blake3 v1.5.5
   Compiling toml_edit v0.22.22
   Compiling serde_derive v1.0.215
   Compiling bytemuck_derive v1.8.0
   Compiling solana-sdk-macro v2.1.4
   Compiling thiserror-impl v1.0.69
   Compiling num-derive v0.4.2
   Compiling proc-macro-crate v3.2.0
   Compiling borsh-derive v1.5.3
   Compiling thiserror v1.0.69
   Compiling solana-secp256k1-recover v2.1.4
   Compiling solana-borsh v2.1.4
   Compiling solana-hash v2.1.4
   Compiling bincode v1.3.3
   Compiling bv v0.11.1
   Compiling solana-serde-varint v2.1.4
   Compiling serde_bytes v0.11.15
   Compiling solana-fee-calculator v2.1.4
   Compiling solana-short-vec v2.1.4
   Compiling solana-sha256-hasher v2.1.4
   Compiling solana-pubkey v2.1.4
   Compiling solana-instruction v2.1.4
   Compiling solana-sysvar-id v2.1.4
   Compiling solana-slot-hashes v2.1.4
   Compiling solana-clock v2.1.4
   Compiling solana-epoch-schedule v2.1.4
   Compiling solana-last-restart-slot v2.1.4
   Compiling solana-rent v2.1.4
   Compiling solana-program-error v2.1.4
   Compiling solana-stable-layout v2.1.4
   Compiling solana-serialize-utils v2.1.4
   Compiling solana-account-info v2.1.4
   Compiling solana-program-pack v2.1.4
   Compiling solana-bincode v2.1.4
   Compiling solana-slot-history v2.1.4
   Compiling solana-program-entrypoint v2.1.4
   Compiling solana-cpi v2.1.4
   Compiling solana-program v2.1.4
   Compiling hello_world v0.1.0 (/Users/dylan/Code/solana/projects/hello_world)
    Finished `release` profile [optimized] target(s) in 50.87s

总算编译成功了,开瓶香槟庆祝一下吧!

这里是 Cargo.toml 文件:

[package]
name = "hello_world"
version = "0.1.0"
edition = "2021"

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

[dependencies]
solana-program = "2.1.4"
# solana-program = "=1.17.0"

构建产物

cargo build-sbf 是 Solana 提供的一个特殊的构建命令,用于将 Rust 程序编译成可以在 Solana 运行时环境中执行的 BPF (Berkeley Packet Filter) 字节码。这个命令做了以下几件事:

  1. 使用特定的 Rust 工具链编译代码

    • 使用针对 Solana 优化的 Rust 编译器
    • 使用 bpfel-unknown-unknown 目标平台
    • 启用发布模式优化
  2. 生成必要的部署文件

    • 编译出 .so 文件(共享对象文件)
    • 生成程序密钥对(如果不存在)
    • 优化和压缩最终的二进制文件
  3. 验证编译结果

    • 检查程序大小是否在限制范围内
    • 验证程序格式是否正确

命令执行流程:

  1. 首先检查并下载必要的工具链
  2. 使用 cargo 编译项目
  3. 对编译产物进行后处理(如剥离调试信息)
  4. 将最终文件放置在 target/deploy 目录

这个命令替代了早期的 cargo build-bpf,提供了更好的构建体验和更现代的工具链支持。

我们来看看具体生成了哪些文件,运行 cargo build-sbf 这个命令之后会在 target/deploy 目录下生成两个重要文件:

  • hello_world.so:编译后的程序文件,这是一个 BPF (Berkeley Packet Filter) 格式的可执行文件
  • hello_world-keypair.json:程序的密钥对文件,用于程序的部署和升级

如果你看到类似下面的输出,说明构建成功:

BPF SDK: /Users/username/.local/share/solana/install/releases/1.14.x/solana-release/bin/sdk/bpf
cargo-build-sbf child: rustup toolchain list -v
cargo-build-sbf child: cargo +bpf build --target bpfel-unknown-unknown --release
    Finished release [optimized] target(s) in 0.20s
cargo-build-sbf child: /Users/username/.local/share/solana/install/releases/1.14.x/solana-release/bin/sdk/bpf/scripts/strip.sh /Users/username/projects/hello_world/target/bpfel-unknown-unknown/release/hello_world.so /Users/username/projects/hello_world/target/deploy/hello_world.so

部署

现在我们可以将编译好的程序部署到 Solana 网络上。在开发阶段,我们通常使用本地测试网(localhost)或开发网(devnet)进行测试。

首先确保你的 Solana CLI 配置指向了正确的集群:

# 切换到开发网
solana config set --url devnet
# 切换到本地测试网
solana config set --url localnet

# 查看当前配置
solana config get

然后使用以下命令部署程序:

solana program deploy target/deploy/hello_world.so

部署成功后,你会看到程序的 ID(公钥地址)。请保存这个地址,因为在后续与程序交互时会需要它。

但是,当我们通过运行 solana program deploy 命令来部署程序的时候,部署失败了。

dylan@smalltown ~/Code/solana/projects/helloworld (master)> solana program deploy ./target/deploy/helloworld.so
⠁   0.0% | Sending 1/173 transactions               [block height 2957; re-sign in 150 blocks]
    thread 'main' panicked at quic-client/src/nonblocking/quic_client.rs:142:14:
QuicLazyInitializedEndpoint::create_endpoint bind_in_range: Os { code: 55, kind: Uncategorized, message: "No buffer space available" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

那么这个 No buffer space available 是什么意思呢?

排查了很久终于无果,凭借多年的经验,大概率应该是 版本 的问题,因为通过 Anchor 创建的工程是能够正常部署的。

这里记录一下 solana 命令的版本信息:

> solana --version
solana-cli 2.2.0 (src:67704836; feat:1081947060, client:Agave)

回到 Anchor 工程验证部署失败源自版本的问题

我们可以通过 anchor init helloworld 新建工程,并通过 anchor buildanchor deploy 来部署程序。

anchor init helloworld
cd helloworld
anchor build
anchor deploy

从出错信息了解到,全新生成的 anchor 工程部署的时候会发生同样的错误:No buffer space available

dylan@smalltown ~/tmp/helloworld (main)> anchor deploy
Deploying cluster: https://api.devnet.solana.com
Upgrade authority: /Users/dylan/.config/solana/id.json
Deploying program "helloworld"...
Program path: /Users/dylan/tmp/helloworld/target/deploy/helloworld.so...
⠁   0.0% | Sending 1/180 transactions               [block height 332937196; re-sign in 150 blocks]                                                       thread 'main' panicked at quic-client/src/nonblocking/quic_client.rs:142:14:
QuicLazyInitializedEndpoint::create_endpoint bind_in_range: Os { code: 55, kind: Uncategorized, message: "No buffer space available" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
There was a problem deploying: Output { status: ExitStatus(unix_wait_status(25856)), stdout: "", stderr: "" }.

检查下 anchor 的版本:

dylan@smalltown ~/tmp/helloworld (main)> anchor deploy --help
Deploys each program in the workspace

Usage: anchor-0.30.1 deploy [OPTIONS] [-- <SOLANA_ARGS>...]

Arguments:
  [SOLANA_ARGS]...  Arguments to pass to the underlying `solana program deploy` command

Options:
  -p, --program-name <PROGRAM_NAME>        Only deploy this program
      --provider.cluster <CLUSTER>         Cluster override
      --program-keypair <PROGRAM_KEYPAIR>  Keypair of the program (filepath) (requires program-name)
      --provider.wallet <WALLET>           Wallet override
  -v, --verifiable                         If true, deploy from path target/verifiable
  -h, --help                               Print help

检查下 solana 的版本:

> solana --version
solana-cli 2.2.0 (src:67704836; feat:1081947060, client:Agave)

这个 2.2.0 的版本看着有些奇怪,忽然想到为了编译 solana 程序,我安装了 edge 版本的 solana cli,其携带的 solana cli 的版本是 2.2.0:

sh -c "$(curl -sSfL https://release.anza.xyz/edge/install)"

于是换回了 stable 版本:

> sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
downloading stable installer
  ✨ stable commit fbead11 initialized

而 stable 版本的 solana 是 2.0.19

> solana --version
solana-cli 2.0.19 (src:fbead118; feat:607245837, client:Agave)

重新部署程序之前,我们先来清理下之前部署失败的程序的 buffers,也就是 buffer accounts。关于什么是 buffer accounts,请参考 Tips 3。

  • 查看所有的 buffer accounts: solana program show --buffers
  • 关闭所有的 buffer accounts: solana program close --buffers
    • 关闭 buffer accounts 可以回收存储在 buffer accounts 里的 SOL
Error: error sending request for url (https://api.devnet.solana.com/): operation timed out
dylan@smalltown ~/tmp/helloworld (main)> solana program show --buffers

Buffer Address                               | Authority                                    | Balance
CcKFVBzcsrcReZHBLnwzkQbNGXoK4hUee7hkgtbHCKtL | FCxBXdduz9HqTEPvEBSuFLLAjbVYh9a5ZgEnZwKyN2ZH | 0.12492504 SOL
62wFzMYBhxWg4ntEJmFZcQ3P3Qtm9SbaBcbTmV8o8yPk | FCxBXdduz9HqTEPvEBSuFLLAjbVYh9a5ZgEnZwKyN2ZH | 0.12492504 SOL
9q88jzvR5AdPdNTihxWroxRL7cBWQ5xXepNfDdaqmMTv | FCxBXdduz9HqTEPvEBSuFLLAjbVYh9a5ZgEnZwKyN2ZH | 1.26224472 SOL
3nqzHv9vUphsmAjoR1C5ShgZ54muTzkZZ6Z4NKfqrKqt | FCxBXdduz9HqTEPvEBSuFLLAjbVYh9a5ZgEnZwKyN2ZH | 1.26224472 SOL
8tZ8YYA1WS6WFVyEbJAdgnszXYZwwq7b9RLdoiry2Fb1 | FCxBXdduz9HqTEPvEBSuFLLAjbVYh9a5ZgEnZwKyN2ZH | 0.12492504 SOL

dylan@smalltown ~/tmp/helloworld (main)> solana program close --buffers

Buffer Address                               | Authority                                    | Balance
CcKFVBzcsrcReZHBLnwzkQbNGXoK4hUee7hkgtbHCKtL | FCxBXdduz9HqTEPvEBSuFLLAjbVYh9a5ZgEnZwKyN2ZH | 0.12492504 SOL
62wFzMYBhxWg4ntEJmFZcQ3P3Qtm9SbaBcbTmV8o8yPk | FCxBXdduz9HqTEPvEBSuFLLAjbVYh9a5ZgEnZwKyN2ZH | 0.12492504 SOL
9q88jzvR5AdPdNTihxWroxRL7cBWQ5xXepNfDdaqmMTv | FCxBXdduz9HqTEPvEBSuFLLAjbVYh9a5ZgEnZwKyN2ZH | 1.26224472 SOL
3nqzHv9vUphsmAjoR1C5ShgZ54muTzkZZ6Z4NKfqrKqt | FCxBXdduz9HqTEPvEBSuFLLAjbVYh9a5ZgEnZwKyN2ZH | 1.26224472 SOL
8tZ8YYA1WS6WFVyEbJAdgnszXYZwwq7b9RLdoiry2Fb1 | FCxBXdduz9HqTEPvEBSuFLLAjbVYh9a5ZgEnZwKyN2ZH | 0.12492504 SOL

好了 buffer accounts 清理完毕,此时我们也换回了 stable 版本的 solana cli,我们再尝试部署程序:

> anchor deploy
Deploying cluster: https://api.devnet.solana.com
Upgrade authority: /Users/dylan/.config/solana/id.json
Deploying program "helloworld"...
Program path: /Users/dylan/tmp/helloworld/target/deploy/helloworld.so...
Program Id: DiSGTiXGq4HXCxq1pAibuGZjSpKT4Av8WShvuuYhTks9

Signature: 2EXHmU68k9SmJ5mXuM61pFDnUgozbJZ5ihHChPqFMVgjRJy4zCqnq6NAbvDkfiHd29xsmW4Vr3Kk6wHFbLEdCEZb

Deploy success

成功了 🎉,再开一瓶香槟庆祝下吧!

这更加深了我们的猜测:版本问题导致程序无法部署。

再回来部署我们的 hello_world 工程

好了,验证了部署失败不是工程类型(anchor project or cargo projct)导致的原因之后,我们再回到 cargo init 创建的工程:hello_world.

我们可以通过 solana 的子命令来部署程序: 运行 solana program deploy ./target/deploy/helloworld.so 部署程序。

我们会分别在 localnetdevnet 部署。

localnet 部署

首先是 localnet 部署。

切换环境到 localnet:

dylan@smalltown ~/Code/solana/projects/hello_world (master)> solana_local
Config File: /Users/dylan/.config/solana/cli/config.yml
RPC URL: http://localhost:8899
WebSocket URL: ws://localhost:8900/ (computed)
Keypair Path: /Users/dylan/.config/solana/id.json
Commitment: confirmed
dylan@smalltown ~/Code/solana/projects/hello_world (master)> solana config get
Config File: /Users/dylan/.config/solana/cli/config.yml
RPC URL: http://localhost:8899
WebSocket URL: ws://localhost:8900/ (computed)
Keypair Path: /Users/dylan/.config/solana/id.json
Commitment: confirmed

部署程序:

dylan@smalltown ~/Code/solana/projects/hello_world (master)> solana program deploy ./target/deploy/hello_world.so
Program Id: DhQr1KGGQcf8BeU5uQvR35p2kgKqEinD45PRTDDRqx7z

Signature: 3WVEWN4NUodsb8ZDjbjrTWXLikZ7wbWCuzuRZtSBmyKL4kVvESSeLwKZ3cJo1At4vDcaBs5iEcHhdteyXCwqwmDw

devnet 部署

下面是 devnet 部署。

切换环境到 localnet:

dylan@smalltown ~/Code/solana/projects/hello_world (master)> solana_devnet
Config File: /Users/dylan/.config/solana/cli/config.yml
RPC URL: https://api.devnet.solana.com
WebSocket URL: wss://api.devnet.solana.com/ (computed)
Keypair Path: /Users/dylan/.config/solana/id.json
Commitment: confirmed

dylan@smalltown ~/Code/solana/projects/hello_world (master)> solana config get
Config File: /Users/dylan/.config/solana/cli/config.yml
RPC URL: https://api.devnet.solana.com
WebSocket URL: wss://api.devnet.solana.com/ (computed)
Keypair Path: /Users/dylan/.config/solana/id.json
Commitment: confirmed

dylan@smalltown ~/Code/solana/projects/hello_world (master)> solana program deploy ./target/deploy/hello_world.so
Program Id: DhQr1KGGQcf8BeU5uQvR35p2kgKqEinD45PRTDDRqx7z

Signature: 4P89gHNUNccQKJAsE3aXJVpFrWeqLxcmk9SYHbQCX7T1sEvyPrxcbrAeJbk8F8YKwWT79nTswSZkz7mtSb55nboF

我们可以通过 solana balance 来查询下部署前后的余额

# 部署之前余额
(base) dylan@smalltown ~/Code/solana/projects/hello_world (master)> solana balance
75.153619879 SOL

# 部署之后余额
(base) dylan@smalltown ~/Code/solana/projects/hello_world (master)> solana balance
75.152378439 SOL

而此时的版本:

dylan@smalltown ~/Code/solana/projects/helloworld (master)> solana --version
solana-cli 2.0.19 (src:fbead118; feat:607245837, client:Agave)

由此可见,不要尝鲜用最新的版本(solana-cli 2.2.0),否则会弄巧成拙。

Tips

Tip 1: solana cli 的版本和 Cargo.toml 里的版本保持一致

solana 的官方教程里提到这个 Tip:

It is highly recommended to keep your solana-program and other Solana Rust dependencies in-line with your installed version of the Solana CLI. For example, if you are running Solana CLI 2.0.3, you can instead run:

cargo add solana-program@"=2.0.3"

This will ensure your crate uses only 2.0.3 and nothing else. If you experience compatibility issues with Solana dependencies, check out the

Tip 2: 不要在 dependencies 里添加 solana-sdk,因为这是 offchain 使用的

参考这里的说明: https://solana.stackexchange.com/questions/9109/cargo-build-bpf-failed

I have identified the issue. The solana-sdk is designed for off-chain use only, so it should be removed from the dependencies.

错误将 solana-sdk 添加到 dependencies 报错:

   Compiling autocfg v1.4.0
   Compiling jobserver v0.1.32
error: target is not supported, for more information see: https://docs.rs/getrandom/#unsupported-targets
   --> src/lib.rs:267:9
    |
267 | /         compile_error!("\
268 | |             target is not supported, for more information see: \
269 | |             https://docs.rs/getrandom/#unsupported-targets\
270 | |         ");
    | |__________^

error[E0433]: failed to resolve: use of undeclared crate or module `imp`
   --> src/lib.rs:291:5
    |
291 |     imp::getrandom_inner(dest)
    |     ^^^ use of undeclared crate or module `imp`

For more information about this error, try `rustc --explain E0433`.
error: could not compile `getrandom` (lib) due to 2 previous errors
warning: build failed, waiting for other jobs to finish...

Tip 3: 关于 buffer accounts

在 Solana 中,buffer accounts 是用于程序部署过程中的一种临时账户,它是 Solana 部署程序时的一个重要机制。由于 Solana 的交易大小限制为 1232 字节,部署程序时通常需要多个交易步骤。在这个过程中,buffer account 的作用是存储程序的字节码,直到部署完成。

buffer account 的关键点:

  • 临时存储:buffer account 用于存放程序的字节码,确保在部署过程中能够处理较大的程序。
  • 自动关闭:一旦程序成功部署,相关的 buffer account 会自动关闭,从而释放占用的资源。
  • 失败处理:如果部署失败,buffer account 不会自动删除,用户可以选择:
    • 继续使用现有的 buffer account 来完成部署。
    • 关闭 buffer account,以便回收已分配的 SOL(租金)。
  • 检查 buffer accounts:可以通过命令 solana program show --buffers 来检查当前是否存在未关闭的 buffer accounts。
  • 关闭 buffer accounts:可以通过命令 solana program close --buffers 来关闭 buffer accounts。

关于 solana 程序部署的过程的解释,可以查考官方文档: https://solana.com/docs/programs/deploying#program-deployment-process

重新部署

重新部署只需要编辑代码之后运行 cargo build-sbf 编译代码,再通过 solana program deply ./target/deploy/hello_world.so 部署即可。

cargo build-sbf
solana program deploy ./target/deploy/hello_world.so

可以通过运行测试和 client 脚本来验证运行的是新版本的 program。

# 运行测试
cargo test-sbf
# 运行 client 脚本
cargo run --example client

比如,我修改 msg! 输入内容为 Hello, world! GM!GN!,运行测试和 client 脚本能够看到 log 里有这个输出。

#![allow(unused)]
fn main() {
pub fn process_instruction(
    _program_id: &Pubkey,
    _accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    msg!("Hello, world! GM!GN!");
    Ok(())
}
}

运行测试:

(base) dylan@smalltown ~/Code/solana/projects/hello_world (master)> cargo test-sbf
   Compiling hello_world v0.1.0 (/Users/dylan/Code/solana/projects/hello_world)
    Finished release [optimized] target(s) in 1.76s
   Compiling hello_world v0.1.0 (/Users/dylan/Code/solana/projects/hello_world)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 13.92s
     Running unittests src/lib.rs (target/debug/deps/hello_world-ee1a919556768e26)

running 1 test
[2024-12-06T08:06:57.714248000Z INFO  solana_program_test] "hello_world" SBF program from /Users/dylan/Code/solana/projects/hello_world/target/deploy/hello_world.so, modified 19 seconds, 228 ms, 255 µs and 392 ns ago
[2024-12-06T08:06:57.947344000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1]
[2024-12-06T08:06:57.947695000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Hello, world! GM!GN!
[2024-12-06T08:06:57.947738000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 140 of 200000 compute units
[2024-12-06T08:06:57.947897000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM success
test test::test_hello_world ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.24s

   Doc-tests hello_world

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

TODO: image

最佳实践

安装 solana-cli 的最佳实践

最好的方式是安装指定版本的 solana cli,如可以用以下方式安装 2.0.3 的版本:

# 安装 stable 和 beta 都不推荐
# sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"
# sh -c "$(curl -sSfL https://release.anza.xyz/beta/install)"
# 推荐安装指定版本
sh -c "$(curl -sSfL https://release.anza.xyz/v2.0.3/install)"

输出:

downloading v2.0.3 installer
  ✨ 2.0.3 initialized

运行 cargo build-sbf --version 查看下 cargo build-sbf 的版本:

(base) dylan@smalltown ~/Code/solana/projects/hello_world (master) [1]> cargo build-sbf --version
solana-cargo-build-sbf 2.0.3
platform-tools v1.41
rustc 1.75.0

可以看到,这里的 rustc 版本是 1.75.0,比较老旧,编译的时候必须带上 -Znext-lockfile-bump 参数,否则编译出错:

(base) dylan@smalltown ~/Code/solana/projects/hello_world (master)> cargo build-sbf
info: uninstalling toolchain 'solana'
info: toolchain 'solana' uninstalled
error: failed to parse lock file at: /Users/dylan/Code/solana/projects/hello_world/Cargo.lock

Caused by:
  lock file version 4 requires `-Znext-lockfile-bump`

以下是传递 -Znext-lockfile-bump 参数之后,完整的编译过程:

(base) dylan@smalltown ~/Code/solana/projects/hello_world (master)> cargo build-sbf -- -Znext-lockfile-bump
   Compiling proc-macro2 v1.0.92
   Compiling unicode-ident v1.0.14
   Compiling version_check v0.9.5
   Compiling typenum v1.17.0
   Compiling autocfg v1.4.0
   Compiling serde v1.0.215
   Compiling syn v1.0.109
   Compiling cfg-if v1.0.0
   Compiling equivalent v1.0.1
   Compiling hashbrown v0.15.2
   Compiling semver v1.0.23
   Compiling generic-array v0.14.7
   Compiling ahash v0.8.11
   Compiling winnow v0.6.20
   Compiling indexmap v2.7.0
   Compiling toml_datetime v0.6.8
   Compiling shlex v1.3.0
   Compiling quote v1.0.37
   Compiling subtle v2.6.1
   Compiling cc v1.2.2
   Compiling syn v2.0.90
   Compiling once_cell v1.20.2
   Compiling rustversion v1.0.18
   Compiling feature-probe v0.1.1
   Compiling zerocopy v0.7.35
   Compiling cfg_aliases v0.2.1
   Compiling borsh v1.5.3
   Compiling bv v0.11.1
   Compiling rustc_version v0.4.1
   Compiling num-traits v0.2.19
   Compiling memoffset v0.9.1
   Compiling thiserror v1.0.69
   Compiling toml_edit v0.22.22
   Compiling blake3 v1.5.5
   Compiling block-buffer v0.10.4
   Compiling crypto-common v0.1.6
   Compiling solana-program v2.0.3
   Compiling digest v0.10.7
   Compiling hashbrown v0.13.2
   Compiling constant_time_eq v0.3.1
   Compiling bs58 v0.5.1
   Compiling arrayvec v0.7.6
   Compiling arrayref v0.3.9
   Compiling keccak v0.1.5
   Compiling sha2 v0.10.8
   Compiling toml v0.5.11
   Compiling sha3 v0.10.8
   Compiling proc-macro-crate v3.2.0
   Compiling borsh-derive-internal v0.10.4
   Compiling borsh-schema-derive-internal v0.10.4
   Compiling getrandom v0.2.15
   Compiling lazy_static v1.5.0
   Compiling bytemuck v1.20.0
   Compiling log v0.4.22
   Compiling proc-macro-crate v0.1.5
   Compiling serde_derive v1.0.215
   Compiling thiserror-impl v1.0.69
   Compiling num-derive v0.4.2
   Compiling solana-sdk-macro v2.0.3
   Compiling bytemuck_derive v1.8.0
   Compiling borsh-derive v1.5.3
   Compiling borsh-derive v0.10.4
   Compiling borsh v0.10.4
   Compiling serde_bytes v0.11.15
   Compiling bincode v1.3.3
   Compiling hello_world v0.1.0 (/Users/dylan/Code/solana/projects/hello_world)
    Finished release [optimized] target(s) in 2m 28s
+ ./platform-tools/rust/bin/rustc --version
+ ./platform-tools/rust/bin/rustc --print sysroot
+ set +e
+ rustup toolchain uninstall solana
info: uninstalling toolchain 'solana'
info: toolchain 'solana' uninstalled
+ set -e
+ rustup toolchain link solana platform-tools/rust
+ exit 0

值得注意的是,无论是安装 stable 版本还是 beta 版本都会导致编译失败,stable 版本运行 cargo build-sbf 会去 github release 页面下载针对 x86_64 架构的 platform-tools,但是官方没有发布提供针对这个版本的 platform-tools。以下是出错信息:

(base) dylan@smalltown ~/Code/solana/projects/hello_world (master) [1]> cargo build-sbf --version
solana-cargo-build-sbf 2.0.19
platform-tools v1.42

(base) dylan@smalltown ~/Code/solana/projects/hello_world (master) [1]> cargo build-sbf
[2024-12-05T06:17:30.547088000Z ERROR cargo_build_sbf] Failed to install platform-tools: HTTP status client error (404 Not Found) for url (https://github.com/anza-xyz/platform-tools/releases/download/v1.42/platform-tools-osx-x86_64.tar.bz2)

发现如果指定 --tools-versionv1.43 也不能成功编译。

(base) dylan@smalltown ~/Code/solana/projects/hello_world (master) [1]> cargo build-sbf --tools-version v1.43
    Blocking waiting for file lock on package cache
    Blocking waiting for file lock on package cache
   Compiling blake3 v1.5.5
   Compiling solana-program v2.0.3
   Compiling bs58 v0.5.1
   Compiling solana-sdk-macro v2.0.3
   Compiling hello_world v0.1.0 (/Users/dylan/Code/solana/projects/hello_world)
    Finished `release` profile [optimized] target(s) in 1m 16s
+ curl -L https://github.com/anza-xyz/platform-tools/releases/download/v1.42/platform-tools-osx-x86_64.tar.bz2 -o platform-tools-osx-x86_64.tar.bz2
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100     9  100     9    0     0     16      0 --:--:-- --:--:-- --:--:--    16
+ tar --strip-components 1 -jxf platform-tools-osx-x86_64.tar.bz2
tar: Error opening archive: Unrecognized archive format
+ return 1
+ popd
+ return 1
/Users/dylan/.local/share/solana/install/releases/stable-fbead118867c08e6c3baaf8d196897c2536f067a/solana-release/bin/sdk/sbf/scripts/strip.sh: line 23: /Users/dylan/.local/share/solana/install/releases/stable-fbead118867c08e6c3baaf8d196897c2536f067a/solana-release/bin/sdk/sbf/dependencies/platform-tools/llvm/bin/llvm-objcopy: No such file or directory

所以还是老老实实安装指定版本的 solana cli 吧。

如何查看部署的 program

我们可以通过访问以下地址来查看部署的 program。

https://explorer.solana.com/?cluster=custom

它会自动用本地的 localhost:8899 作为 rpc endpoint,在搜索栏搜索 program id,即可看到 transaction 详情。

客户端调用

客户调用程序 (Rust) (invoke solana program)

首先创建 examples 目录,并在 examples 目录下创建 client.rs 文件。

mkdir -p examples
touch examples/client.rs

Cargo.toml 增加以下内容:

[[example]]
name = "client"
path = "examples/client.rs"

添加 solana-client 依赖:

cargo add solana-client@1.18.26 --dev

添加以下代码到 examples/client.rs,注意替换你自己部署的 program ID:

use solana_client::rpc_client::RpcClient;
use solana_sdk::{
    commitment_config::CommitmentConfig,
    instruction::Instruction,
    pubkey::Pubkey,
    signature::{Keypair, Signer},
    transaction::Transaction,
};
use std::str::FromStr;

#[tokio::main]
async fn main() {
    // Program ID (replace with your actual program ID)
    let program_id = Pubkey::from_str("85K3baeo8tvZBmuty2UP8mMVd1vZtxLkmeUkj1s6tnT6").unwrap();

    // Connect to the Solana devnet
    let rpc_url = String::from("http://127.0.0.1:8899");
    let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());

    // Generate a new keypair for the payer
    let payer = Keypair::new();

    // Request airdrop
    let airdrop_amount = 1_000_000_000; // 1 SOL
    let signature = client
        .request_airdrop(&payer.pubkey(), airdrop_amount)
        .expect("Failed to request airdrop");

    // Wait for airdrop confirmation
    loop {
        let confirmed = client.confirm_transaction(&signature).unwrap();
        if confirmed {
            break;
        }
    }

    // Create the instruction
    let instruction = Instruction::new_with_borsh(
        program_id,
        &(),    // Empty instruction data
        vec![], // No accounts needed
    );

    // Add the instruction to new transaction
    let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
    transaction.sign(&[&payer], client.get_latest_blockhash().unwrap());

    // Send and confirm the transaction
    match client.send_and_confirm_transaction(&transaction) {
        Ok(signature) => println!("Transaction Signature: {}", signature),
        Err(err) => eprintln!("Error sending transaction: {}", err),
    }
}

这个简单的脚本能够调用已部署的 solana program,它主要做了以下几件事:

  • 连接本地 RPC
  • 创建新账户
  • 空投 1 SOL 给新开的账户
  • 创建 hello_world program 所需的指令(Instruction)
  • 发送交易 (通过 send_and_confirm_transaction

关于 program ID,我们可以通过 solana address -k <program keypair>.json 命令来获取 program ID:

solana address -k ./target/deploy/hello_world-keypair.json

-k 参数接收 keypair 的文件,可以获得 PublicKey。

运行 client:

cargo run --example client

运行 client 代码的输出:

(base) dylan@smalltown ~/Code/solana/projects/hello_world (master)> cargo run --example client
    Blocking waiting for file lock on package cache
    Blocking waiting for file lock on package cache
    Blocking waiting for file lock on package cache
   Compiling hello_world v0.1.0 (/Users/dylan/Code/solana/projects/hello_world)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.13s
     Running `target/debug/examples/client`
Transaction Signature: iPcYzbBCM6kkXvdx5GQLS9WYunT6yWFAp8NeRyNH5ZHbjXNpGuT1pqLAmQZSa2g7mubuFmaCTxqPVS54J4Zz22h

客户端调用(TypeScript)

我们可以通过建立 nodejs 工程来发送交易:

mkdir -p helloworld
npm init -y
npm install --save-dev typescript
npm install @solana/web3.js@1 @solana-developers/helpers@2

建立 tsconfig.json 配置文件:

{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "types": ["node"],
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

创建 hello-world-client.ts 文件,注意修改 PublicKey 的参数为你部署时生成的 programID:

import {
  Connection,
  PublicKey,
  Transaction,
  TransactionInstruction,
} from "@solana/web3.js";
import { getKeypairFromFile } from "@solana-developers/helpers";

async function main() {
  const programId = new PublicKey(
    "DhQr1KGGQcf8BeU5uQvR35p2kgKqEinD45PRTDDRqx7z"
  );

  // Connect to a solana cluster. Either to your local test validator or to devnet
  const connection = new Connection("http://localhost:8899", "confirmed");
  //const connection = new Connection("https://api.devnet.solana.com", "confirmed");

  // We load the keypair that we created in a previous step
  const keyPair = await getKeypairFromFile("~/.config/solana/id.json");

  // Every transaction requires a blockhash
  const blockhashInfo = await connection.getLatestBlockhash();

  // Create a new transaction
  const tx = new Transaction({
    ...blockhashInfo,
  });

  // Add our Hello World instruction
  tx.add(
    new TransactionInstruction({
      programId: programId,
      keys: [],
      data: Buffer.from([]),
    })
  );

  // Sign the transaction with your previously created keypair
  tx.sign(keyPair);

  // Send the transaction to the Solana network
  const txHash = await connection.sendRawTransaction(tx.serialize());

  console.log("Transaction sent with hash:", txHash);

  await connection.confirmTransaction({
    blockhash: blockhashInfo.blockhash,
    lastValidBlockHeight: blockhashInfo.lastValidBlockHeight,
    signature: txHash,
  });

  console.log(
    `Congratulations! Look at your ‘Hello World' transaction in the Solana Explorer:
  https://explorer.solana.com/tx/${txHash}?cluster=custom`
  );
}

main();

运行:

npx ts-node hello-world-client.ts

输出:

(base) dylan@smalltown ~/Code/solana/projects/solana-web3-example (master)> npx ts-node hello-world-client.ts
(node:4408) ExperimentalWarning: CommonJS module /usr/local/lib/node_modules/npm/node_modules/debug/src/node.js is loading ES Module /usr/local/lib/node_modules/npm/node_modules/supports-color/index.js using require().
Support for loading ES Module in require() is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:4467) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
Transaction sent with hash: 29aFYDNv1cyrByA8FTBxrhohJx3H1FVLSUordaA1RVcXSNSy7zN5mGW5rwj6pDuopMvvoBaKNHeKmQ8c17uVnqoN
Congratulations! Look at your ‘Hello World' transaction in the Solana Explorer:
  https://explorer.solana.com/tx/29aFYDNv1cyrByA8FTBxrhohJx3H1FVLSUordaA1RVcXSNSy7zN5mGW5rwj6pDuopMvvoBaKNHeKmQ8c17uVnqoN?cluster=custom

一些实验

哪些版本能成功编译和测试

首先看一下我们安装的 build-sbftest-sbf 的版本:

# build-sbf 版本
> cargo build-sbf --version
solana-cargo-build-sbf 2.1.4
platform-tools v1.43
rustc 1.79.0

# test-sbf 版本
> cargo test-sbf --version
solana-cargo-test-sbf 2.1.4

我们通过这个命令来测试哪些版本能够正确编译和测试: rm -rf target Cargo.lock && cargo build-sbf && cargo test-sbf

versionDevDependencies & DependenciesNOTE
✅2.1.4cargo add solana-sdk@2.1.4 solana-program-test@2.1.4 tokio --dev && cargo add solana-program@2.1.4latest version
✅2.0.18cargo add solana-sdk@2.0.18 solana-program-test@2.0.18 tokio --dev && cargo add solana-program@2.0.18latest version
✅2.0.3cargo add solana-sdk@2.0.3 solana-program-test@2.0.3 tokio --dev && cargo add solana-program@2.0.3
✅1.18.26cargo add solana-sdk@1.18.26 solana-program-test@1.18.26 tokio --dev && cargo add solana-program@1.18.26

这里是 Cargo.toml 的例子(对应版本是 2.0.3):

[package]
name = "hello_world"
version = "0.1.0"
edition = "2021"

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

[dependencies]
solana-program = "2.0.3"

[dev-dependencies]
solana-program-test = "2.0.3"
solana-sdk = "2.0.3"
tokio = "1.42.0"

测试

关于 solana 程序的测试,我们一般采用

bankrun 是一个用于在 Node.js 中测试 Solana 程序的轻量级框架。与传统的 solana-test-validator 相比,bankrun 提供了更高的速度和便利性。它能够实现一些 solana-test-validator 无法做到的功能,例如时间回溯和动态设置账户数据。

它会启动一个轻量级的 BanksServer,这个服务类似于一个 RPC 节点,但速度更快,并且创建一个 BanksClient 来与服务器进行通信

主要特点:

  • 高效性:比 solana-test-validator 快得多。
  • 灵活性:支持时间回溯和动态账户数据设置。
  • solana-bankrun 底层基于 solana-program-test,使用轻量级的 BanksServer 和 BanksClient。

接下来,我们来看看如何用 Rust(solana-program-test) 和 NodeJS(solana-bankrun) 编写测试用例。

测试(Rust)

首先,我们来用 Rust 代码进行测试。

首先安装测试所需要的依赖:

cargo add solana-sdk@1.18.26 solana-program-test@1.18.26 tokio --dev
# NOTE: There's no error like `Exceeding maximum ...` when building with solana-program = 2.1.4
# We use solana cli with version `2.1.4`
# To install solana-cli with version 2.1.4, run this command:
#
# sh -c "$(curl -sSfL https://release.anza.xyz/v2.1.4/install)"
#
# cargo add solana-sdk@=2.1.4 solana-program-test@=2.1.4 tokio --dev
# cargo add solana-program@=2.1.4

因为我们已经测试过,对于版本 2.1.4, 2.0.18, 2.0.3, 1.18.26 都能成功编译和测试,所以我们只选择了其中一个版本 1.18.26 来做演示。

测试结果输出:

(base) dylan@smalltown ~/Code/solana/projects/hello_world (master)> cargo test-sbf
   Compiling hello_world v0.1.0 (/Users/dylan/Code/solana/projects/hello_world)
    Finished `release` profile [optimized] target(s) in 2.46s
    Blocking waiting for file lock on build directory
   Compiling hello_world v0.1.0 (/Users/dylan/Code/solana/projects/hello_world)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 14.29s
     Running unittests src/lib.rs (target/debug/deps/hello_world-823cf88515d0fd05)

running 1 test
[2024-12-06T02:00:47.545448000Z INFO  solana_program_test] "hello_world" SBF program from /Users/dylan/Code/solana/projects/hello_world/target/deploy/hello_world.so, modified 16 seconds, 964 ms, 380 µs and 220 ns ago
[2024-12-06T02:00:47.750627000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1]
[2024-12-06T02:00:47.750876000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Hello, world!
[2024-12-06T02:00:47.750906000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 137 of 200000 compute units
[2024-12-06T02:00:47.750953000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM success
test test::test_hello_world ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.21s

   Doc-tests hello_world

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

测试(NodeJS)

接下来,我们来用 NodeJS 编写测试用例。

首先使用 pnpm 新建工程。

mkdir hello_world_frontend
cd hello_world_frontend

# 初始化 pnpm 项目
pnpm init

接下来安装依赖:

# 安装必要的依赖
pnpm add -D typescript ts-node @types/node chai ts-mocha solana-bankrun
pnpm add @solana/web3.js solana-bankrun

然后,编写测试程序:

import {
  PublicKey,
  Transaction,
  TransactionInstruction,
} from "@solana/web3.js";
import { start } from "solana-bankrun";
import { describe, test } from "node:test";
import { assert } from "chai";

describe("hello-solana", async () => {
  // load program in solana-bankrun
  const PROGRAM_ID = PublicKey.unique();
  const context = await start(
    [{ name: "hello_world", programId: PROGRAM_ID }],
    []
  );
  const client = context.banksClient;
  const payer = context.payer;

  test("Say hello!", async () => {
    const blockhash = context.lastBlockhash;
    // We set up our instruction first.
    let ix = new TransactionInstruction({
      // using payer keypair from context to sign the txn
      keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: true }],
      programId: PROGRAM_ID,
      data: Buffer.alloc(0), // No data
    });

    const tx = new Transaction();
    tx.recentBlockhash = blockhash;
    // using payer keypair from context to sign the txn
    tx.add(ix).sign(payer);

    // Now we process the transaction
    let transaction = await client.processTransaction(tx);

    assert(transaction.logMessages[0].startsWith("Program " + PROGRAM_ID));
    const message = "Program log: " + "Hello, world! GM!GN!";
    console.log("🌈🌈🌈 ");
    console.log(transaction.logMessages[1]);
    // NOTE: transaction.logMesages is an array:
    //
    // [
    //     'Program 11111111111111111111111111111112 invoke [1]',
    //     'Program log: Hello, world! GM!GN!',
    //     'Program 11111111111111111111111111111112 consumed 340 of 200000 compute units',
    //     'Program 11111111111111111111111111111112 success'
    // ]
    assert(transaction.logMessages[1] === message);
    assert(
      transaction.logMessages[2] ===
        "Program log: Our program's Program ID: " + PROGRAM_ID
    );
    assert(
      transaction.logMessages[3].startsWith(
        "Program " + PROGRAM_ID + " consumed"
      )
    );
    assert(transaction.logMessages[4] === "Program " + PROGRAM_ID + " success");
    assert(transaction.logMessages.length == 5);
  });
});

首先,我们通过 start 函数生成一个 context,这个 context 里会有和 bankServer 交互的 bankClient 以及 payer 账户。

接下来,通过 TransactionInstruction 来准备交易的 Instruction,发送交易需要对消息进行签名,这里使用 payer 来对交易进行签名,将它放在 keys 数组里。

let ix = new TransactionInstruction({
  keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: true }],
  programId: PROGRAM_ID,
  data: Buffer.alloc(0), // No data
});

创建一个新的交易指令 (TransactionInstruction),TransactionInstruction 的定义及参数类型 TransactionInstructionCtorFields 如下:

/**
 * Transaction Instruction class
 */
declare class TransactionInstruction {
  /**
   * Public keys to include in this transaction
   * Boolean represents whether this pubkey needs to sign the transaction
   */
  keys: Array<AccountMeta>;
  /**
   * Program Id to execute
   */
  programId: PublicKey;
  /**
   * Program input
   */
  data: Buffer;
  constructor(opts: TransactionInstructionCtorFields);
}

/**
 * List of TransactionInstruction object fields that may be initialized at construction
 */
type TransactionInstructionCtorFields = {
  keys: Array<AccountMeta>;
  programId: PublicKey;
  data?: Buffer;
};

关于 TransactionInstructionCtorFields 的说明:

  • keys: 需要签名的公钥(支付者的公钥)。
  • programId: 程序的 ID。
  • data: 这里没有附加数据。

然后我们准备 Transaction 的数据。

首先 Transaction 需要最近的区块哈希,这个可以从 contextlastBlockHash 获取。

const blockhash = context.lastBlockhash;

下面是创建交易的过程。

const tx = new Transaction();
tx.recentBlockhash = blockhash;
tx.add(ix).sign(payer);

创建一个新的交易 (Transaction) 需要如下步骤:

  • 设置最近的区块哈希。
  • 添加之前定义的指令(tx.add),并使用支付者的密钥对交易进行签名(.sign)。

add 函数通过 Javascript 的 Rest parameters 特性将参数转换成数组类型,每个数组类型的是 Transaction | TransactionInstruction | TransactionInstructionCtorFields 的联合类型 Union Type

declare class Transaction {
  /**
   * Signatures for the transaction.  Typically created by invoking the
   * `sign()` method
   */
  signatures: Array<SignaturePubkeyPair>;
  /**
   * The first (payer) Transaction signature
   *
   * @returns {Buffer | null} Buffer of payer's signature
   */
  get signature(): Buffer | null;
  /**
   * The transaction fee payer
   */
  feePayer?: PublicKey;
  /**
   * The instructions to atomically execute
   */
  instructions: Array<TransactionInstruction>;
  /**
   * Add one or more instructions to this Transaction
   *
   * @param {Array< Transaction | TransactionInstruction | TransactionInstructionCtorFields >} items - Instructions to add to the Transaction
   */
  add(
    ...items: Array<
      Transaction | TransactionInstruction | TransactionInstructionCtorFields
    >
  ): Transaction;
}

创建完交易之后,通过 client.processTransaction 发送交易并等到结果。

let transaction = await client.processTransaction(tx);

这里是 processTransaction 的定义:

/**
 * A client for the ledger state, from the perspective of an arbitrary validator.
 *
 * The client is used to send transactions and query account data, among other things.
 * Use `start()` to initialize a BanksClient.
 */
export declare class BanksClient {
  constructor(inner: BanksClientInner);
  private inner;
  /**
   * Send a transaction and return immediately.
   * @param tx - The transaction to send.
   */
  sendTransaction(tx: Transaction | VersionedTransaction): Promise<void>;
  /**
   * Process a transaction and return the result with metadata.
   * @param tx - The transaction to send.
   * @returns The transaction result and metadata.
   */
  processTransaction(
    tx: Transaction | VersionedTransaction
  ): Promise<BanksTransactionMeta>;
}

inner 是个 BanksClient,除了处理交易外,它还能干很多事情,以下是它的定义。

export class BanksClient {
  getAccount(address: Uint8Array, commitment?: CommitmentLevel | undefined | null): Promise<Account | null>
  sendLegacyTransaction(txBytes: Uint8Array): Promise<void>
  sendVersionedTransaction(txBytes: Uint8Array): Promise<void>
  processLegacyTransaction(txBytes: Uint8Array): Promise<BanksTransactionMeta>
  processVersionedTransaction(txBytes: Uint8Array): Promise<BanksTransactionMeta>
  tryProcessLegacyTransaction(txBytes: Uint8Array): Promise<BanksTransactionResultWithMeta>
  tryProcessVersionedTransaction(txBytes: Uint8Array): Promise<BanksTransactionResultWithMeta>
  simulateLegacyTransaction(txBytes: Uint8Array, commitment?: CommitmentLevel | undefined | null): Promise<BanksTransactionResultWithMeta>
  simulateVersionedTransaction(txBytes: Uint8Array, commitment?: CommitmentLevel | undefined | null): Promise<BanksTransactionResultWithMeta>
  getTransactionStatus(signature: Uint8Array): Promise<TransactionStatus | null>
  getTransactionStatuses(signatures: Array<Uint8Array>): Promise<Array<TransactionStatus | undefined | null>>
  getSlot(commitment?: CommitmentLevel | undefined | null): Promise<bigint>
  getBlockHeight(commitment?: CommitmentLevel | undefined | null): Promise<bigint>
  getRent(): Promise<Rent>
  getClock(): Promise<Clock>
  getBalance(address: Uint8Array, commitment?: CommitmentLevel | undefined | null): Promise<bigint>
  getLatestBlockhash(commitment?: CommitmentLevel | undefined | null): Promise<BlockhashRes | null>
  getFeeForMessage(messageBytes: Uint8Array, commitment?: CommitmentLevel | undefined | null): Promise<bigint | null>
}

/**
	 * Process a transaction and return the result with metadata.
	 * @param tx - The transaction to send.
	 * @returns The transaction result and metadata.
	 */
	async processTransaction(
		tx: Transaction | VersionedTransaction,
	): Promise<BanksTransactionMeta> {
		const serialized = tx.serialize();
		const internal = this.inner;
		const inner =
			tx instanceof Transaction
				? await internal.processLegacyTransaction(serialized)
				: await internal.processVersionedTransaction(serialized);
		return new BanksTransactionMeta(inner);
	}

processTransaction 会先通过 serialize 对 transaction 进行序列化,判断属于 LegacyTransaction 还是 VersionedTransaction,分别调用 processLegacyTransactionprocessVersionedTransaction 异步方法,并将结果通过 BanksTransactionMeta 返回。

BanksTransactionMeta 包含了 logMessages returnDatacomputeUnitsConsumed 属性。

export class TransactionReturnData {
  get programId(): Uint8Array;
  get data(): Uint8Array;
}
export class BanksTransactionMeta {
  get logMessages(): Array<string>;
  get returnData(): TransactionReturnData | null;
  get computeUnitsConsumed(): bigint;
}

其中 logMessages 是一个字符串数组,用于存储与交易相关的日志消息。我们可以通过这些日志信息,对测试结果进行验证。

比如,可以通过对 logMessages[0] 验证 solana program 被调用时,会输出以 Program + PROGRAM_ID 开头的内容:

assert(transaction.logMessages[0].startsWith("Program " + PROGRAM_ID));

一个简单的 logMessages 数组的例子:

[
  "Program 11111111111111111111111111111112 invoke [1]",
  "Program log: Hello, world! GM!GN!",
  "Program log: Our program's Program ID: {program_id}",
  "Program 11111111111111111111111111111112 consumed 443 of 200000 compute units",
  "Program 11111111111111111111111111111112 success"
]

值得注意的是,在我们的 solana program 里,第一个 msg! 输出的日志是 Hello, world! GM!GN!,但是发送交易返回的 logMessages 数组里它在数组的第二个元素,这是什么原因呢?

#![allow(unused)]
fn main() {
pub fn process_instruction(
    program_id: &Pubkey,
    _accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    msg!("Hello, world! GM!GN!");
    // NOTE: You must not use interpolating string like this, as it will not
    // output the string value correctly.
    //
    // You must use placeholder instead.
    //
    // Below is the transaction.logMessages array when using interpolating string
    //
    // [
    //     'Program 11111111111111111111111111111112 invoke [1]',
    //     'Program log: Hello, world! GM!GN!',
    //     "Program log: Our program's Program ID: {program_id}",
    //     'Program 11111111111111111111111111111112 consumed 443 of 200000 compute units',
    //     'Program 11111111111111111111111111111112 success'
    // ]
    // msg!("Our program's Program ID: {program_id}");
    msg!("Our program's Program ID: {}", program_id);
    Ok(())
}
}

其原因是 solana program 执行时 program runtime 会通过 program_invoke 函数打印被调用的日志,也就是这里的: Program 11111111111111111111111111111112 invoke [1]。关于 program_invoke 函数的代码可以在 anza-xyz/agave 这里找到。

#![allow(unused)]
fn main() {
/// Log a program invoke.
///
/// The general form is:
///
/// ```notrust
/// "Program <address> invoke [<depth>]"
/// ```
pub fn program_invoke(
    log_collector: &Option<Rc<RefCell<LogCollector>>>,
    program_id: &Pubkey,
    invoke_depth: usize,
) {
    ic_logger_msg!(
        log_collector,
        "Program {} invoke [{}]",
        program_id,
        invoke_depth
    );
}
}

接下来的检查可以根据具体的业务场景按部就班的进行。

比如,下面检查 solana program 里第一个 msg! 打印的内容:

const message = "Program log: " + "Hello, world! GM!GN!";
assert(transaction.logMessages[1] === message);

接下来,检查 solana program 里第二个 msg! 打印的内容:

assert(transaction.logMessages[1] === message);
assert(
  transaction.logMessages[2] ===
    "Program log: Our program's Program ID: " + PROGRAM_ID
);

再下来,检查其他日志消息的内容和格式,包括程序的成功消息和消耗的计算单位,并确保日志消息的总数为 5

assert(
  transaction.logMessages[3].startsWith("Program " + PROGRAM_ID + " consumed")
);
assert(transaction.logMessages[4] === "Program " + PROGRAM_ID + " success");
assert(transaction.logMessages.length == 5);

至此,一个简单的通过 NodeJS 编写的测试就写好了。

All in one test setup script

如果你比较懒,可以直接运行以下脚本到 setup.sh,并运行 bash setup.sh

# 创建测试目录
mkdir hello_world_frontend
cd hello_world_frontend

# 初始化 pnpm 项目
pnpm init

# 安装必要的依赖
pnpm add -D typescript ts-node @types/node chai ts-mocha solana-bankrun
pnpm add @solana/web3.js solana-bankrun

# 创建 TypeScript 配置文件
cat > tsconfig.json << EOF
{
  "compilerOptions": {
    "target": "es2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}
EOF

# 创建源代码目录和测试文件
mkdir -p tests
cat > tests/hello_world.test.ts << EOF
import {
    PublicKey,
    Transaction,
    TransactionInstruction,
  } from "@solana/web3.js";
  import { start } from "solana-bankrun";
  import { describe, test } from "node:test";
  import { assert } from "chai";

  describe("hello-solana", async () => {
    // load program in solana-bankrun
    const PROGRAM_ID = PublicKey.unique();
    const context = await start(
      [{ name: "hello_world", programId: PROGRAM_ID }],
      [],
    );
    const client = context.banksClient;
    const payer = context.payer;

    test("Say hello!", async () => {
        const blockhash = context.lastBlockhash;
        // We set up our instruction first.
        let ix = new TransactionInstruction({
          // using payer keypair from context to sign the txn
          keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: true }],
          programId: PROGRAM_ID,
          data: Buffer.alloc(0), // No data
        });

        const tx = new Transaction();
        tx.recentBlockhash = blockhash;
        // using payer keypair from context to sign the txn
        tx.add(ix).sign(payer);

        // Now we process the transaction
        let transaction = await client.processTransaction(tx);

        assert(transaction.logMessages[0].startsWith("Program " + PROGRAM_ID));
        const message = "Program log: " + "Hello, world! GM!GN!";
        console.log("🌈🌈🌈 ");
        console.log(transaction.logMessages);
        assert(transaction.logMessages[1] === message);
        assert(
          transaction.logMessages[2] ===
            "Program log: Our program's Program ID: " + PROGRAM_ID,
        );
        assert(
          transaction.logMessages[3].startsWith(
            "Program " + PROGRAM_ID + " consumed",
          ),
        );
        assert(transaction.logMessages[4] === "Program " + PROGRAM_ID + " success");
        assert(transaction.logMessages.length == 5);
      });
});
EOF

# 更新 package.json 添加测试脚本
cat > package.json << EOF
{
  "name": "hello_world_frontend",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "pnpm ts-mocha -p ./tsconfig.json -t 1000000 ./tests/hello_world.test.ts"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/jest": "^29.5.11",
    "@types/node": "^20.10.5",
    "chai": "^5.1.2",
    "jest": "^29.7.0",
    "solana-bankrun": "^0.4.0",
    "ts-jest": "^29.1.1",
    "ts-mocha": "^10.0.0",
    "ts-node": "^10.9.2",
    "typescript": "^5.3.3"
  },
  "dependencies": {
    "@solana/web3.js": "^1.87.6"
  }
}

# 运行测试
pnpm test
EOF

Frontend

我们有两种方法来开发 solana frontend:

  1. 使用 Anchor 框架
  2. 不使用 Anchor 框架

我会帮你实现两种方法来开发 Solana frontend。让我们从最基础的开始,逐步构建。

1. 不使用 Anchor 框架

首先创建一个新的 Next.js 项目:

npx create-next-app@latest solana-frontend-nextjs --typescript --tailwind --eslint
cd solana-frontend-nextjs

安装必要的依赖:

pnpm install \
  @solana/web3.js \
  @solana/wallet-adapter-react \
  @solana/wallet-adapter-react-ui \
  @solana/wallet-adapter-base \
  @solana/wallet-adapter-wallets

1.1 基础设置

首先创建钱包配置文件:

'use client'

import { FC, ReactNode, useMemo } from "react";
import {
  ConnectionProvider,
  WalletProvider,
} from "@solana/wallet-adapter-react";
import { WalletModalProvider } from "@solana/wallet-adapter-react-ui";
import { clusterApiUrl } from "@solana/web3.js";
import {
  PhantomWalletAdapter,
  SolflareWalletAdapter,
} from "@solana/wallet-adapter-wallets";

require("@solana/wallet-adapter-react-ui/styles.css");

export const WalletContextProvider: FC<{ children: ReactNode }> = ({ children }) => {
  const url = useMemo(() => clusterApiUrl("devnet"), []);
  const wallets = useMemo(
    () => [
      new PhantomWalletAdapter(),
      new SolflareWalletAdapter(),
    ],
    []
  );

  return (
    <ConnectionProvider endpoint={url}>
      <WalletProvider wallets={wallets} autoConnect>
        <WalletModalProvider>{children}</WalletModalProvider>
      </WalletProvider>
    </ConnectionProvider>
  );
};

更新 layout 文件:

import { WalletContextProvider } from '@/context/WalletContextProvider'
import './globals.css'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <WalletContextProvider>
          {children}
        </WalletContextProvider>
      </body>
    </html>
  )
}

1.2 创建主页面组件

注意,要在 src/app/page.tsx 文件中,将 PROGRAM_ID 替换为你的程序 ID。

'use client'

import { useConnection, useWallet } from '@solana/wallet-adapter-react'
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'
import { LAMPORTS_PER_SOL, PublicKey, Transaction, TransactionInstruction } from '@solana/web3.js'
import { FC, useState } from 'react'

const Home: FC = () => {
  const { connection } = useConnection()
  const { publicKey, sendTransaction } = useWallet()
  const [loading, setLoading] = useState(false)

  // 替换为你的程序 ID
  const PROGRAM_ID = new PublicKey("3KUbj4gMH77adZnZhatXutJ695qCGzB6G8cmMU1SYMWW")

  const sayHello = async () => {
    if (!publicKey) {
      alert("Please connect your wallet!")
      return
    }

    setLoading(true)
    try {
      const instruction = new TransactionInstruction({
        keys: [
          {
            pubkey: publicKey,
            isSigner: true,
            isWritable: true,
          },
        ],
        programId: PROGRAM_ID,
        data: Buffer.from([]),
      })

      const transaction = new Transaction()
      transaction.add(instruction)

      const signature = await sendTransaction(transaction, connection)
      await connection.confirmTransaction(signature)

      alert("Transaction successful!")
    } catch (error) {
      console.error(error)
      alert(`Error: ${error instanceof Error ? error.message : String(error)}`)
    } finally {
      setLoading(false)
    }
  }

  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm">
        <div className="flex flex-col items-center gap-8">
          <h1 className="text-4xl font-bold">Solana Hello World</h1>
          <WalletMultiButton />
          {publicKey && (
            <button
              onClick={sayHello}
              disabled={loading}
              className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
            >
              {loading ? "Processing..." : "Say Hello"}
            </button>
          )}
        </div>
      </div>
    </main>
  )
}

export default Home

1.3 运行项目

运行:

pnpm dev

点击 Say Hello 按钮通过 phantom wallet 发送交易,交易成功之后,可以在 explorer 上看到交易详情:

https://explorer.solana.com/tx/4H3nfuDqaz1s6TDGe3HSL6DsEvq9r3TwcGqqw9kfGGk3c9pjK2HGohmCrfWcZCFXdMJsPobsbcj3UAdmkj2QK8vd?cluster=devnet

2. 使用 Anchor 框架

创建新项目:

npx create-next-app@latest solana-anchor-frontend-nextjs --typescript --tailwind --eslint
cd solana-anchor-frontend-nextjs

安装依赖:

pnpm install \
  @coral-xyz/anchor \
  @solana/web3.js \
  @solana/wallet-adapter-react \
  @solana/wallet-adapter-react-ui \
  @solana/wallet-adapter-base \
  @solana/wallet-adapter-wallets

2.1 创建 Anchor IDL 类型

export type HelloWorld = {
  "version": "0.1.0",
  "name": "hello_world",
  "instructions": [
    {
      "name": "sayHello",
      "accounts": [],
      "args": []
    }
  ]
};

export const IDL: HelloWorld = {
  "version": "0.1.0",
  "name": "hello_world",
  "instructions": [
    {
      "name": "sayHello",
      "accounts": [],
      "args": []
    }
  ]
};

2.2 创建 Anchor 工作区提供者

"use client";

import { createContext, useContext, ReactNode } from "react"
import { Program, AnchorProvider } from "@coral-xyz/anchor"
import { AnchorWallet, useAnchorWallet, useConnection } from "@solana/wallet-adapter-react"
import { HelloWorld, IDL } from "@/idl/hello_world"
import { PublicKey } from "@solana/web3.js"

const WorkspaceContext = createContext({})

interface Workspace {
  program?: Program<HelloWorld>
}

export const WorkspaceProvider = ({ children }: { children: ReactNode }) => {
  const { connection } = useConnection()
  const wallet = useAnchorWallet()

  const provider = new AnchorProvider(
    connection,
    wallet as AnchorWallet,
    AnchorProvider.defaultOptions()
  )

  const program = new Program(
    IDL,
    new PublicKey("3KUbj4gMH77adZnZhatXutJ695qCGzB6G8cmMU1SYMWW"),
    provider
  )

  const workspace = {
    program,
  }

  return (
    <WorkspaceContext.Provider value={workspace}>
      {children}
    </WorkspaceContext.Provider>
  )
}

export const useWorkspace = (): Workspace => {
  return useContext(WorkspaceContext) as Workspace
}

2.3 更新布局组件

import { WalletContextProvider } from '@/context/WalletContextProvider'
import { WorkspaceProvider } from '@/context/WorkspaceProvider'
import './globals.css'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>
        <WalletContextProvider>
          <WorkspaceProvider>
            {children}
          </WorkspaceProvider>
        </WalletContextProvider>
      </body>
    </html>
  )
}

2.4 创建主页面组件

'use client'

import { useWallet } from '@solana/wallet-adapter-react'
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'
import { FC, useState } from 'react'
import { useWorkspace } from '@/context/WorkspaceProvider'

const Home: FC = () => {
  const { publicKey } = useWallet()
  const { program } = useWorkspace()
  const [loading, setLoading] = useState(false)

  const sayHello = async () => {
    if (!publicKey || !program) {
      alert("Please connect your wallet!")
      return
    }

    setLoading(true)
    try {
      const tx = await program.methods
        .sayHello()
        .accounts({})
        .rpc()

      alert(`Transaction successful! Signature: ${tx}`)
    } catch (error) {
      console.error(error)
      alert(`Error: ${error instanceof Error ? error.message : String(error)}`)
    } finally {
      setLoading(false)
    }
  }

  return (
    <main className="flex min-h-screen flex-col items-center justify-between p-24">
      <div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm">
        <div className="flex flex-col items-center gap-8">
          <h1 className="text-4xl font-bold">Solana Hello World (Anchor)</h1>
          <WalletMultiButton />
          {publicKey && (
            <button
              onClick={sayHello}
              disabled={loading}
              className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
            >
              {loading ? "Processing..." : "Say Hello"}
            </button>
          )}
        </div>
      </div>
    </main>
  )
}

export default Home

2.5 tsconfig.json 配置

为了正确使用 @ 路径别名,需要配置 tsconfig.json 文件:

{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./*"],
      "@/idl/*": ["./app/idl/*"],
      "@/context/*": ["./app/context/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

这个配置文件增加了 @/idl/*@/context/* 的别名,以便在代码中使用这些路径。

2.6 运行项目

运行:

pnpm dev

点击 Say Hello 按钮通过 phantom wallet 发送交易,交易成功之后,可以在 explorer 上看到交易详情:

https://explorer.solana.com/tx/5dustfzfhSopVKrDiL3CoXAg35jimMBs3oFkxDsiBqM1xQ6t4JnsonbdZirzYdR5i5HGsUKmfhKZb3NQunWDbWiw?cluster=devnet

两种方法的主要区别

  1. 不使用 Anchor:
  • 直接使用 @solana/web3.js 创建交易和指令
  • 手动构建交易结构
  • 更底层的控制
  1. 使用 Anchor:
  • 使用 Anchor IDL 类型定义
  • 更高级的抽象和类型安全
  • 更简洁的程序调用方式
  • 更好的开发体验

选择哪种方法取决于你的需求:

  • 如果需要更多底层控制或项目较小,可以选择不使用 Anchor
  • 如果需要更好的开发体验和类型安全,建议使用 Anchor

下一步

至此,我们已经完成了一个最基础的 Solana 程序的开发和部署。虽然这个程序只是简单地打印 "Hello, world!",但它包含了 Solana 程序开发的基本要素:

  • 程序入口点的定义
  • 基本的参数结构
  • 构建和部署流程

在接下来的内容中,我们将学习:

  • 如何使用 Anchor 框架开发程序
  • 如何处理账户数据
  • 如何实现更复杂的指令逻辑
  • 如何进行程序测试
  • 如何确保程序安全性

敬请期待吧!

Refs

关于 cargo-build-sbf 解释 https://github.com/solana-labs/solana/issues/34987#issuecomment-1913538260

https://solana.stackexchange.com/questions/16443/error-function-stack-offset-of-7256-exceeded-max-offset-of-4096-by-3160-bytes

安装 solana cli tool suites(注意不要安装 edge 版本,会发现部署不成功问题) https://solana.com/docs/intro/installation

https://github.com/solana-labs/solana/issues/34987#issuecomment-1914665002 https://github.com/anza-xyz/agave/issues/1572

在 solana 编写一个 helloworld https://solana.com/developers/guides/getstarted/local-rust-hello-world#create-a-new-rust-library-with-cargo

solana wallet nextjs setup https://solana.com/developers/guides/wallets/add-solana-wallet-adapter-to-nextjs

https://solana.com/developers/cookbook/wallets/connect-wallet-react https://www.anza.xyz/blog/solana-web3-js-2-release

https://solana.stackexchange.com/questions/1723/anchor-useanchorwallet-vs-solanas-usewallet

anchor client side development https://solana.com/developers/courses/onchain-development/intro-to-anchor-frontend