Rust programmastructuur

Solana-programma's geschreven in Rust hebben minimale structurele vereisten, wat flexibiliteit biedt in hoe code wordt georganiseerd. De enige vereiste is dat een programma een entrypoint moet hebben, die definieert waar de uitvoering van een programma begint.

Programmastructuur

Hoewel er geen strikte regels zijn voor bestandsstructuur, volgen Solana-programma's doorgaans een gemeenschappelijk patroon:

  • entrypoint.rs: Definieert het entrypoint dat binnenkomende instructies routeert.
  • state.rs: Definieert de programmastatus (accountgegevens).
  • instructions.rs: Definieert de instructies die het programma kan uitvoeren.
  • processor.rs: Definieert de instructiehandlers (functies) die de bedrijfslogica voor elke instructie implementeren.
  • error.rs: Definieert aangepaste fouten die het programma kan retourneren.

Zie bijvoorbeeld het Token Program.

Voorbeeldprogramma

Om te demonstreren hoe je een native Rust-programma bouwt met meerdere instructies, doorlopen we een eenvoudig tellerprogramma dat twee instructies implementeert:

  1. InitializeCounter: Creëert en initialiseert een nieuw account met een initiële waarde.
  2. IncrementCounter: Verhoogt de waarde opgeslagen in een bestaand account.

Voor de eenvoud wordt het programma geïmplementeerd in een enkel lib.rs-bestand, hoewel je in de praktijk grotere programma's wellicht in meerdere bestanden wilt splitsen.

Deel 1: Het programma schrijven

Laten we beginnen met het bouwen van het tellerprogramma. We maken een programma dat een teller kan initialiseren met een startwaarde en deze kan verhogen.

Maak een nieuw programma

Laten we eerst een nieuw Rust-project maken voor ons Solana-programma.

Terminal
$
cargo new counter_program --lib
$
cd counter_program

Je zou de standaard src/lib.rs en Cargo.toml bestanden moeten zien.

Werk het edition veld in Cargo.toml bij naar 2021. Anders kun je een foutmelding krijgen bij het bouwen van het programma.

Afhankelijkheden toevoegen

Laten we nu de benodigde afhankelijkheden toevoegen voor het bouwen van een Solana-programma. We hebben solana-program nodig voor de core SDK en borsh voor serialisatie.

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

Het is niet verplicht om Borsh te gebruiken. Het is echter een veelgebruikte serialisatiebibliotheek voor Solana-programma's.

Configureer crate-type

Solana-programma's moeten worden gecompileerd als dynamische bibliotheken. Voeg de [lib] sectie toe om te configureren hoe Cargo het programma bouwt.

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

Als je deze configuratie niet toevoegt, wordt de target/deploy directory niet gegenereerd wanneer je het programma bouwt.

Stel programma entrypoint in

Elk Solana-programma heeft een entrypoint, de functie die wordt aangeroepen wanneer het programma wordt geïnvoceerd. Laten we beginnen met het toevoegen van de imports die we nodig hebben voor het programma en het instellen van het entrypoint.

Voeg de volgende code toe aan 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(())
}

De entrypoint macro verzorgt de deserialisatie van de input data naar de parameters van de process_instruction functie.

Een Solana-programma entrypoint heeft de volgende functiesignatuur. Ontwikkelaars zijn vrij om hun eigen implementatie van de entrypoint functie te maken.

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

Definieer programma state

Laten we nu de datastructuur definiëren die in onze counter accounts wordt opgeslagen. Dit is de data die wordt opgeslagen in het data veld van het account.

Voeg de volgende code toe aan lib.rs:

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

Definieer instruction enum

Laten we de instructies definiëren die ons programma kan uitvoeren. We gebruiken een enum waarbij elke variant een andere instructie vertegenwoordigt.

Voeg de volgende code toe aan lib.rs:

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

Implementeer instruction deserialisatie

Nu moeten we de instruction_data (ruwe bytes) deserialiseren naar een van onze CounterInstruction enum varianten. De Borsh try_from_slice methode handelt deze conversie automatisch af.

Werk de process_instruction functie bij om Borsh deserialisatie te gebruiken:

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

Route instructies naar handlers

Laten we nu de hoofdfunctie process_instruction bijwerken om instructies naar hun bijbehorende handler functies te routeren.

Dit routing patroon komt vaak voor in Solana programma's. De instruction_data wordt gedeserialiseerd naar een variant van een enum die de instructie vertegenwoordigt, waarna de juiste handler functie wordt aangeroepen. Elke handler functie bevat de implementatie voor die instructie.

Voeg de volgende code toe aan lib.rs en werk de process_instruction functie bij en voeg de handlers toe voor de InitializeCounter en IncrementCounter instructies:

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

Implementeer initialize handler

Laten we de handler implementeren om een nieuw counter account aan te maken en te initialiseren. Omdat alleen het System Program accounts kan aanmaken op Solana, gebruiken we een Cross Program Invocation (CPI), wat in feite het aanroepen van een ander programma vanuit ons programma betekent.

Ons programma maakt een CPI om de create_account instructie van het System Program aan te roepen. Het nieuwe account wordt aangemaakt met ons programma als eigenaar, waardoor ons programma de mogelijkheid krijgt om naar het account te schrijven en de gegevens te initialiseren.

Voeg de volgende code toe aan lib.rs en werk de process_initialize_counter functie bij:

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

Deze instructie is alleen voor demonstratiedoeleinden. Het bevat geen beveiligings- en validatiecontroles die vereist zijn voor productieprogramma's.

Implementeer increment handler

Laten we nu de handler implementeren die een bestaande teller verhoogt. Deze instructie:

  • Leest het data veld van het account voor de counter_account
  • Deserialiseert het naar een CounterAccount struct
  • Verhoogt het count veld met 1
  • Serialiseert de CounterAccount struct terug naar het data veld van het account

Voeg de volgende code toe aan lib.rs en werk de process_increment_counter functie bij:

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

Deze instructie is alleen voor demonstratiedoeleinden. Het bevat geen beveiligings- en validatiecontroles die vereist zijn voor productieprogramma's.

Voltooid programma

Gefeliciteerd! Je hebt een compleet Solana-programma gebouwd dat de basisstructuur demonstreert die alle Solana-programma's delen:

  • Entrypoint: Definieert waar de programma-uitvoering begint en routeert alle inkomende verzoeken naar de juiste instruction handlers
  • Instruction handling: Definieert instructies en hun bijbehorende handler functies
  • State management: Definieert accountgegevensstructuren en beheert hun status in programma-eigendom accounts
  • Cross program invocation (CPI): Roept het System Program aan om nieuwe programma-eigendom accounts aan te maken

De volgende stap is het testen van het programma om ervoor te zorgen dat alles correct werkt.

Maak een nieuw programma

Laten we eerst een nieuw Rust-project maken voor ons Solana-programma.

Terminal
$
cargo new counter_program --lib
$
cd counter_program

Je zou de standaard src/lib.rs en Cargo.toml bestanden moeten zien.

Werk het edition veld in Cargo.toml bij naar 2021. Anders kun je een foutmelding krijgen bij het bouwen van het programma.

Afhankelijkheden toevoegen

Laten we nu de benodigde afhankelijkheden toevoegen voor het bouwen van een Solana-programma. We hebben solana-program nodig voor de core SDK en borsh voor serialisatie.

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

Het is niet verplicht om Borsh te gebruiken. Het is echter een veelgebruikte serialisatiebibliotheek voor Solana-programma's.

Configureer crate-type

Solana-programma's moeten worden gecompileerd als dynamische bibliotheken. Voeg de [lib] sectie toe om te configureren hoe Cargo het programma bouwt.

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

Als je deze configuratie niet toevoegt, wordt de target/deploy directory niet gegenereerd wanneer je het programma bouwt.

Stel programma entrypoint in

Elk Solana-programma heeft een entrypoint, de functie die wordt aangeroepen wanneer het programma wordt geïnvoceerd. Laten we beginnen met het toevoegen van de imports die we nodig hebben voor het programma en het instellen van het entrypoint.

Voeg de volgende code toe aan 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(())
}

De entrypoint macro verzorgt de deserialisatie van de input data naar de parameters van de process_instruction functie.

Een Solana-programma entrypoint heeft de volgende functiesignatuur. Ontwikkelaars zijn vrij om hun eigen implementatie van de entrypoint functie te maken.

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

Definieer programma state

Laten we nu de datastructuur definiëren die in onze counter accounts wordt opgeslagen. Dit is de data die wordt opgeslagen in het data veld van het account.

Voeg de volgende code toe aan lib.rs:

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

Definieer instruction enum

Laten we de instructies definiëren die ons programma kan uitvoeren. We gebruiken een enum waarbij elke variant een andere instructie vertegenwoordigt.

Voeg de volgende code toe aan lib.rs:

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

Implementeer instruction deserialisatie

Nu moeten we de instruction_data (ruwe bytes) deserialiseren naar een van onze CounterInstruction enum varianten. De Borsh try_from_slice methode handelt deze conversie automatisch af.

Werk de process_instruction functie bij om Borsh deserialisatie te gebruiken:

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

Route instructies naar handlers

Laten we nu de hoofdfunctie process_instruction bijwerken om instructies naar hun bijbehorende handler functies te routeren.

Dit routing patroon komt vaak voor in Solana programma's. De instruction_data wordt gedeserialiseerd naar een variant van een enum die de instructie vertegenwoordigt, waarna de juiste handler functie wordt aangeroepen. Elke handler functie bevat de implementatie voor die instructie.

Voeg de volgende code toe aan lib.rs en werk de process_instruction functie bij en voeg de handlers toe voor de InitializeCounter en IncrementCounter instructies:

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

Implementeer initialize handler

Laten we de handler implementeren om een nieuw counter account aan te maken en te initialiseren. Omdat alleen het System Program accounts kan aanmaken op Solana, gebruiken we een Cross Program Invocation (CPI), wat in feite het aanroepen van een ander programma vanuit ons programma betekent.

Ons programma maakt een CPI om de create_account instructie van het System Program aan te roepen. Het nieuwe account wordt aangemaakt met ons programma als eigenaar, waardoor ons programma de mogelijkheid krijgt om naar het account te schrijven en de gegevens te initialiseren.

Voeg de volgende code toe aan lib.rs en werk de process_initialize_counter functie bij:

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

Deze instructie is alleen voor demonstratiedoeleinden. Het bevat geen beveiligings- en validatiecontroles die vereist zijn voor productieprogramma's.

Implementeer increment handler

Laten we nu de handler implementeren die een bestaande teller verhoogt. Deze instructie:

  • Leest het data veld van het account voor de counter_account
  • Deserialiseert het naar een CounterAccount struct
  • Verhoogt het count veld met 1
  • Serialiseert de CounterAccount struct terug naar het data veld van het account

Voeg de volgende code toe aan lib.rs en werk de process_increment_counter functie bij:

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

Deze instructie is alleen voor demonstratiedoeleinden. Het bevat geen beveiligings- en validatiecontroles die vereist zijn voor productieprogramma's.

Voltooid programma

Gefeliciteerd! Je hebt een compleet Solana-programma gebouwd dat de basisstructuur demonstreert die alle Solana-programma's delen:

  • Entrypoint: Definieert waar de programma-uitvoering begint en routeert alle inkomende verzoeken naar de juiste instruction handlers
  • Instruction handling: Definieert instructies en hun bijbehorende handler functies
  • State management: Definieert accountgegevensstructuren en beheert hun status in programma-eigendom accounts
  • Cross program invocation (CPI): Roept het System Program aan om nieuwe programma-eigendom accounts aan te maken

De volgende stap is het testen van het programma om ervoor te zorgen dat alles correct werkt.

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

Deel 2: Het programma testen

Laten we nu ons tellerprogramma testen. We gebruiken LiteSVM, een testframework waarmee we programma's kunnen testen zonder te deployen naar een cluster.

Voeg testafhankelijkheden toe

Laten we eerst de benodigde afhankelijkheden voor testen toevoegen. We gebruiken litesvm voor testen en solana-sdk.

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

Maak testmodule aan

Laten we nu een testmodule toevoegen aan ons programma. We beginnen met de basisstructuur en imports.

Voeg de volgende code toe aan lib.rs, direct onder de programmacode:

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

Het #[cfg(test)] attribuut zorgt ervoor dat deze code alleen wordt gecompileerd bij het uitvoeren van tests.

Initialiseer testomgeving

Laten we de testomgeving opzetten met LiteSVM en een betaler-account financieren.

LiteSVM simuleert de Solana runtime-omgeving, waardoor we ons programma kunnen testen zonder te deployen naar een echt cluster.

Voeg de volgende code toe aan lib.rs om de test_counter_program functie bij te werken:

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

Laad het programma

Nu moeten we ons programma bouwen en laden in de testomgeving. Voer het cargo build-sbf commando uit om het programma te bouwen. Dit genereert het counter_program.so bestand in de target/deploy directory.

Terminal
$
cargo build-sbf

Zorg ervoor dat de edition in Cargo.toml is ingesteld op 2021.

Na het bouwen kunnen we het programma laden.

Werk de test_counter_program functie bij om het programma in de testomgeving te laden.

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");

Je moet cargo build-sbf uitvoeren voordat je tests draait om het .so bestand te genereren. De test laadt het gecompileerde programma.

Test initialisatie-instructie

Laten we de initialisatie-instructie testen door een nieuw counter-account aan te maken met een startwaarde.

Voeg de volgende code toe aan lib.rs en werk de test_counter_program functie bij:

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);

Verifieer initialisatie

Na initialisatie gaan we verifiëren of het counter-account correct is aangemaakt met de verwachte waarde.

Voeg de volgende code toe aan lib.rs en werk de test_counter_program functie bij:

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);

Test increment-instructie

Nu gaan we de increment-instructie testen om te controleren of deze de counter-waarde correct bijwerkt.

Voeg de volgende code toe aan lib.rs en werk de test_counter_program functie bij:

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);

Verifieer eindresultaten

Tot slot gaan we verifiëren of de increment correct heeft gewerkt door de bijgewerkte counter-waarde te controleren.

Voeg de volgende code toe aan lib.rs en werk de test_counter_program functie bij:

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);

Voer de tests uit met het volgende commando. De --nocapture vlag toont de uitvoer van de test.

Terminal
$
cargo test -- --nocapture

Verwachte uitvoer:

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

Voeg testafhankelijkheden toe

Laten we eerst de benodigde afhankelijkheden voor testen toevoegen. We gebruiken litesvm voor testen en solana-sdk.

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

Maak testmodule aan

Laten we nu een testmodule toevoegen aan ons programma. We beginnen met de basisstructuur en imports.

Voeg de volgende code toe aan lib.rs, direct onder de programmacode:

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

Het #[cfg(test)] attribuut zorgt ervoor dat deze code alleen wordt gecompileerd bij het uitvoeren van tests.

Initialiseer testomgeving

Laten we de testomgeving opzetten met LiteSVM en een betaler-account financieren.

LiteSVM simuleert de Solana runtime-omgeving, waardoor we ons programma kunnen testen zonder te deployen naar een echt cluster.

Voeg de volgende code toe aan lib.rs om de test_counter_program functie bij te werken:

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

Laad het programma

Nu moeten we ons programma bouwen en laden in de testomgeving. Voer het cargo build-sbf commando uit om het programma te bouwen. Dit genereert het counter_program.so bestand in de target/deploy directory.

Terminal
$
cargo build-sbf

Zorg ervoor dat de edition in Cargo.toml is ingesteld op 2021.

Na het bouwen kunnen we het programma laden.

Werk de test_counter_program functie bij om het programma in de testomgeving te laden.

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");

Je moet cargo build-sbf uitvoeren voordat je tests draait om het .so bestand te genereren. De test laadt het gecompileerde programma.

Test initialisatie-instructie

Laten we de initialisatie-instructie testen door een nieuw counter-account aan te maken met een startwaarde.

Voeg de volgende code toe aan lib.rs en werk de test_counter_program functie bij:

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);

Verifieer initialisatie

Na initialisatie gaan we verifiëren of het counter-account correct is aangemaakt met de verwachte waarde.

Voeg de volgende code toe aan lib.rs en werk de test_counter_program functie bij:

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);

Test increment-instructie

Nu gaan we de increment-instructie testen om te controleren of deze de counter-waarde correct bijwerkt.

Voeg de volgende code toe aan lib.rs en werk de test_counter_program functie bij:

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);

Verifieer eindresultaten

Tot slot gaan we verifiëren of de increment correct heeft gewerkt door de bijgewerkte counter-waarde te controleren.

Voeg de volgende code toe aan lib.rs en werk de test_counter_program functie bij:

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);

Voer de tests uit met het volgende commando. De --nocapture vlag toont de uitvoer van de test.

Terminal
$
cargo test -- --nocapture

Verwachte uitvoer:

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"

Deel 3: Het programma aanroepen

Laten we nu een clientscript toevoegen om het programma aan te roepen.

Clientvoorbeeld maken

Laten we een Rust-client maken om te communiceren met ons geïmplementeerde programma.

Terminal
$
mkdir examples
$
touch examples/client.rs

Voeg de volgende configuratie toe aan Cargo.toml:

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

Installeer de clientafhankelijkheden:

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

Clientcode implementeren

Laten we nu de client implementeren die ons geïmplementeerde programma zal aanroepen.

Voer het volgende commando uit om je programma-ID uit het keypair-bestand te halen:

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

Voeg de clientcode toe aan examples/client.rs en vervang de program_id met de uitvoer van het vorige commando:

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

Clientvoorbeeld maken

Laten we een Rust-client maken om te communiceren met ons geïmplementeerde programma.

Terminal
$
mkdir examples
$
touch examples/client.rs

Voeg de volgende configuratie toe aan Cargo.toml:

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

Installeer de clientafhankelijkheden:

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

Clientcode implementeren

Laten we nu de client implementeren die ons geïmplementeerde programma zal aanroepen.

Voer het volgende commando uit om je programma-ID uit het keypair-bestand te halen:

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

Voeg de clientcode toe aan examples/client.rs en vervang de program_id met de uitvoer van het vorige commando:

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"

Deel 4: Het programma implementeren

Nu we ons programma en client klaar hebben, gaan we het programma bouwen, implementeren en aanroepen.

Bouw het programma

Laten we eerst ons programma bouwen.

Terminal
$
cargo build-sbf

Dit commando compileert je programma en genereert twee belangrijke bestanden in target/deploy/:

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

Je kunt de ID van je programma bekijken door het volgende commando uit te voeren:

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

Voorbeelduitvoer:

HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

Start lokale validator

Voor ontwikkeling gebruiken we een lokale testvalidator.

Configureer eerst de Solana CLI om localhost te gebruiken:

Terminal
$
solana config set -ul

Voorbeelduitvoer:

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

Start nu de testvalidator in een apart terminalvenster:

Terminal
$
solana-test-validator

Implementeer het programma

Terwijl de validator draait, implementeer je het programma op de lokale cluster:

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

Voorbeelduitvoer:

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Signature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h

Je kunt de implementatie verifiëren met het solana program show commando met je programma-ID:

Terminal
$
solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

Voorbeelduitvoer:

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

Voer de client uit

Terwijl de lokale validator nog steeds draait, voer je de client uit:

Terminal
$
cargo run --example client

Verwachte uitvoer:

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

Terwijl de lokale validator draait, kun je de transacties bekijken op Solana Explorer met behulp van de uitvoer transactiehandtekeningen. Let op: de cluster op Solana Explorer moet ingesteld zijn op "Custom RPC URL", die standaard http://localhost:8899 is waarop de solana-test-validator draait.

Is this page helpful?

Inhoudsopgave

Pagina Bewerken

Beheerd door

© 2026 Solana Foundation.
Alle rechten voorbehouden.
Blijf Verbonden