Solana DApp 实战(三):从零构建 Rust 客户端与合约交互
Table of Contents
Solana DApp 实战(三):从零构建 Rust 客户端与合约交互
在《Solana 投票 DApp 开发实战》与《Solana 开发进阶:Codama 客户端代码生成与合约交互实战》两篇文章之后,我们已经拥有了一个部署在链上、并通过 TypeScript 脚本完整测试过的投票程序。现在,我们将探索另一条更接近 Solana 底层的道路:使用 Rust——Solana 的原生语言——来构建客户端并与合约交互。
本文将作为我们实战系列的第三篇,利用 Codama 生成的另一半宝贵财富——Rust 客户端代码,从零开始搭建一个独立的客户端项目。我们将直面并解决 Rust 开发中最真实的挑战:棘手的依赖管理与编译错误。通过这个过程,您不仅将学会如何调用合约,更将掌握在复杂工作区(Workspace)中解决依赖冲突的硬核技能,并最终构建出一个专业、可重复的自动化集成测试脚本。
实操
使用生成的 Rust 代码测试 Solana 合约:客户端项目搭建指南
本指南将引导您将 codama
生成的代码配置为一个独立的库,并成功地在 rust-client
项目中调用它。
查看生成的 Rust 代码的目录结构
voting on main [!?] via ⬢ v23.11.0 via 🍞 v1.2.17 via 🦀 1.88.0 on 🐳 v28.2.2 (orbstack) took 3.2s
➜ cd generated/rs
voting/generated/rs on main [!?] via ⬢ v23.11.0 via 🍞 v1.2.17 via 🦀 1.88.0 on 🐳 v28.2.2 (orbstack)
➜ tree . -L 6 -I "migrations|mochawesome-report|.anchor|docs|target|node_modules|voting-graph|voting-substreams"
.
└── voting
├── accounts
│ ├── candidate_account.rs
│ ├── mod.rs
│ ├── poll_account.rs
│ └── voter_receipt.rs
├── Cargo.toml
├── errors
│ ├── mod.rs
│ └── voting.rs
├── instructions
│ ├── add_candidate.rs
│ ├── initialize_poll.rs
│ ├── mod.rs
│ └── vote.rs
├── mod.rs
├── programs.rs
└── shared.rs
5 directories, 14 files
第一步:为生成的代码创建 Cargo.toml
codama
生成的代码需要它自己的 Cargo.toml
文件来声明其依赖项。
请在 generated/rs/voting/
目录下创建一个名为 Cargo.toml
的新文件。
文件路径: generated/rs/voting/Cargo.toml
文件内容:
[package]
name = "voting_client"
version = "0.1.0"
edition = "2021"
[lib]
path = "mod.rs"
[features]
default = []
fetch = []
anchor = []
anchor-idl-build = []
serde = ["dep:serde", "dep:serde_with"]
[dependencies]
# 核心依赖
borsh = "1.5.1"
solana-program = "2.3.0"
solana-pubkey = "2.3.0"
solana-instruction = "2.3.0"
solana-account-info = "2.3.0"
solana-cpi = "2.2.1"
solana-program-error = "2.2.2"
solana-decode-error = "2.3.0"
solana-program-entrypoint = "2.3.0"
solana-msg = "2.2.1"
# 其他必要的辅助库
thiserror = "2.0.12"
num-derive = "0.4"
num-traits = "0.2"
# serde 依赖,设为可选
serde = { version = "1.0", optional = true, features = ["derive"] }
serde_with = { version = "3.8", optional = true, features = ["macros"] }
第二步:创建新的 Rust 客户端项目(rust-client
项目)
首先,我们需要在您现有 solana-voting-dapp
项目的根目录下,创建一个新的二进制(binary)Rust 项目。这个新项目将作为我们的客户端。
打开终端,确保您位于 solana-voting-dapp
目录下,然后运行:
cargo new rust-client --bin
-
cargo new
是创建新 Rust 项目的标准命令。 -
rust-client
是我们新项目的名字。 -
--bin
指定了我们要创建一个可执行的应用,而不是一个库。
实操
voting on main via ⬢ v23.11.0 via 🍞 v1.2.17 via 🦀 1.88.0 on 🐳 v28.2.2 (orbstack)
➜ cargo new rust-client --bin
Creating binary (application) `rust-client` package
Adding `rust-client` as member of workspace at `/Users/qiaopengjun/Code/Solana/voting`
warning: profiles for the non root package will be ignored, specify profiles at the workspace root:
package: /Users/qiaopengjun/Code/Solana/voting/voting-substreams/voting_substreams/Cargo.toml
workspace: /Users/qiaopengjun/Code/Solana/voting/Cargo.toml
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
第三步:配置客户端的 Cargo.toml
现在,我们需要为 rust-client
项目添加必要的依赖。这包括 Solana 相关的 SDK,以及将我们之前用 codama
生成的 Rust 代码作为本地库引用进来。
[package]
name = "rust-client"
version = "0.1.0"
edition = "2021"
license = "MIT"
[dependencies]
# --- 核心依赖 ---
solana-sdk = "2.3.1"
solana-client = "2.3.5"
anchor-client = "0.31.1"
# --- 标准的异步和工具库 ---
tokio = { version = "1", features = ["full"] }
anyhow = "1.0.98"
dotenvy = "0.15.7"
chrono = "0.4"
bs58 = "0.5.1"
serde_json = "1.0"
# --- 通过 path 依赖我们刚刚创建的本地库 ---
voting_client = { path = "../generated/rs/voting" }
第四步:编写第一个 Rust 客户端脚本 (main.rs
)
use anyhow::Result;
use chrono::Utc;
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
signature::{Keypair, Signer},
transaction::Transaction,
};
use std::convert::TryFrom;
use std::{env, fs};
// --- 现在可以直接像使用外部库一样导入 ---
use voting_client::instructions::InitializePollBuilder;
/// 从文件加载钱包 Keypair
fn load_wallet(path: &str) -> Result<Keypair> {
let content = fs::read_to_string(path)?;
let bytes: Vec<u8> = serde_json::from_str(&content)?;
Ok(Keypair::try_from(bytes.as_slice())?)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("--- 🚀 Starting Final Rust Client ---");
// 从项目根目录加载 .env 文件
dotenvy::from_path("../.env").ok();
let rpc_url =
env::var("RPC_URL").unwrap_or_else(|_| "https://api.devnet.solana.com".to_string());
let wallet_path = env::var("WALLET_PATH").unwrap_or_else(|_| {
let home = env::var("HOME").expect("HOME environment variable is not set");
format!("{}/.config/solana/id.json", home)
});
let client = RpcClient::new(rpc_url);
let signer = load_wallet(&wallet_path)?;
let poll_account = Keypair::new();
println!("🔑 Signer (Authority): {}", signer.pubkey());
println!("📝 New Poll Account Address: {}", poll_account.pubkey());
// InitializePollBuilder 会自动处理 program_id 和 system_program
let instruction = InitializePollBuilder::new()
.signer(signer.pubkey())
.poll_account(poll_account.pubkey())
.name("Poll from Rust Client (Final)".to_string())
.description("This should finally work!".to_string())
.start_time((Utc::now().timestamp() - 60) as u64)
.end_time((Utc::now().timestamp() + 3600) as u64)
.instruction();
let recent_blockhash = client.get_latest_blockhash()?;
let transaction = Transaction::new_signed_with_payer(
&[instruction],
Some(&signer.pubkey()),
&[&signer, &poll_account],
recent_blockhash,
);
println!("\n⏳ Sending transaction...");
let signature = client.send_and_confirm_transaction(&transaction)?;
println!("\n✅ Success! The transaction was confirmed.");
println!(" - Transaction Signature: {}", signature);
println!(
" - Review on Explorer: https://explorer.solana.com/tx/{}?cluster=devnet",
signature
);
Ok(())
}
第五步:清理并运行
在应用了上述所有修改后,请在您项目的根目录(solana-voting-dapp/
)下执行以下命令来清理旧的依赖并重新构建:
# 1. 清理旧的构建产物
cargo clean
# 2. 更新整个工作区的依赖锁文件
cargo update
# 3. 现在,进入客户端目录并运行
cd rust-client
cargo run
实操
voting/rust-client on main [!?] is 📦 0.1.0 via ⬢ v23.11.0 via 🍞 v1.2.17 via 🦀 1.88.0 on 🐳 v28.2.2 (orbstack)
➜ cargo clean
cargo update
cargo build
voting/rust-client on main [!?] is 📦 0.1.0 via ⬢ v23.11.0 via 🍞 v1.2.17 via 🦀 1.88.0 on 🐳 v28.2.2 (orbstack)
➜ cargo build
warning: profiles for the non root package will be ignored, specify profiles at the workspace root:
package: /Users/qiaopengjun/Code/Solana/voting/voting-substreams/voting_substreams/Cargo.toml
workspace: /Users/qiaopengjun/Code/Solana/voting/Cargo.toml
warning: use of deprecated trait `solana_program_error::PrintProgramError`: Use `ToStr` instead with `solana_msg::msg!` or any other logging
--> generated/rs/voting/errors/voting.rs:30:28
|
30 | impl solana_program_error::PrintProgramError for VotingError {
| ^^^^^^^^^^^^^^^^^
|
= note: `#[warn(deprecated)]` on by default
warning: use of deprecated trait `solana_decode_error::DecodeError`: Use `num_traits::FromPrimitive` instead
--> generated/rs/voting/errors/voting.rs:36:30
|
36 | impl<T> solana_decode_error::DecodeError<T> for VotingError {
| ^^^^^^^^^^^
warning: `voting_client` (lib) generated 2 warnings
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.74s
在 rust-client
目录下运行 cargo run
来执行您的客户端并与合约交互
voting/rust-client on main [!?] is 📦 0.1.0 via ⬢ v23.11.0 via 🍞 v1.2.17 via 🦀 1.88.0 on 🐳 v28.2.2 (orbstack)
➜ cargo run
warning: profiles for the non root package will be ignored, specify profiles at the workspace root:
package: /Users/qiaopengjun/Code/Solana/voting/voting-substreams/voting_substreams/Cargo.toml
workspace: /Users/qiaopengjun/Code/Solana/voting/Cargo.toml
warning: use of deprecated trait `solana_program_error::PrintProgramError`: Use `ToStr` instead with `solana_msg::msg!` or any other logging
--> generated/rs/voting/errors/voting.rs:30:28
|
30 | impl solana_program_error::PrintProgramError for VotingError {
| ^^^^^^^^^^^^^^^^^
|
= note: `#[warn(deprecated)]` on by default
warning: use of deprecated trait `solana_decode_error::DecodeError`: Use `num_traits::FromPrimitive` instead
--> generated/rs/voting/errors/voting.rs:36:30
|
36 | impl<T> solana_decode_error::DecodeError<T> for VotingError {
| ^^^^^^^^^^^
warning: `voting_client` (lib) generated 2 warnings
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.82s
Running `/Users/qiaopengjun/Code/Solana/voting/target/debug/rust-client`
--- 🚀 Starting Final Rust Client ---
🔑 Signer (Authority): 6MZDRo5v8K2NfdohdD76QNpSgk3GH3Aup53BeMaRAEpd
📝 New Poll Account Address: Gz4dAiGYGjUh3PhXz7DRcN6rMygYtsNHeuPmrwQUR9gb
⏳ Sending transaction...
✅ Success! The transaction was confirmed.
- Transaction Signature: 4iUtG5WYj8R4AJyZjUAKw1PwEQtDy99DEGSJQcUMEEANJ8gcS1YSN12Q1fhgerX1ktHuU6atSsKYzXF1wMk2VSn6
- Review on Explorer: https://explorer.solana.com/tx/4iUtG5WYj8R4AJyZjUAKw1PwEQtDy99DEGSJQcUMEEANJ8gcS1YSN12Q1fhgerX1ktHuU6atSsKYzXF1wMk2VSn6?cluster=devnet
100% 成功了!
🎉 巨大成功!恭喜您!
这标志着您已经彻底攻克了 Rust 客户端开发中最困难的部分——依赖管理和编译。
为什么说成功了?
您看到的日志是成功的最终证明:
Finished dev profile
: 您的rust-client
项目和voting_client
库都已成功编译。Running .../rust-client
: 您的程序开始执行。✅ Success! The transaction was confirmed.
: 这是最重要的信息,Solana Devnet 已经确认您的交易成功上链。Transaction Signature: ...
: 您获得了一个有效的交易签名,可以在区块链浏览器上查看。
您已经成功地用一个独立的 Rust 客户端,调用了您部署在链上的 Anchor 合约。在经历了这么多依赖问题的挑战后,这是一个非常了不起的成就!
现在,您可以充满信心地继续用 Rust 编写后续的脚本了,比如添加候选人、投票和验证结果。
当一个项目逐渐变大时,如何组织代码就变得至关重要。
将不同的测试脚本放在 src/examples/
目录下,而不是把所有代码都堆在 main.rs
里,有几个巨大的好处:
- 清晰性:每个文件只负责一个独立的任务(如
initialize_poll.rs
,add_candidate.rs
),代码逻辑清晰,易于维护。 - 独立运行:您可以使用
cargo run --example <文件名>
来独立运行任何一个测试脚本,非常灵活。 - 遵循惯例:这是标准的 Rust 项目结构,其他开发者也能很快看懂您的项目。
第六步:创建 examples
目录并编写第一个示例
重新组织代码创建 examples
目录并编写第一个示例
现在,我们将采用更专业的项目结构。
- 在
rust-client
目录下,创建一个名为examples
的新文件夹。 - 将
rust-client/src/main.rs
文件移动到rust-client/examples/
目录下,并将其重命名为initialize_poll.rs
。 - 现在,您可以将
rust-client/src/main.rs
文件删除,或者清空其内容,因为它不再被使用。
您的 rust-client
项目结构现在应该是这样的:
voting/rust-client on main [!?] is 📦 0.1.0 via ⬢ v23.11.0 via 🍞 v1.2.17 via 🦀 1.88.0 on 🐳 v28.2.2 (orbstack)
➜ tree . -L 6 -I "migrations|mochawesome-report|.anchor|docs|target|node_modules|voting-graph|voting-substreams"
.
├── Cargo.toml
├── examples
│ └── initialize_poll.rs
└── src
└── main.rs
3 directories, 3 files
第七步:运行您的第一个示例
要运行 examples
目录下的脚本,我们需要使用 --example
标志。
在 rust-client
目录下,运行:
cargo run --example initialize_poll
实操
voting/rust-client on main [!?] is 📦 0.1.0 via ⬢ v23.11.0 via 🍞 v1.2.17 via 🦀 1.88.0 on 🐳 v28.2.2 (orbstack)
➜ cargo run --example initialize_poll
warning: profiles for the non root package will be ignored, specify profiles at the workspace root:
package: /Users/qiaopengjun/Code/Solana/voting/voting-substreams/voting_substreams/Cargo.toml
workspace: /Users/qiaopengjun/Code/Solana/voting/Cargo.toml
warning: use of deprecated trait `solana_program_error::PrintProgramError`: Use `ToStr` instead with `solana_msg::msg!` or any other logging
--> generated/rs/voting/errors/voting.rs:30:28
|
30 | impl solana_program_error::PrintProgramError for VotingError {
| ^^^^^^^^^^^^^^^^^
|
= note: `#[warn(deprecated)]` on by default
warning: use of deprecated trait `solana_decode_error::DecodeError`: Use `num_traits::FromPrimitive` instead
--> generated/rs/voting/errors/voting.rs:36:30
|
36 | impl<T> solana_decode_error::DecodeError<T> for VotingError {
| ^^^^^^^^^^^
warning: `voting_client` (lib) generated 2 warnings
Compiling rust-client v0.1.0 (/Users/qiaopengjun/Code/Solana/voting/rust-client)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.04s
Running `/Users/qiaopengjun/Code/Solana/voting/target/debug/examples/initialize_poll`
--- 🚀 Starting Final Rust Client ---
🔑 Signer (Authority): 6MZDRo5v8K2NfdohdD76QNpSgk3GH3Aup53BeMaRAEpd
📝 New Poll Account Address: 8ucTtmiD8Jw4gCARkfWkVP1fQmfkTnXixEn5enfqCJUy
⏳ Sending transaction...
✅ Success! The transaction was confirmed.
- Transaction Signature: 619YKJtskQ9nkR971NxJpyfeiCD7tN4nGVcYdYWTPaJ9iLUkw49F4BQaTuHghMjKTwKbConxJ84XTpCBMCheptZu
- Review on Explorer: https://explorer.solana.com/tx/619YKJtskQ9nkR971NxJpyfeiCD7tN4nGVcYdYWTPaJ9iLUkw49F4BQaTuHghMjKTwKbConxJ84XTpCBMCheptZu?cluster=devnet
第八步:编写第二个示例 (add_candidate.rs
)
在 rust-client/examples/
目录下,创建一个名为 add_candidate.rs
的新文件。
文件路径: rust-client/examples/add_candidate.rs
文件内容:
use anyhow::Result;
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
pubkey::Pubkey,
signature::{Keypair, Signer},
transaction::Transaction,
};
use std::convert::TryFrom;
use std::{env, fs, str::FromStr};
use voting_client::{accounts::PollAccount, instructions::AddCandidateBuilder};
/// 从文件加载钱包 Keypair
fn load_wallet(path: &str) -> Result<Keypair> {
let content = fs::read_to_string(path)?;
let bytes: Vec<u8> = serde_json::from_str(&content)?;
Ok(Keypair::try_from(bytes.as_slice())?)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("--- 🚀 Starting [Step 2: Add Candidate] Rust Client ---");
// 从项目根目录加载 .env 文件
dotenvy::from_path("../.env").ok(); // 注意路径是 ../../
let rpc_url =
env::var("RPC_URL").unwrap_or_else(|_| "https://api.devnet.solana.com".to_string());
let wallet_path = env::var("WALLET_PATH").unwrap_or_else(|_| {
let home = env::var("HOME").expect("HOME environment variable is not set");
format!("{}/.config/solana/id.json", home)
});
// !! 重要:请将这里的地址替换为您在上一步中创建的 Poll Account 地址 !!
// 我已为您更新为您刚刚创建的地址
let poll_account_pubkey_str = "8ucTtmiD8Jw4gCARkfWkVP1fQmfkTnXixEn5enfqCJUy";
let poll_account_pubkey = Pubkey::from_str(poll_account_pubkey_str)?;
// 1. 初始化连接和钱包
let client = RpcClient::new(rpc_url);
let signer = load_wallet(&wallet_path)?;
println!("🔑 Signer (Authority): {}", signer.pubkey());
println!("📝 Using Poll Account: {}", poll_account_pubkey);
// 2. 获取链上投票账户的数据,以读取当前的 candidate_count
println!("\n⏳ Fetching poll account data...");
let poll_account_info = client.get_account(&poll_account_pubkey)?;
// --- 核心修正: 使用生成的 PollAccount::from_bytes 方法来解码 ---
let poll_account_data = PollAccount::from_bytes(&poll_account_info.data)?;
let current_candidate_count = poll_account_data.candidate_count;
println!("✅ Current candidate count is: {}", current_candidate_count);
// 3. 计算新候选人账户的 PDA
let (candidate_pda, _) = Pubkey::find_program_address(
&[
b"candidate",
&poll_account_pubkey.to_bytes(),
&[current_candidate_count], // count 是 u8
],
&voting_client::programs::VOTING_ID,
);
println!("🌱 New Candidate PDA: {}", candidate_pda);
// 4. 使用 Builder 构造指令
let candidate_name = format!("Candidate #{}", current_candidate_count + 1);
println!("➕ Adding candidate with name: \"{}\"", candidate_name);
let instruction = AddCandidateBuilder::new()
.signer(signer.pubkey())
.poll_account(poll_account_pubkey)
.candidate_account(candidate_pda)
.candidate_name(candidate_name)
.instruction();
// 5. 发送交易
let recent_blockhash = client.get_latest_blockhash()?;
let transaction = Transaction::new_signed_with_payer(
&[instruction],
Some(&signer.pubkey()),
&[&signer], // 只有 authority 需要签名
recent_blockhash,
);
println!("\n⏳ Sending transaction...");
let signature = client.send_and_confirm_transaction(&transaction)?;
println!("\n✅ Success! Candidate has been added.");
println!(" - Transaction Signature: {}", signature);
println!(
" - Review on Explorer: https://explorer.solana.com/tx/{}?cluster=devnet",
signature
);
Ok(())
}
第九步:运行您的第二个示例
voting/rust-client on main [!?] is 📦 0.1.0 via ⬢ v23.11.0 via 🍞 v1.2.17 via 🦀 1.88.0 on 🐳 v28.2.2 (orbstack) took 33.4s
➜ cargo run --example add_candidate
warning: profiles for the non root package will be ignored, specify profiles at the workspace root:
package: /Users/qiaopengjun/Code/Solana/voting/voting-substreams/voting_substreams/Cargo.toml
workspace: /Users/qiaopengjun/Code/Solana/voting/Cargo.toml
warning: use of deprecated trait `solana_program_error::PrintProgramError`: Use `ToStr` instead with `solana_msg::msg!` or any other logging
--> generated/rs/voting/errors/voting.rs:30:28
|
30 | impl solana_program_error::PrintProgramError for VotingError {
| ^^^^^^^^^^^^^^^^^
|
= note: `#[warn(deprecated)]` on by default
warning: use of deprecated trait `solana_decode_error::DecodeError`: Use `num_traits::FromPrimitive` instead
--> generated/rs/voting/errors/voting.rs:36:30
|
36 | impl<T> solana_decode_error::DecodeError<T> for VotingError {
| ^^^^^^^^^^^
warning: `voting_client` (lib) generated 2 warnings
Compiling rust-client v0.1.0 (/Users/qiaopengjun/Code/Solana/voting/rust-client)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.91s
Running `/Users/qiaopengjun/Code/Solana/voting/target/debug/examples/add_candidate`
--- 🚀 Starting [Step 2: Add Candidate] Rust Client ---
🔑 Signer (Authority): 6MZDRo5v8K2NfdohdD76QNpSgk3GH3Aup53BeMaRAEpd
📝 Using Poll Account: 8ucTtmiD8Jw4gCARkfWkVP1fQmfkTnXixEn5enfqCJUy
⏳ Fetching poll account data...
✅ Current candidate count is: 0
🌱 New Candidate PDA: D2dmKitcUCCcDYEsd1vT67rzF11xTV1kDZwJoXhRnet1
➕ Adding candidate with name: "Candidate #1"
⏳ Sending transaction...
✅ Success! Candidate has been added.
- Transaction Signature: 2WKtW4Cyo5yFzmShApBHRCaE4eQFCUd8q7PTGuP6zBuSkJJKpCpMBTpUpmS4PA1X1fUncAcpquTSMKZTKJMgNMT
- Review on Explorer: https://explorer.solana.com/tx/2WKtW4Cyo5yFzmShApBHRCaE4eQFCUd8q7PTGuP6zBuSkJJKpCpMBTpUpmS4PA1X1fUncAcpquTSMKZTKJMgNMT?cluster=devnet
您已经成功地用 Rust 客户端为您的投票活动添加了第一个候选人。日志中的 ✅ Success! Candidate has been added.
和返回的交易签名都证明了这一点。
您可以多次运行此脚本,每次它都会成功添加一个新的候选人。
第十步:编写第三个示例 (vote.rs
)
在 rust-client/examples/
目录下,创建一个名为 vote.rs
的新文件。
文件路径: rust-client/examples/vote.rs
文件内容:
use anyhow::Result;
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
pubkey::Pubkey,
signature::{Keypair, Signer},
transaction::Transaction,
};
use std::convert::TryFrom;
use std::{env, fs, str::FromStr};
// 导入生成的代码
use voting_client::instructions::VoteBuilder;
/// 从文件加载钱包 Keypair
fn load_wallet(path: &str) -> Result<Keypair> {
let content = fs::read_to_string(path)?;
let bytes: Vec<u8> = serde_json::from_str(&content)?;
Ok(Keypair::try_from(bytes.as_slice())?)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("--- 🚀 Starting [Step 3: Vote] Rust Client ---");
dotenvy::from_path("../.env").ok();
let rpc_url =
env::var("RPC_URL").unwrap_or_else(|_| "https://api.devnet.solana.com".to_string());
let wallet_path = env::var("WALLET_PATH").unwrap_or_else(|_| {
let home = env::var("HOME").expect("HOME environment variable is not set");
format!("{}/.config/solana/id.json", home)
});
// !! 重要:请将这里的地址替换为您之前步骤中创建的账户地址 !!
let poll_account_pubkey = Pubkey::from_str("8ucTtmiD8Jw4gCARkfWkVP1fQmfkTnXixEn5enfqCJUy")?;
let candidate_account_pubkey =
Pubkey::from_str("D2dmKitcUCCcDYEsd1vT67rzF11xTV1kDZwJoXhRnet1")?;
let client = RpcClient::new(rpc_url);
let voter = load_wallet(&wallet_path)?;
println!("🔑 Voter: {}", voter.pubkey());
println!("📝 Voting in Poll: {}", poll_account_pubkey);
println!("👍 Voting for Candidate: {}", candidate_account_pubkey);
// 1. 计算投票回执账户的 PDA
let (voter_receipt_pda, _) = Pubkey::find_program_address(
&[
b"receipt",
&poll_account_pubkey.to_bytes(),
&voter.pubkey().to_bytes(),
],
&voting_client::programs::VOTING_ID,
);
println!("🧾 Voter Receipt PDA: {}", voter_receipt_pda);
// 2. 使用 Builder 构造指令
let instruction = VoteBuilder::new()
.signer(voter.pubkey())
.poll_account(poll_account_pubkey)
.candidate_account(candidate_account_pubkey)
.voter_receipt(voter_receipt_pda)
.instruction();
// 3. 发送交易
let recent_blockhash = client.get_latest_blockhash()?;
let transaction = Transaction::new_signed_with_payer(
&[instruction],
Some(&voter.pubkey()),
&[&voter], // 只有投票者需要签名
recent_blockhash,
);
println!("\n⏳ Sending transaction...");
let signature = client.send_and_confirm_transaction(&transaction)?;
println!("\n✅ Success! Your vote has been cast.");
println!(" - Transaction Signature: {}", signature);
println!(
" - Review on Explorer: https://explorer.solana.com/tx/{}?cluster=devnet",
signature
);
Ok(())
}
第十一步:运行您的第三个示例
在运行前,请务必将 vote.rs
文件中的 poll_account_pubkey
和 candidate_account_pubkey
变量的值,替换为您通过前两个示例实际创建的地址。
然后,在 rust-client
目录下,运行:
voting/rust-client on main [!?] is 📦 0.1.0 via ⬢ v23.11.0 via 🍞 v1.2.17 via 🦀 1.88.0 on 🐳 v28.2.2 (orbstack) took 33.6s
➜ cargo run --example vote
warning: profiles for the non root package will be ignored, specify profiles at the workspace root:
package: /Users/qiaopengjun/Code/Solana/voting/voting-substreams/voting_substreams/Cargo.toml
workspace: /Users/qiaopengjun/Code/Solana/voting/Cargo.toml
warning: use of deprecated trait `solana_program_error::PrintProgramError`: Use `ToStr` instead with `solana_msg::msg!` or any other logging
--> generated/rs/voting/errors/voting.rs:30:28
|
30 | impl solana_program_error::PrintProgramError for VotingError {
| ^^^^^^^^^^^^^^^^^
|
= note: `#[warn(deprecated)]` on by default
warning: use of deprecated trait `solana_decode_error::DecodeError`: Use `num_traits::FromPrimitive` instead
--> generated/rs/voting/errors/voting.rs:36:30
|
36 | impl<T> solana_decode_error::DecodeError<T> for VotingError {
| ^^^^^^^^^^^
warning: `voting_client` (lib) generated 2 warnings
Compiling rust-client v0.1.0 (/Users/qiaopengjun/Code/Solana/voting/rust-client)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.44s
Running `/Users/qiaopengjun/Code/Solana/voting/target/debug/examples/vote`
--- 🚀 Starting [Step 3: Vote] Rust Client ---
🔑 Voter: 6MZDRo5v8K2NfdohdD76QNpSgk3GH3Aup53BeMaRAEpd
📝 Voting in Poll: 8ucTtmiD8Jw4gCARkfWkVP1fQmfkTnXixEn5enfqCJUy
👍 Voting for Candidate: D2dmKitcUCCcDYEsd1vT67rzF11xTV1kDZwJoXhRnet1
🧾 Voter Receipt PDA: CNHpJXwVtDB1xRk6psSoAwmxM7gFhaWJXLbJdm24VZ8P
⏳ Sending transaction...
Error: RPC response error -32002: Transaction simulation failed: Error processing Instruction 0: custom program error: 0x1771; 7 log messages:
Program Doo2arLUifZbfqGVS5Uh7nexAMmsMzaQH5zcwZhSoijz invoke [1]
Program log: Instruction: Vote
Program 11111111111111111111111111111111 invoke [2]
Program 11111111111111111111111111111111 success
Program log: AnchorError thrown in programs/voting/src/lib.rs:72. Error Code: PollEnded. Error Number: 6001. Error Message: Poll ended.
Program Doo2arLUifZbfqGVS5Uh7nexAMmsMzaQH5zcwZhSoijz consumed 10618 of 200000 compute units
Program Doo2arLUifZbfqGVS5Uh7nexAMmsMzaQH5zcwZhSoijz failed: custom program error: 0x1771
Caused by:
RPC response error -32002: Transaction simulation failed: Error processing Instruction 0: custom program error: 0x1771; 7 log messages:
Program Doo2arLUifZbfqGVS5Uh7nexAMmsMzaQH5zcwZhSoijz invoke [1]
Program log: Instruction: Vote
Program 11111111111111111111111111111111 invoke [2]
Program 11111111111111111111111111111111 success
Program log: AnchorError thrown in programs/voting/src/lib.rs:72. Error Code: PollEnded. Error Number: 6001. Error Message: Poll ended.
Program Doo2arLUifZbfqGVS5Uh7nexAMmsMzaQH5zcwZhSoijz consumed 10618 of 200000 compute units
Program Doo2arLUifZbfqGVS5Uh7nexAMmsMzaQH5zcwZhSoijz failed: custom program error: 0x1771
您的 vote.rs
脚本成功地编译、连接到了 Solana Devnet、正确地构建了交易,并成功地将它发送给了链上的合约。
错误信息是 Error Code: PollEnded
,意思是“投票已结束”。
因为我们在 initialize_poll
脚本中设置了投票的有效时间只有一小时。
当您运行 vote
脚本时,距离投票创建时已经过去很久了,投票早已过期。
您的合约在收到投票请求后,正确地检查了当前时间,发现投票已经结束,于是正确地拒绝了这笔交易,并返回了“PollEnded”错误。
您的合约正确地执行了您编写的时间检查规则,所以这是一个非常成功的“负面测试”。
第十二步:编写最终验证脚本 (verify_vote.rs
)
在 rust-client/examples/
目录下,创建一个名为 verify_vote.rs
的新文件。
文件路径: rust-client/examples/verify_vote.rs
文件内容:
use anyhow::Result;
use solana_client::rpc_client::RpcClient;
use solana_sdk::pubkey::Pubkey;
use std::{env, str::FromStr};
use voting_client::accounts::CandidateAccount;
#[tokio::main]
async fn main() -> Result<()> {
println!("--- 🚀 Starting [Step 4: Verify Vote] Rust Client ---");
dotenvy::from_path("../.env").ok();
let rpc_url =
env::var("RPC_URL").unwrap_or_else(|_| "https://api.devnet.solana.com".to_string());
// !! 重要:请将这里的地址替换为您投票的候选人地址 !!
let candidate_account_pubkey =
Pubkey::from_str("D2dmKitcUCCcDYEsd1vT67rzF11xTV1kDZwJoXhRnet1")?;
let client = RpcClient::new(rpc_url);
println!(
"🔍 Checking candidate account: {}",
candidate_account_pubkey
);
let account_info = client.get_account(&candidate_account_pubkey)?;
let candidate_data = CandidateAccount::from_bytes(&account_info.data)?;
println!("\n✅ Verification Successful!");
println!(" - Candidate Name: \"{}\"", candidate_data.name);
println!(" - Vote Count: {}", candidate_data.votes);
if candidate_data.votes > 0 {
println!("\n🎉🎉 Great! The vote was correctly recorded on-chain.");
} else {
println!("\n🤔 Hmm, the vote count is still 0. Something might be wrong.");
}
Ok(())
}
第十三步:运行验证脚本
将 verify_vote.rs
中的地址替换为您刚刚投票的候选人地址,然后在 rust-client
目录下运行:
voting/rust-client on main [!?] is 📦 0.1.0 via ⬢ v23.11.0 via 🍞 v1.2.17 via 🦀 1.88.0 on 🐳 v28.2.2 (orbstack) took 5.4s
➜ cargo run --example verify_vote
warning: profiles for the non root package will be ignored, specify profiles at the workspace root:
package: /Users/qiaopengjun/Code/Solana/voting/voting-substreams/voting_substreams/Cargo.toml
workspace: /Users/qiaopengjun/Code/Solana/voting/Cargo.toml
warning: use of deprecated trait `solana_program_error::PrintProgramError`: Use `ToStr` instead with `solana_msg::msg!` or any other logging
--> generated/rs/voting/errors/voting.rs:30:28
|
30 | impl solana_program_error::PrintProgramError for VotingError {
| ^^^^^^^^^^^^^^^^^
|
= note: `#[warn(deprecated)]` on by default
warning: use of deprecated trait `solana_decode_error::DecodeError`: Use `num_traits::FromPrimitive` instead
--> generated/rs/voting/errors/voting.rs:36:30
|
36 | impl<T> solana_decode_error::DecodeError<T> for VotingError {
| ^^^^^^^^^^^
warning: `voting_client` (lib) generated 2 warnings
Compiling rust-client v0.1.0 (/Users/qiaopengjun/Code/Solana/voting/rust-client)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.35s
Running `/Users/qiaopengjun/Code/Solana/voting/target/debug/examples/verify_vote`
--- 🚀 Starting [Step 4: Verify Vote] Rust Client ---
🔍 Checking candidate account: D2dmKitcUCCcDYEsd1vT67rzF11xTV1kDZwJoXhRnet1
✅ Verification Successful!
- Candidate Name: "Candidate #1"
- Vote Count: 0
🤔 Hmm, the vote count is still 0. Something might be wrong.
您看到的 Vote Count: 0
这个结果,正是因为我们之前尝试运行 vote
脚本时,交易因为投票已过期而被合约正确地拒绝了。
所以,虽然 verify_vote
脚本的输出看起来像是一个问题(“Hmm, something might be wrong.”),但它实际上是一次成功的验证。它证明了:
- 您的
vote
脚本因为投票过期而失败了。 - 因此,候选人的票数没有被增加。
- 您的
verify_vote
脚本正确地从链上读取并报告了这个事实。
这整个流程完美地展示了您合约的时间锁功能是正常工作的。
第十四步:编写完整的集成测试脚本 (full_test.rs
)
在 rust-client/examples/
目录下,创建一个名为 full_test.rs
的新文件。
文件路径: rust-client/examples/full_test.rs
文件内容:
use anyhow::Result;
use chrono::Utc;
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
pubkey::Pubkey,
signature::{Keypair, Signer},
transaction::Transaction,
};
use std::convert::TryFrom;
use std::{env, fs};
// 导入所有需要的生成代码
use voting_client::{
accounts::{CandidateAccount, PollAccount},
instructions::{AddCandidateBuilder, InitializePollBuilder, VoteBuilder},
programs::VOTING_ID,
};
/// 从文件加载钱包 Keypair
fn load_wallet(path: &str) -> Result<Keypair> {
let content = fs::read_to_string(path)?;
let bytes: Vec<u8> = serde_json::from_str(&content)?;
Ok(Keypair::try_from(bytes.as_slice())?)
}
#[tokio::main]
async fn main() -> Result<()> {
println!("--- 🚀 Starting Full Integration Test ---");
// --- 设置 ---
dotenvy::from_path("../.env").ok();
let rpc_url =
env::var("RPC_URL").unwrap_or_else(|_| "https://api.devnet.solana.com".to_string());
let wallet_path = env::var("WALLET_PATH").unwrap_or_else(|_| {
let home = env::var("HOME").expect("HOME not set");
format!("{}/.config/solana/id.json", home)
});
let client = RpcClient::new(rpc_url);
let signer = load_wallet(&wallet_path)?;
println!("🔑 Signer Wallet: {}", signer.pubkey());
// --- 步骤 1: 初始化投票 ---
let poll_account = Keypair::new();
let init_instruction = InitializePollBuilder::new()
.signer(signer.pubkey())
.poll_account(poll_account.pubkey())
.name("Full Integration Test Poll".to_string())
.description("Automated test poll.".to_string())
.start_time((Utc::now().timestamp() - 60) as u64)
.end_time((Utc::now().timestamp() + 3600) as u64)
.instruction();
let recent_blockhash = client.get_latest_blockhash()?;
let init_tx = Transaction::new_signed_with_payer(
&[init_instruction],
Some(&signer.pubkey()),
&[&signer, &poll_account],
recent_blockhash,
);
let init_sig = client.send_and_confirm_transaction(&init_tx)?;
println!(
"\n[✅ Step 1 SUCCESS] Poll initialized. Signature: {}",
init_sig
);
println!(" Poll Account: {}", poll_account.pubkey());
// --- 步骤 2: 添加候选人 ---
let poll_account_info = client.get_account(&poll_account.pubkey())?;
let poll_account_data = PollAccount::from_bytes(&poll_account_info.data)?;
let (candidate_pda, _) = Pubkey::find_program_address(
&[
b"candidate",
&poll_account.pubkey().to_bytes(),
&[poll_account_data.candidate_count],
],
&VOTING_ID,
);
let add_cand_instruction = AddCandidateBuilder::new()
.signer(signer.pubkey())
.poll_account(poll_account.pubkey())
.candidate_account(candidate_pda)
.candidate_name("Candidate A".to_string())
.instruction();
let recent_blockhash = client.get_latest_blockhash()?;
let add_cand_tx = Transaction::new_signed_with_payer(
&[add_cand_instruction],
Some(&signer.pubkey()),
&[&signer],
recent_blockhash,
);
let add_cand_sig = client.send_and_confirm_transaction(&add_cand_tx)?;
println!(
"\n[✅ Step 2 SUCCESS] Candidate added. Signature: {}",
add_cand_sig
);
println!(" Candidate Account: {}", candidate_pda);
// --- 步骤 3: 投票 ---
let (receipt_pda, _) = Pubkey::find_program_address(
&[
b"receipt",
&poll_account.pubkey().to_bytes(),
&signer.pubkey().to_bytes(),
],
&VOTING_ID,
);
let vote_instruction = VoteBuilder::new()
.signer(signer.pubkey())
.poll_account(poll_account.pubkey())
.candidate_account(candidate_pda)
.voter_receipt(receipt_pda)
.instruction();
let recent_blockhash = client.get_latest_blockhash()?;
let vote_tx = Transaction::new_signed_with_payer(
&[vote_instruction],
Some(&signer.pubkey()),
&[&signer],
recent_blockhash,
);
let vote_sig = client.send_and_confirm_transaction(&vote_tx)?;
println!("\n[✅ Step 3 SUCCESS] Vote cast. Signature: {}", vote_sig);
// --- 步骤 4: 验证结果 ---
let candidate_info = client.get_account(&candidate_pda)?;
let candidate_data = CandidateAccount::from_bytes(&candidate_info.data)?;
println!("\n[✅ Step 4 SUCCESS] Verification complete.");
println!(
" Candidate \"{}\" has {} vote(s).",
candidate_data.name, candidate_data.votes
);
if candidate_data.votes == 1 {
println!("\n🎉🎉🎉 INTEGRATION TEST PASSED! 🎉🎉🎉");
} else {
anyhow::bail!(
"Verification failed! Expected 1 vote, but found {}.",
candidate_data.votes
);
}
Ok(())
}
第十五步:运行您的集成测试
现在,您可以在 rust-client
目录下,一键运行完整的测试流程:
voting/rust-client on main [!?] is 📦 0.1.0 via ⬢ v23.11.0 via 🍞 v1.2.17 via 🦀 1.88.0 on 🐳 v28.2.2 (orbstack) took 6.2s
➜ cargo run --example full_test
warning: profiles for the non root package will be ignored, specify profiles at the workspace root:
package: /Users/qiaopengjun/Code/Solana/voting/voting-substreams/voting_substreams/Cargo.toml
workspace: /Users/qiaopengjun/Code/Solana/voting/Cargo.toml
warning: use of deprecated trait `solana_program_error::PrintProgramError`: Use `ToStr` instead with `solana_msg::msg!` or any other logging
--> generated/rs/voting/errors/voting.rs:30:28
|
30 | impl solana_program_error::PrintProgramError for VotingError {
| ^^^^^^^^^^^^^^^^^
|
= note: `#[warn(deprecated)]` on by default
warning: use of deprecated trait `solana_decode_error::DecodeError`: Use `num_traits::FromPrimitive` instead
--> generated/rs/voting/errors/voting.rs:36:30
|
36 | impl<T> solana_decode_error::DecodeError<T> for VotingError {
| ^^^^^^^^^^^
warning: `voting_client` (lib) generated 2 warnings
Compiling rust-client v0.1.0 (/Users/qiaopengjun/Code/Solana/voting/rust-client)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.32s
Running `/Users/qiaopengjun/Code/Solana/voting/target/debug/examples/full_test`
--- 🚀 Starting Full Integration Test ---
🔑 Signer Wallet: 6MZDRo5v8K2NfdohdD76QNpSgk3GH3Aup53BeMaRAEpd
[✅ Step 1 SUCCESS] Poll initialized. Signature: 4t9Kh4wdXeXA8jYSpXPVc3nLKGS5BpM8Ew2AroYrUzsicDnp8KkKUMfbYGiRHu41Dj78bpRRbpaou4fj1oCHrYWP
Poll Account: 8WYpqpKudVFmNHvbM29RihQ1ZMfe13k6yadffV1XVFDe
[✅ Step 2 SUCCESS] Candidate added. Signature: 3ZMEBxUtvzF8GwxE3U3Nbac48q9gxDgq6RkWy6GPsRZxvoF7oeQXTF6BagSwEcK2s1TZvbkhNkHGd7yM1mbMXHaW
Candidate Account: 6QJvBy2u5Aj43U5Apnksn2HLiAcS61bZz8SqLGTnDgJq
[✅ Step 3 SUCCESS] Vote cast. Signature: 9eqAALDsWBLZ2xnz2T4yFVK4iMEbrgLUqAtWM9XTVSoShUUweN3KdbXdAR8tfZU8S1YSZd48kqgi4CnssqF4Fhg
[✅ Step 4 SUCCESS] Verification complete.
Candidate "Candidate A" has 1 vote(s).
🎉🎉🎉 INTEGRATION TEST PASSED! 🎉🎉🎉
巨大成功!
您运行的 full_test
自动化集成测试完美地通过了!
您看到的日志是最终的、最全面的成功证明:
[✅ Step 1 SUCCESS] Poll initialized.
: 成功创建投票。[✅ Step 2 SUCCESS] Candidate added.
: 成功添加候选人。[✅ Step 3 SUCCESS] Vote cast.
: 成功投票。[✅ Step 4 SUCCESS] Verification complete.
: 成功验证结果。Candidate "Candidate A" has 1 vote(s).
: 验证结果正确,票数是 1。🎉🎉🎉 INTEGRATION TEST PASSED! 🎉🎉🎉
: 脚本最终确认整个流程无误。
恭喜您!您不仅成功地用 Rust 客户端完整地测试了合约的所有核心功能,还将其整合成了一个专业、可重复的自动化测试脚本。这是一个非常了不起的成就,标志着您已经完全掌握了使用 Rust 与 Solana 合约交互的整个流程。
总结
恭喜您走完了这段充满挑战但收获颇丰的 Rust 客户端开发之旅!从 cargo new
开始,我们不仅仅是简单地调用了合约指令,更重要的是,我们亲身经历并解决了 Rust 与 Solana 生态结合时最常见的痛点——依赖管理。从 zeroize
的版本冲突,到 solana-program
的重复定义,再到 codama
生成代码的内部依赖配置,每一步调试都是一次宝贵的实战经验。
通过将生成的代码封装为独立的库、将测试脚本组织到 examples
目录,我们最终构建了一个结构清晰、可维护性强的客户端项目。最后的 full_test.rs
自动化集成脚本,更是为我们 DApp 的健壮性提供了坚实的保障。
现在,您已经完全掌握了使用 TypeScript 和 Rust 这两种主流语言与 Anchor 合约进行端到端交互的核心能力。这为您未来无论是构建前端应用、后端服务,还是复杂的链上机器人,都打下了最坚实的基础。
参考
- https://explorer.solana.com/tx/4iUtG5WYj8R4AJyZjUAKw1PwEQtDy99DEGSJQcUMEEANJ8gcS1YSN12Q1fhgerX1ktHuU6atSsKYzXF1wMk2VSn6?cluster=devnet
- https://solana.com/zh/docs
- https://beta.solpg.io/
- https://solanacookbook.com/zh/
- https://github.com/codama-idl/codama
- https://www.anchor-lang.com/docs
- https://soldev.cn/
- https://explorer.solana.com/tx/619YKJtskQ9nkR971NxJpyfeiCD7tN4nGVcYdYWTPaJ9iLUkw49F4BQaTuHghMjKTwKbConxJ84XTpCBMCheptZu?cluster=devnet