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 meestal 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 teruggeven.

Zie bijvoorbeeld het Token Program.

Voorbeeldprogramma

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

  1. InitializeCounter: Maakt een nieuw account aan en initialiseert het met een beginwaarde.
  2. IncrementCounter: Verhoogt de waarde die is opgeslagen in een bestaand account.

Voor de eenvoud wordt het programma geïmplementeerd in één lib.rsbestand, hoewel je in de praktijk grotere programma's misschien wilt opsplitsen in meerdere bestanden.

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 nodige 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 opneemt, wordt de target/deploy directory niet gegenereerd wanneer je het programma bouwt.

Instellen van programma-entrypoint

Elk Solana-programma heeft een entrypoint, dit is de functie die wordt aangeroepen wanneer het programma wordt gestart. 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 handelt de deserialisatie van de input data naar de parameters van de process_instruction functie af.

Een Solana-programma entrypoint heeft de volgende functiehandtekening. 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 programmastatus

Laten we nu de datastructuur definiëren die in onze counter-accounts wordt opgeslagen. Dit zijn de gegevens die worden 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 instructie enum

Laten we de instructies definiëren die ons programma kan uitvoeren. We gebruiken een enum waarin elke variant een verschillende 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 instructie 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.

Update de process_instruction functie 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 hoofd process_instruction functie updaten om instructies naar hun bijbehorende handlefuncties te routeren.

Dit routeringspatroon is gebruikelijk in Solana-programma's. De instruction_data wordt gedeserialiseerd naar een variant van een enum die de instructie vertegenwoordigt, waarna de juiste handlefunctie wordt aangeroepen. Elke handlefunctie bevat de implementatie voor die instructie.

Voeg de volgende code toe aan lib.rs om de process_instruction functie te updaten en de handlers voor de InitializeCounter en IncrementCounter instructies toe te voegen:

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. Aangezien alleen het System Program accounts kan aanmaken op Solana, gebruiken we een Cross Program Invocation (CPI), waarbij we in feite een ander programma aanroepen vanuit ons programma.

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 om de process_initialize_counter functie bij te werken:

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 om de process_increment_counter functie bij te werken:

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 toont die alle Solana-programma's delen:

  • Entrypoint: Definieert waar de programma-uitvoering begint en routeert alle binnenkomende verzoeken naar de juiste instructiehandlers
  • Instruction Handling: Definieert instructies en hun bijbehorende handler functies
  • State Management: Definieert accountdatastructuren en beheert hun status in programma-eigen accounts
  • Cross Program Invocation (CPI): Roept het System Program aan om nieuwe programma-eigen accounts te creëren

De volgende stap is het testen van het programma om 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 nodige 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 opneemt, wordt de target/deploy directory niet gegenereerd wanneer je het programma bouwt.

Instellen van programma-entrypoint

Elk Solana-programma heeft een entrypoint, dit is de functie die wordt aangeroepen wanneer het programma wordt gestart. 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 handelt de deserialisatie van de input data naar de parameters van de process_instruction functie af.

Een Solana-programma entrypoint heeft de volgende functiehandtekening. 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 programmastatus

Laten we nu de datastructuur definiëren die in onze counter-accounts wordt opgeslagen. Dit zijn de gegevens die worden 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 instructie enum

Laten we de instructies definiëren die ons programma kan uitvoeren. We gebruiken een enum waarin elke variant een verschillende 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 instructie 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.

Update de process_instruction functie 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 hoofd process_instruction functie updaten om instructies naar hun bijbehorende handlefuncties te routeren.

Dit routeringspatroon is gebruikelijk in Solana-programma's. De instruction_data wordt gedeserialiseerd naar een variant van een enum die de instructie vertegenwoordigt, waarna de juiste handlefunctie wordt aangeroepen. Elke handlefunctie bevat de implementatie voor die instructie.

Voeg de volgende code toe aan lib.rs om de process_instruction functie te updaten en de handlers voor de InitializeCounter en IncrementCounter instructies toe te voegen:

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. Aangezien alleen het System Program accounts kan aanmaken op Solana, gebruiken we een Cross Program Invocation (CPI), waarbij we in feite een ander programma aanroepen vanuit ons programma.

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 om de process_initialize_counter functie bij te werken:

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 om de process_increment_counter functie bij te werken:

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 toont die alle Solana-programma's delen:

  • Entrypoint: Definieert waar de programma-uitvoering begint en routeert alle binnenkomende verzoeken naar de juiste instructiehandlers
  • Instruction Handling: Definieert instructies en hun bijbehorende handler functies
  • State Management: Definieert accountdatastructuren en beheert hun status in programma-eigen accounts
  • Cross Program Invocation (CPI): Roept het System Program aan om nieuwe programma-eigen accounts te creëren

De volgende stap is het testen van het programma om 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 zullen LiteSVM gebruiken, een testframework waarmee we programma's kunnen testen zonder ze op een cluster te implementeren.

Voeg testafhankelijkheden toe

Eerst voegen we de benodigde afhankelijkheden toe voor het testen. We zullen litesvm gebruiken voor testen en solana-sdk.

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

Testmodule maken

Laten we nu een testmodule aan ons programma toevoegen. 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.

Testomgeving initialiseren

Laten we de testomgeving opzetten met LiteSVM en een betaalaccount financieren.

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

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

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

Het programma laden

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

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 functie test_counter_program 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 uitvoert om het bestand .so te genereren. De test laadt het gecompileerde programma.

Initialisatie-instructie testen

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

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

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

Initialisatie verifiëren

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

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

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

Increment-instructie testen

Laten we nu de increment-instructie testen om er zeker van te zijn dat deze de counter-waarde correct bijwerkt.

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

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

Eindresultaten verifiëren

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

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

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

Eerst voegen we de benodigde afhankelijkheden toe voor het testen. We zullen litesvm gebruiken voor testen en solana-sdk.

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

Testmodule maken

Laten we nu een testmodule aan ons programma toevoegen. 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.

Testomgeving initialiseren

Laten we de testomgeving opzetten met LiteSVM en een betaalaccount financieren.

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

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

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

Het programma laden

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

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 functie test_counter_program 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 uitvoert om het bestand .so te genereren. De test laadt het gecompileerde programma.

Initialisatie-instructie testen

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

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

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

Initialisatie verifiëren

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

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

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

Increment-instructie testen

Laten we nu de increment-instructie testen om er zeker van te zijn dat deze de counter-waarde correct bijwerkt.

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

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

Eindresultaten verifiëren

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

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

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 client-script toevoegen om het programma aan te roepen.

Client voorbeeld 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 client-afhankelijkheden:

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

Client code implementeren

Nu gaan we 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 client-code 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);
}
}
}

Client voorbeeld 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 client-afhankelijkheden:

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

Client code implementeren

Nu gaan we 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 client-code 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 de client klaar hebben, laten we het programma bouwen, implementeren en aanroepen.

Bouw het programma

Eerst gaan we 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 aparte terminal:

Terminal
$
solana-test-validator

Programma implementeren

Met de validator actief, implementeer je 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

Voer de client uit terwijl de lokale validator nog steeds actief is:

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

Met de lokale validator actief kun je de transacties bekijken op Solana Explorer met behulp van de handtekeningen van de uitgevoerde transacties. Let op dat de cluster op Solana Explorer moet worden ingesteld op "Custom RPC URL", die standaard is ingesteld op http://localhost:8899 waarop de solana-test-validator draait.

Is this page helpful?

Inhoudsopgave

Pagina Bewerken