Rust 程序结构

用 Rust 编写的 Solana 程序对结构的要求非常少,因此代码组织方式非常灵活。唯一的要求是,程序必须有一个 entrypoint,它定义了程序执行的起点。

程序结构

虽然没有严格的文件结构规定,但 Solana 程序通常遵循一个通用模式:

  • entrypoint.rs:定义路由传入指令的入口点。
  • state.rs:定义程序状态(账户数据)。
  • instructions.rs:定义程序可执行的指令。
  • processor.rs:定义实现每个指令业务逻辑的指令处理函数。
  • error.rs:定义程序可返回的自定义错误。

例如,请参见 Token Program

示例程序

为了演示如何构建包含多个指令的原生 Rust 程序,我们将以一个简单的计数器程序为例,介绍实现两个指令的过程:

  1. InitializeCounter:创建并初始化一个带有初始值的新账户。
  2. IncrementCounter:对现有账户中存储的值进行递增。

为简化起见,程序将全部实现于一个 lib.rs 文件中,但在实际开发中,你可能会将较大的程序拆分为多个文件。

第 1 部分:编写程序

我们先从构建计数器程序开始。我们将创建一个可以用初始值初始化计数器并递增计数的程序。

创建新程序

首先,让我们为 Solana 程序创建一个新的 Rust 项目。

Terminal
$
cargo new counter_program --lib
$
cd counter_program

你应该会看到默认的 src/lib.rsCargo.toml 文件。

请将 edition 字段在 Cargo.toml 文件中更新为 2021。否则,在构建程序时可能会遇到错误。

添加依赖项

现在让我们添加构建 Solana 程序所需的依赖项。我们需要用于核心 SDK 的 solana-program,以及用于序列化的 borsh

Terminal
$
cargo add solana-program@2.2.0
$
cargo add borsh

并没有强制要求使用 Borsh。不过,它是 Solana 程序中常用的序列化库。

配置 crate-type

Solana 程序必须编译为动态库。请添加 [lib] 部分,以配置 Cargo 如何构建该程序。

Cargo.toml
[lib]
crate-type = ["cdylib", "lib"]

如果没有包含此配置,构建程序时将不会生成 target/deploy 目录。

设置程序入口

每个 Solana 程序都有一个入口点,即在程序被调用时执行的函数。我们先添加程序所需的 import,并设置入口点。

将以下代码添加到 lib.rs

lib.rs
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
sysvar::{rent::Rent, Sysvar},
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
Ok(())
}

entrypoint 宏会将 input 数据反序列化为 process_instruction 函数的参数。

Solana 程序的 entrypoint 具有如下函数签名。开发者可以自由实现自己的 entrypoint 函数。

#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;

定义程序状态

现在我们来定义将存储在计数器账户中的数据结构。这些数据将被存储在账户的 data 字段中。

将以下代码添加到 lib.rs

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
pub count: u64,
}

定义指令枚举

现在我们来定义程序可以执行的指令。我们将使用一个枚举,其中每个变体代表一个不同的指令。

将以下代码添加到 lib.rs

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 },
IncrementCounter,
}

实现指令反序列化

现在我们需要将 instruction_data(原始字节)反序列化为我们的 CounterInstruction 枚举的某个变体。Borsh 的 try_from_slice 方法会自动处理这个转换。

更新 process_instruction 函数,使用 Borsh 反序列化:

lib.rs
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instruction = CounterInstruction::try_from_slice(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
Ok(())
}

路由指令到处理器

现在我们来更新主 process_instruction 函数,将指令路由到对应的处理函数。

这种路由模式在 Solana 程序中很常见。instruction_data 会被反序列化为表示指令的枚举变体,然后调用相应的处理函数。每个处理函数都包含该指令的具体实现。

将以下代码添加到 lib.rs,更新 process_instruction 函数,并添加 InitializeCounterIncrementCounter 指令的处理器:

lib.rs
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instruction = CounterInstruction::try_from_slice(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
match instruction {
CounterInstruction::InitializeCounter { initial_value } => {
process_initialize_counter(program_id, accounts, initial_value)?
}
CounterInstruction::IncrementCounter => {
process_increment_counter(program_id, accounts)?
}
};
Ok(())
}
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
Ok(())
}
fn process_increment_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
Ok(())
}

实现初始化处理器

现在我们来实现用于创建和初始化新计数器账户的处理器。由于只有 System Program 可以在 Solana 上创建账户,我们将使用跨程序调用(CPI),也就是从我们的程序中调用另一个程序。

我们的程序通过 CPI 调用 System Program 的 create_account 指令。新账户由我们的程序作为 owner 创建,这样我们的程序就可以写入该账户并初始化数据。

将以下代码添加到 lib.rs 中,更新 process_initialize_counter 函数:

lib.rs
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
let payer_account = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
let account_space = 8;
let rent = Rent::get()?;
let required_lamports = rent.minimum_balance(account_space);
invoke(
&system_instruction::create_account(
payer_account.key,
counter_account.key,
required_lamports,
account_space as u64,
program_id,
),
&[
payer_account.clone(),
counter_account.clone(),
system_program.clone(),
],
)?;
let counter_data = CounterAccount {
count: initial_value,
};
let mut account_data = &mut counter_account.data.borrow_mut()[..];
counter_data.serialize(&mut account_data)?;
msg!("Counter initialized with value: {}", initial_value);
Ok(())
}

此指令仅用于演示目的。它未包含生产环境程序所需的安全性和校验。

实现增量处理器

现在让我们实现一个处理器,用于递增已有计数器。此指令:

  • 读取账户 data 字段中的 counter_account
  • 将其反序列化为 CounterAccount 结构体
  • count 字段加 1
  • CounterAccount 结构体重新序列化回账户的 data 字段

将以下代码添加到 lib.rs 中,更新 process_increment_counter 函数:

lib.rs
fn process_increment_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
if counter_account.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
let mut data = counter_account.data.borrow_mut();
let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;
counter_data.count = counter_data
.count
.checked_add(1)
.ok_or(ProgramError::InvalidAccountData)?;
counter_data.serialize(&mut &mut data[..])?;
msg!("Counter incremented to: {}", counter_data.count);
Ok(())
}

此指令仅用于演示目的。它未包含生产环境程序所需的安全性和校验。

程序已完成

恭喜!你已经构建了一个完整的 Solana 程序,演示了所有 Solana 程序共有的基本结构:

  • Entrypoint:定义程序执行的起点,并将所有传入请求路由到相应的指令处理器
  • Instruction Handling:定义指令及其对应的处理函数
  • State Management:定义账户数据结构并管理其在程序拥有账户中的状态
  • Cross Program Invocation (CPI):调用 System Program 创建新的程序拥有账户

下一步是测试程序,以确保一切正常运行。

创建新程序

首先,让我们为 Solana 程序创建一个新的 Rust 项目。

Terminal
$
cargo new counter_program --lib
$
cd counter_program

你应该会看到默认的 src/lib.rsCargo.toml 文件。

请将 edition 字段在 Cargo.toml 文件中更新为 2021。否则,在构建程序时可能会遇到错误。

添加依赖项

现在让我们添加构建 Solana 程序所需的依赖项。我们需要用于核心 SDK 的 solana-program,以及用于序列化的 borsh

Terminal
$
cargo add solana-program@2.2.0
$
cargo add borsh

并没有强制要求使用 Borsh。不过,它是 Solana 程序中常用的序列化库。

配置 crate-type

Solana 程序必须编译为动态库。请添加 [lib] 部分,以配置 Cargo 如何构建该程序。

Cargo.toml
[lib]
crate-type = ["cdylib", "lib"]

如果没有包含此配置,构建程序时将不会生成 target/deploy 目录。

设置程序入口

每个 Solana 程序都有一个入口点,即在程序被调用时执行的函数。我们先添加程序所需的 import,并设置入口点。

将以下代码添加到 lib.rs

lib.rs
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
msg,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
sysvar::{rent::Rent, Sysvar},
};
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
Ok(())
}

entrypoint 宏会将 input 数据反序列化为 process_instruction 函数的参数。

Solana 程序的 entrypoint 具有如下函数签名。开发者可以自由实现自己的 entrypoint 函数。

#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;

定义程序状态

现在我们来定义将存储在计数器账户中的数据结构。这些数据将被存储在账户的 data 字段中。

将以下代码添加到 lib.rs

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
pub count: u64,
}

定义指令枚举

现在我们来定义程序可以执行的指令。我们将使用一个枚举,其中每个变体代表一个不同的指令。

将以下代码添加到 lib.rs

lib.rs
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 },
IncrementCounter,
}

实现指令反序列化

现在我们需要将 instruction_data(原始字节)反序列化为我们的 CounterInstruction 枚举的某个变体。Borsh 的 try_from_slice 方法会自动处理这个转换。

更新 process_instruction 函数,使用 Borsh 反序列化:

lib.rs
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instruction = CounterInstruction::try_from_slice(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
Ok(())
}

路由指令到处理器

现在我们来更新主 process_instruction 函数,将指令路由到对应的处理函数。

这种路由模式在 Solana 程序中很常见。instruction_data 会被反序列化为表示指令的枚举变体,然后调用相应的处理函数。每个处理函数都包含该指令的具体实现。

将以下代码添加到 lib.rs,更新 process_instruction 函数,并添加 InitializeCounterIncrementCounter 指令的处理器:

lib.rs
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let instruction = CounterInstruction::try_from_slice(instruction_data)
.map_err(|_| ProgramError::InvalidInstructionData)?;
match instruction {
CounterInstruction::InitializeCounter { initial_value } => {
process_initialize_counter(program_id, accounts, initial_value)?
}
CounterInstruction::IncrementCounter => {
process_increment_counter(program_id, accounts)?
}
};
Ok(())
}
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
Ok(())
}
fn process_increment_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
Ok(())
}

实现初始化处理器

现在我们来实现用于创建和初始化新计数器账户的处理器。由于只有 System Program 可以在 Solana 上创建账户,我们将使用跨程序调用(CPI),也就是从我们的程序中调用另一个程序。

我们的程序通过 CPI 调用 System Program 的 create_account 指令。新账户由我们的程序作为 owner 创建,这样我们的程序就可以写入该账户并初始化数据。

将以下代码添加到 lib.rs 中,更新 process_initialize_counter 函数:

lib.rs
fn process_initialize_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
initial_value: u64,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
let payer_account = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;
let account_space = 8;
let rent = Rent::get()?;
let required_lamports = rent.minimum_balance(account_space);
invoke(
&system_instruction::create_account(
payer_account.key,
counter_account.key,
required_lamports,
account_space as u64,
program_id,
),
&[
payer_account.clone(),
counter_account.clone(),
system_program.clone(),
],
)?;
let counter_data = CounterAccount {
count: initial_value,
};
let mut account_data = &mut counter_account.data.borrow_mut()[..];
counter_data.serialize(&mut account_data)?;
msg!("Counter initialized with value: {}", initial_value);
Ok(())
}

此指令仅用于演示目的。它未包含生产环境程序所需的安全性和校验。

实现增量处理器

现在让我们实现一个处理器,用于递增已有计数器。此指令:

  • 读取账户 data 字段中的 counter_account
  • 将其反序列化为 CounterAccount 结构体
  • count 字段加 1
  • CounterAccount 结构体重新序列化回账户的 data 字段

将以下代码添加到 lib.rs 中,更新 process_increment_counter 函数:

lib.rs
fn process_increment_counter(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let counter_account = next_account_info(accounts_iter)?;
if counter_account.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
let mut data = counter_account.data.borrow_mut();
let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;
counter_data.count = counter_data
.count
.checked_add(1)
.ok_or(ProgramError::InvalidAccountData)?;
counter_data.serialize(&mut &mut data[..])?;
msg!("Counter incremented to: {}", counter_data.count);
Ok(())
}

此指令仅用于演示目的。它未包含生产环境程序所需的安全性和校验。

程序已完成

恭喜!你已经构建了一个完整的 Solana 程序,演示了所有 Solana 程序共有的基本结构:

  • Entrypoint:定义程序执行的起点,并将所有传入请求路由到相应的指令处理器
  • Instruction Handling:定义指令及其对应的处理函数
  • State Management:定义账户数据结构并管理其在程序拥有账户中的状态
  • Cross Program Invocation (CPI):调用 System Program 创建新的程序拥有账户

下一步是测试程序,以确保一切正常运行。

Cargo.toml
lib.rs
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[dependencies]

第 2 部分:测试程序

现在让我们测试计数器程序。我们将使用 LiteSVM,这是一个测试框架,可以让我们在不部署到集群的情况下测试程序。

添加测试依赖项

首先,让我们添加测试所需的依赖项。我们将使用 litesvm 进行测试,还会用到 solana-sdk

Terminal
$
cargo add litesvm@0.6.1 --dev
$
cargo add solana-sdk@2.2.0 --dev

创建测试模块

现在让我们为程序添加一个测试模块。我们将从基础结构和导入开始。

将以下代码添加到 lib.rs,直接放在程序代码下方:

lib.rs
#[cfg(test)]
mod test {
use super::*;
use litesvm::LiteSVM;
use solana_sdk::{
account::ReadableAccount,
instruction::{AccountMeta, Instruction},
message::Message,
signature::{Keypair, Signer},
system_program,
transaction::Transaction,
};
#[test]
fn test_counter_program() {
// Test implementation will go here
}
}

#[cfg(test)] 属性确保这段代码只会在运行测试时编译。

初始化测试环境

让我们用 LiteSVM 搭建测试环境,并为付款账户充值。

LiteSVM 可以模拟 Solana 运行环境,让我们无需部署到真实集群即可测试程序。

将以下代码添加到 lib.rs,并更新 test_counter_program 函数:

lib.rs
let mut svm = LiteSVM::new();
let payer = Keypair::new();
svm.airdrop(&payer.pubkey(), 1_000_000_000)
.expect("Failed to airdrop");

加载程序

现在我们需要将程序构建并加载到测试环境中。运行 cargo build-sbf 命令来构建程序。这会在 target/deploy 目录下生成 counter_program.so 文件。

Terminal
$
cargo build-sbf

请确保 Cargo.toml 中的 edition 已设置为 2021

构建完成后,我们可以加载该程序。

更新 test_counter_program 函数,将程序加载到测试环境中。

lib.rs
let program_keypair = Keypair::new();
let program_id = program_keypair.pubkey();
svm.add_program_from_file(
program_id,
"target/deploy/counter_program.so"
).expect("Failed to load program");

在运行测试前,必须先运行 cargo build-sbf,以生成 .so 文件。测试会加载已编译的程序。

测试初始化指令

我们通过创建一个具有初始值的新计数器账户来测试初始化指令。

将以下代码添加到 lib.rs 中,更新 test_counter_program 函数:

lib.rs
let counter_keypair = Keypair::new();
let initial_value: u64 = 42;
println!("Testing counter initialization...");
let init_instruction_data =
borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value })
.expect("Failed to serialize instruction");
let initialize_instruction = Instruction::new_with_bytes(
program_id,
&init_instruction_data,
vec![
AccountMeta::new(counter_keypair.pubkey(), true),
AccountMeta::new(payer.pubkey(), true),
AccountMeta::new_readonly(system_program::id(), false),
],
);
let message = Message::new(&[initialize_instruction], Some(&payer.pubkey()));
let transaction = Transaction::new(
&[&payer, &counter_keypair],
message,
svm.latest_blockhash()
);
let result = svm.send_transaction(transaction);
assert!(result.is_ok(), "Initialize transaction should succeed");
let logs = result.unwrap().logs;
println!("Transaction logs:\n{:#?}", logs);

验证初始化

初始化后,我们来验证计数器账户是否已按预期值正确创建。

将以下代码添加到 lib.rs 中,更新 test_counter_program 函数:

lib.rs
let account = svm
.get_account(&counter_keypair.pubkey())
.expect("Failed to get counter account");
let counter: CounterAccount = CounterAccount::try_from_slice(account.data())
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 42);
println!("Counter initialized successfully with value: {}", counter.count);

测试自增指令

现在我们来测试自增指令,确保它能正确更新计数器的值。

将以下代码添加到 lib.rs 中,更新 test_counter_program 函数:

lib.rs
println!("Testing counter increment...");
let increment_instruction_data =
borsh::to_vec(&CounterInstruction::IncrementCounter)
.expect("Failed to serialize instruction");
let increment_instruction = Instruction::new_with_bytes(
program_id,
&increment_instruction_data,
vec![AccountMeta::new(counter_keypair.pubkey(), true)],
);
let message = Message::new(&[increment_instruction], Some(&payer.pubkey()));
let transaction = Transaction::new(
&[&payer, &counter_keypair],
message,
svm.latest_blockhash()
);
let result = svm.send_transaction(transaction);
assert!(result.is_ok(), "Increment transaction should succeed");
let logs = result.unwrap().logs;
println!("Transaction logs:\n{:#?}", logs);

验证最终结果

最后,我们通过检查更新后的计数器值,验证自增操作是否正确。

将以下代码添加到 lib.rs 中,更新 test_counter_program 函数:

lib.rs
let account = svm
.get_account(&counter_keypair.pubkey())
.expect("Failed to get counter account");
let counter: CounterAccount = CounterAccount::try_from_slice(account.data())
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 43);
println!("Counter incremented successfully to: {}", counter.count);

使用以下命令运行测试。--nocapture 标志会打印测试输出。

Terminal
$
cargo test -- --nocapture

预期输出:

Testing counter initialization...
Transaction logs:
[
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program log: Counter initialized with value: 42",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 3803 of 200000 compute units",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",
]
Counter initialized successfully with value: 42
Testing counter increment...
Transaction logs:
[
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]",
"Program log: Counter incremented to: 43",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 762 of 200000 compute units",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",
]
Counter incremented successfully to: 43

添加测试依赖项

首先,让我们添加测试所需的依赖项。我们将使用 litesvm 进行测试,还会用到 solana-sdk

Terminal
$
cargo add litesvm@0.6.1 --dev
$
cargo add solana-sdk@2.2.0 --dev

创建测试模块

现在让我们为程序添加一个测试模块。我们将从基础结构和导入开始。

将以下代码添加到 lib.rs,直接放在程序代码下方:

lib.rs
#[cfg(test)]
mod test {
use super::*;
use litesvm::LiteSVM;
use solana_sdk::{
account::ReadableAccount,
instruction::{AccountMeta, Instruction},
message::Message,
signature::{Keypair, Signer},
system_program,
transaction::Transaction,
};
#[test]
fn test_counter_program() {
// Test implementation will go here
}
}

#[cfg(test)] 属性确保这段代码只会在运行测试时编译。

初始化测试环境

让我们用 LiteSVM 搭建测试环境,并为付款账户充值。

LiteSVM 可以模拟 Solana 运行环境,让我们无需部署到真实集群即可测试程序。

将以下代码添加到 lib.rs,并更新 test_counter_program 函数:

lib.rs
let mut svm = LiteSVM::new();
let payer = Keypair::new();
svm.airdrop(&payer.pubkey(), 1_000_000_000)
.expect("Failed to airdrop");

加载程序

现在我们需要将程序构建并加载到测试环境中。运行 cargo build-sbf 命令来构建程序。这会在 target/deploy 目录下生成 counter_program.so 文件。

Terminal
$
cargo build-sbf

请确保 Cargo.toml 中的 edition 已设置为 2021

构建完成后,我们可以加载该程序。

更新 test_counter_program 函数,将程序加载到测试环境中。

lib.rs
let program_keypair = Keypair::new();
let program_id = program_keypair.pubkey();
svm.add_program_from_file(
program_id,
"target/deploy/counter_program.so"
).expect("Failed to load program");

在运行测试前,必须先运行 cargo build-sbf,以生成 .so 文件。测试会加载已编译的程序。

测试初始化指令

我们通过创建一个具有初始值的新计数器账户来测试初始化指令。

将以下代码添加到 lib.rs 中,更新 test_counter_program 函数:

lib.rs
let counter_keypair = Keypair::new();
let initial_value: u64 = 42;
println!("Testing counter initialization...");
let init_instruction_data =
borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value })
.expect("Failed to serialize instruction");
let initialize_instruction = Instruction::new_with_bytes(
program_id,
&init_instruction_data,
vec![
AccountMeta::new(counter_keypair.pubkey(), true),
AccountMeta::new(payer.pubkey(), true),
AccountMeta::new_readonly(system_program::id(), false),
],
);
let message = Message::new(&[initialize_instruction], Some(&payer.pubkey()));
let transaction = Transaction::new(
&[&payer, &counter_keypair],
message,
svm.latest_blockhash()
);
let result = svm.send_transaction(transaction);
assert!(result.is_ok(), "Initialize transaction should succeed");
let logs = result.unwrap().logs;
println!("Transaction logs:\n{:#?}", logs);

验证初始化

初始化后,我们来验证计数器账户是否已按预期值正确创建。

将以下代码添加到 lib.rs 中,更新 test_counter_program 函数:

lib.rs
let account = svm
.get_account(&counter_keypair.pubkey())
.expect("Failed to get counter account");
let counter: CounterAccount = CounterAccount::try_from_slice(account.data())
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 42);
println!("Counter initialized successfully with value: {}", counter.count);

测试自增指令

现在我们来测试自增指令,确保它能正确更新计数器的值。

将以下代码添加到 lib.rs 中,更新 test_counter_program 函数:

lib.rs
println!("Testing counter increment...");
let increment_instruction_data =
borsh::to_vec(&CounterInstruction::IncrementCounter)
.expect("Failed to serialize instruction");
let increment_instruction = Instruction::new_with_bytes(
program_id,
&increment_instruction_data,
vec![AccountMeta::new(counter_keypair.pubkey(), true)],
);
let message = Message::new(&[increment_instruction], Some(&payer.pubkey()));
let transaction = Transaction::new(
&[&payer, &counter_keypair],
message,
svm.latest_blockhash()
);
let result = svm.send_transaction(transaction);
assert!(result.is_ok(), "Increment transaction should succeed");
let logs = result.unwrap().logs;
println!("Transaction logs:\n{:#?}", logs);

验证最终结果

最后,我们通过检查更新后的计数器值,验证自增操作是否正确。

将以下代码添加到 lib.rs 中,更新 test_counter_program 函数:

lib.rs
let account = svm
.get_account(&counter_keypair.pubkey())
.expect("Failed to get counter account");
let counter: CounterAccount = CounterAccount::try_from_slice(account.data())
.expect("Failed to deserialize counter data");
assert_eq!(counter.count, 43);
println!("Counter incremented successfully to: {}", counter.count);

使用以下命令运行测试。--nocapture 标志会打印测试输出。

Terminal
$
cargo test -- --nocapture

预期输出:

Testing counter initialization...
Transaction logs:
[
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]",
"Program 11111111111111111111111111111111 invoke [2]",
"Program 11111111111111111111111111111111 success",
"Program log: Counter initialized with value: 42",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 3803 of 200000 compute units",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",
]
Counter initialized successfully with value: 42
Testing counter increment...
Transaction logs:
[
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq invoke [1]",
"Program log: Counter incremented to: 43",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq consumed 762 of 200000 compute units",
"Program 3QpyHXhFtYY32iY7foF3EjkVdCDrUppADk9aDwSWn6Sq success",
]
Counter incremented successfully to: 43
Cargo.toml
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
borsh = "1.5.7"
solana-program = "2.2.0"
[dev-dependencies]
litesvm = "0.6.1"
solana-sdk = "2.2.0"

第 3 部分:调用程序

现在让我们添加一个客户端脚本来调用该程序。

创建客户端示例

让我们创建一个 Rust 客户端与已部署的程序进行交互。

Terminal
$
mkdir examples
$
touch examples/client.rs

将以下配置添加到 Cargo.toml

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

安装客户端依赖:

Terminal
$
cargo add solana-client@2.2.0 --dev
$
cargo add tokio --dev

实现客户端代码

现在让我们实现客户端,用于调用已部署的程序。

运行以下命令,从 keypair 文件中获取你的程序 ID:

Terminal
$
solana address -k ./target/deploy/counter_program-keypair.json

将客户端代码添加到 examples/client.rs,并用上一步命令的输出替换 program_id

examples/client.rs
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH")
.expect("Invalid program ID");
examples/client.rs
use solana_client::rpc_client::RpcClient;
use solana_sdk::{
commitment_config::CommitmentConfig,
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
signature::{Keypair, Signer},
system_program,
transaction::Transaction,
};
use std::str::FromStr;
use counter_program::CounterInstruction;
#[tokio::main]
async fn main() {
// Replace with your actual program ID from deployment
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH")
.expect("Invalid program ID");
// Connect to local cluster
let rpc_url = String::from("http://localhost:8899");
let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());
// Generate a new keypair for paying fees
let payer = Keypair::new();
// Request airdrop of 1 SOL for transaction fees
println!("Requesting airdrop...");
let airdrop_signature = client
.request_airdrop(&payer.pubkey(), 1_000_000_000)
.expect("Failed to request airdrop");
// Wait for airdrop confirmation
loop {
if client
.confirm_transaction(&airdrop_signature)
.unwrap_or(false)
{
break;
}
std::thread::sleep(std::time::Duration::from_millis(500));
}
println!("Airdrop confirmed");
println!("\nInitializing counter...");
let counter_keypair = Keypair::new();
let initial_value = 100u64;
// Serialize the initialize instruction data
let instruction_data = borsh::to_vec(&CounterInstruction::InitializeCounter { initial_value })
.expect("Failed to serialize instruction");
let initialize_instruction = Instruction::new_with_bytes(
program_id,
&instruction_data,
vec![
AccountMeta::new(counter_keypair.pubkey(), true),
AccountMeta::new(payer.pubkey(), true),
AccountMeta::new_readonly(system_program::id(), false),
],
);
let mut transaction =
Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey()));
let blockhash = client
.get_latest_blockhash()
.expect("Failed to get blockhash");
transaction.sign(&[&payer, &counter_keypair], blockhash);
match client.send_and_confirm_transaction(&transaction) {
Ok(signature) => {
println!("Counter initialized!");
println!("Transaction: {}", signature);
println!("Counter address: {}", counter_keypair.pubkey());
}
Err(err) => {
eprintln!("Failed to initialize counter: {}", err);
return;
}
}
println!("\nIncrementing counter...");
// Serialize the increment instruction data
let increment_data = borsh::to_vec(&CounterInstruction::IncrementCounter)
.expect("Failed to serialize instruction");
let increment_instruction = Instruction::new_with_bytes(
program_id,
&increment_data,
vec![AccountMeta::new(counter_keypair.pubkey(), true)],
);
let mut transaction =
Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey()));
transaction.sign(&[&payer, &counter_keypair], blockhash);
match client.send_and_confirm_transaction(&transaction) {
Ok(signature) => {
println!("Counter incremented!");
println!("Transaction: {}", signature);
}
Err(err) => {
eprintln!("Failed to increment counter: {}", err);
}
}
}

创建客户端示例

让我们创建一个 Rust 客户端与已部署的程序进行交互。

Terminal
$
mkdir examples
$
touch examples/client.rs

将以下配置添加到 Cargo.toml

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

安装客户端依赖:

Terminal
$
cargo add solana-client@2.2.0 --dev
$
cargo add tokio --dev

实现客户端代码

现在让我们实现客户端,用于调用已部署的程序。

运行以下命令,从 keypair 文件中获取你的程序 ID:

Terminal
$
solana address -k ./target/deploy/counter_program-keypair.json

将客户端代码添加到 examples/client.rs,并用上一步命令的输出替换 program_id

examples/client.rs
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH")
.expect("Invalid program ID");
Cargo.toml
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
borsh = "1.5.7"
solana-program = "2.2.0"
[dev-dependencies]
litesvm = "0.6.1"
solana-sdk = "2.2.0"
solana-client = "2.2.0"
tokio = "1.47.1"
[[example]]
name = "client"
path = "examples/client.rs"

第 4 部分:部署程序

现在我们的程序和客户端都已准备好,接下来让我们构建、部署并调用该程序。

构建程序

首先,让我们构建我们的程序。

Terminal
$
cargo build-sbf

此命令会编译你的程序,并在 target/deploy/ 目录下生成两个重要文件:

counter_program.so # The compiled program
counter_program-keypair.json # Keypair for the program ID

你可以通过运行以下命令查看你的程序 ID:

Terminal
$
solana address -k ./target/deploy/counter_program-keypair.json

示例输出:

HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

启动本地 validator

在开发过程中,我们将使用本地测试 validator。

首先,将 Solana CLI 配置为使用 localhost:

Terminal
$
solana config set -ul

示例输出:

Config File: ~/.config/solana/cli/config.yml
RPC URL: http://localhost:8899
WebSocket URL: ws://localhost:8900/ (computed)
Keypair Path: ~/.config/solana/id.json
Commitment: confirmed

现在,在另一个终端中启动测试 validator:

Terminal
$
solana-test-validator

部署程序

在 validator 正在运行时,将你的程序部署到本地集群:

Terminal
$
solana program deploy ./target/deploy/counter_program.so

示例输出:

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Signature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h

你可以使用 solana program show 命令和你的程序 ID 验证部署情况:

Terminal
$
solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

示例输出:

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Owner: BPFLoaderUpgradeab1e11111111111111111111111
ProgramData Address: 47MVf5tRZ4zWXQMX7ydrkgcFQr8XTk1QBjohwsUzaiuM
Authority: 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1
Last Deployed In Slot: 16
Data Length: 82696 (0x14308) bytes
Balance: 0.57676824 SOL

运行客户端

在本地 validator 仍然运行时,执行客户端:

Terminal
$
cargo run --example client

预期输出:

Requesting airdrop...
Airdrop confirmed
Initializing counter...
Counter initialized!
Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14k
Counter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9Zfofcy
Incrementing counter...
Counter incremented!
Transaction: 4qv1Rx6FHu1M3woVgDQ6KtYUaJgBzGcHnhej76ZpaKGCgsTorbcHnPKxoH916UENw7X5ppnQ8PkPnhXxEwrYuUxS

在本地 validator 运行时,你可以通过 Solana Explorer 查看交易,使用输出的交易签名。请注意,Solana Explorer 上的集群必须设置为“Custom RPC URL”,默认是 http://localhost:8899,即 solana-test-validator 正在运行的地址。

Is this page helpful?

Table of Contents

Edit Page

管理者

©️ 2026 Solana 基金会版权所有
取得联系