Rustプログラム構造

Rustで書かれたSolanaプログラムは、コードの構成方法に柔軟性を持たせるため、構造的な要件は最小限に抑えられています。唯一の要件は、プログラムの実行が開始される場所を定義するentrypointを持つことです。

プログラム構造

ファイル構造に厳格なルールはありませんが、Solanaプログラムは通常、次のような共通のパターンに従います:

  • entrypoint.rs:受信したinstructionsをルーティングするエントリーポイントを定義します。
  • state.rs:プログラムの状態(アカウントデータ)を定義します。
  • instructions.rs:プログラムが実行できるinstructionsを定義します。
  • processor.rs:各instructionのビジネスロジックを実装するinstruction処理関数を定義します。
  • error.rs:プログラムが返すことができるカスタムエラーを定義します。

例として、トークンプログラムをご覧ください。

サンプルプログラム

複数のinstructionsを持つネイティブRustプログラムの構築方法を示すために、2つのinstructionsを実装する簡単なカウンタープログラムを見ていきましょう:

  1. InitializeCounter:初期値で新しいアカウントを作成し初期化します。
  2. IncrementCounter:既存のアカウントに格納されている値をインクリメントします。

簡略化のため、このプログラムは単一のlib.rsファイルで実装されますが、実際には大きなプログラムを複数のファイルに分割することをお勧めします。

パート1:プログラムの作成

まずはカウンタープログラムの構築から始めましょう。開始値でカウンターを初期化し、それをインクリメントできるプログラムを作成します。

新しいプログラムを作成する

まず、Solanaプログラム用の新しいRustプロジェクトを作成しましょう。

Terminal
$
cargo new counter_program --lib
$
cd counter_program

デフォルトのsrc/lib.rsCargo.tomlファイルが表示されるはずです。

Cargo.tomleditionフィールドを2021に更新してください。そうしないと、プログラムをビルドする際にエラーが発生する可能性があります。

依存関係の追加

次に、Solanaプログラムを構築するために必要な依存関係を追加しましょう。コアSDK用のsolana-programとシリアライゼーション用のborshが必要です。

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

Borshを使用する必要はありません。ただし、Solanaプログラムでは一般的に使用されるシリアライゼーションライブラリです。

crate-typeの設定

Solanaプログラムは動的ライブラリとしてコンパイルする必要があります。Cargoがプログラムをどのようにビルドするかを設定するために[lib]セクションを追加します。

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

この設定を含めないと、プログラムをビルドしたときにtarget/deployディレクトリが生成されません。

プログラムエントリポイントの設定

すべてのSolanaプログラムにはエントリポイントがあり、これはプログラムが呼び出されたときに実行される関数です。プログラムに必要なインポートを追加し、エントリポイントを設定することから始めましょう。

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,
}

instructionの列挙型を定義する

プログラムが実行できるinstructionsを定義しましょう。各バリアントが異なるinstructionを表す列挙型を使用します。

以下のコードをlib.rsに追加してください:

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

instructionのデシリアライズを実装する

次に、instruction_data(生のバイト)をCounterInstruction列挙型のバリアントのいずれかにデシリアライズする必要があります。Borshのtry_from_sliceメソッドがこの変換を自動的に処理します。

Borshデシリアライゼーションを使用するためにprocess_instruction関数を更新してください:

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(())
}

instructionsをハンドラーにルーティングする

次に、メインのprocess_instruction関数を更新して、instructionsを適切なハンドラー関数にルーティングしましょう。

このルーティングパターンはSolanaプログラムでは一般的です。instruction_dataはinstructionを表す列挙型のバリアントにデシリアライズされ、その後適切なハンドラー関数が呼び出されます。各ハンドラー関数にはそのinstructionの実装が含まれています。

以下のコードをlib.rsに追加して、process_instruction関数を更新し、InitializeCounterIncrementCounterinstructionsのハンドラーを追加してください:

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(())
}

初期化ハンドラーを実装する

新しいカウンターアカウントを作成して初期化するハンドラーを実装しましょう。Solanaではシステムプログラムだけがアカウントを作成できるため、Cross Program Invocation(CPI)を使用します。これは本質的に私たちのプログラムから別のプログラムを呼び出すことです。

私たちのプログラムはCPIを使用してシステムプログラムのcreate_accountinstructionを呼び出します。新しいアカウントは私たちのプログラムを所有者として作成され、プログラムにアカウントへの書き込みとデータの初期化能力を与えます。

以下のコードを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プログラムを構築しました:

  • エントリーポイント:プログラム実行が開始される場所を定義し、すべての着信リクエストを適切なinstruction handlerにルーティングします
  • Instruction処理:instructionsとそれに関連するハンドラー関数を定義します
  • 状態管理:アカウントデータ構造を定義し、プログラム所有アカウントでその状態を管理します
  • Cross Program Invocation (CPI):System Programを呼び出して新しいプログラム所有アカウントを作成します

次のステップは、すべてが正しく動作することを確認するためにプログラムをテストすることです。

新しいプログラムを作成する

まず、Solanaプログラム用の新しいRustプロジェクトを作成しましょう。

Terminal
$
cargo new counter_program --lib
$
cd counter_program

デフォルトのsrc/lib.rsCargo.tomlファイルが表示されるはずです。

Cargo.tomleditionフィールドを2021に更新してください。そうしないと、プログラムをビルドする際にエラーが発生する可能性があります。

依存関係の追加

次に、Solanaプログラムを構築するために必要な依存関係を追加しましょう。コアSDK用のsolana-programとシリアライゼーション用のborshが必要です。

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

Borshを使用する必要はありません。ただし、Solanaプログラムでは一般的に使用されるシリアライゼーションライブラリです。

crate-typeの設定

Solanaプログラムは動的ライブラリとしてコンパイルする必要があります。Cargoがプログラムをどのようにビルドするかを設定するために[lib]セクションを追加します。

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

この設定を含めないと、プログラムをビルドしたときにtarget/deployディレクトリが生成されません。

プログラムエントリポイントの設定

すべてのSolanaプログラムにはエントリポイントがあり、これはプログラムが呼び出されたときに実行される関数です。プログラムに必要なインポートを追加し、エントリポイントを設定することから始めましょう。

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,
}

instructionの列挙型を定義する

プログラムが実行できるinstructionsを定義しましょう。各バリアントが異なるinstructionを表す列挙型を使用します。

以下のコードをlib.rsに追加してください:

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

instructionのデシリアライズを実装する

次に、instruction_data(生のバイト)をCounterInstruction列挙型のバリアントのいずれかにデシリアライズする必要があります。Borshのtry_from_sliceメソッドがこの変換を自動的に処理します。

Borshデシリアライゼーションを使用するためにprocess_instruction関数を更新してください:

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(())
}

instructionsをハンドラーにルーティングする

次に、メインのprocess_instruction関数を更新して、instructionsを適切なハンドラー関数にルーティングしましょう。

このルーティングパターンはSolanaプログラムでは一般的です。instruction_dataはinstructionを表す列挙型のバリアントにデシリアライズされ、その後適切なハンドラー関数が呼び出されます。各ハンドラー関数にはそのinstructionの実装が含まれています。

以下のコードをlib.rsに追加して、process_instruction関数を更新し、InitializeCounterIncrementCounterinstructionsのハンドラーを追加してください:

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(())
}

初期化ハンドラーを実装する

新しいカウンターアカウントを作成して初期化するハンドラーを実装しましょう。Solanaではシステムプログラムだけがアカウントを作成できるため、Cross Program Invocation(CPI)を使用します。これは本質的に私たちのプログラムから別のプログラムを呼び出すことです。

私たちのプログラムはCPIを使用してシステムプログラムのcreate_accountinstructionを呼び出します。新しいアカウントは私たちのプログラムを所有者として作成され、プログラムにアカウントへの書き込みとデータの初期化能力を与えます。

以下のコードを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プログラムを構築しました:

  • エントリーポイント:プログラム実行が開始される場所を定義し、すべての着信リクエストを適切なinstruction handlerにルーティングします
  • Instruction処理:instructionsとそれに関連するハンドラー関数を定義します
  • 状態管理:アカウントデータ構造を定義し、プログラム所有アカウントでその状態を管理します
  • 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.tomledition2021に設定されていることを確認してください。

ビルド後、プログラムを読み込むことができます。

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ファイルを生成する必要があります。テストはコンパイルされたプログラムを読み込みます。

初期化instructionのテスト

初期化instructionをテストするために、初期値を持つ新しいカウンターアカウントを作成しましょう。

以下のコードを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);

インクリメントinstructionのテスト

次に、インクリメントinstructionをテストして、カウンター値が正しく更新されることを確認しましょう。

以下のコードを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.tomledition2021に設定されていることを確認してください。

ビルド後、プログラムを読み込むことができます。

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ファイルを生成する必要があります。テストはコンパイルされたプログラムを読み込みます。

初期化instructionのテスト

初期化instructionをテストするために、初期値を持つ新しいカウンターアカウントを作成しましょう。

以下のコードを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);

インクリメントinstructionのテスト

次に、インクリメントinstructionをテストして、カウンター値が正しく更新されることを確認しましょう。

以下のコードを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/に2つの重要なファイルを生成します:

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

プログラムIDを使用してsolana program showコマンドでデプロイを確認できます:

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:8899solana-test-validatorが実行されているアドレス)に設定されています。

Is this page helpful?