Struttura del programma Rust

I programmi Solana scritti in Rust hanno requisiti strutturali minimi, permettendo flessibilità nell'organizzazione del codice. L'unico requisito è che un programma deve avere un entrypoint, che definisce dove inizia l'esecuzione di un programma.

Struttura del programma

Sebbene non ci siano regole rigide per la struttura dei file, i programmi Solana tipicamente seguono uno schema comune:

  • entrypoint.rs: Definisce l'entrypoint che indirizza le istruzioni in arrivo.
  • state.rs: Definisce lo stato del programma (dati dell'account).
  • instructions.rs: Definisce le istruzioni che il programma può eseguire.
  • processor.rs: Definisce i gestori di istruzioni (funzioni) che implementano la logica di business per ogni istruzione.
  • error.rs: Definisce gli errori personalizzati che il programma può restituire.

Per esempio, vedi il Token Program.

Programma di esempio

Per dimostrare come costruire un programma nativo in Rust con multiple istruzioni, esamineremo un semplice programma contatore che implementa due istruzioni:

  1. InitializeCounter: Crea e inizializza un nuovo account con un valore iniziale.
  2. IncrementCounter: Incrementa il valore memorizzato in un account esistente.

Per semplicità, il programma sarà implementato in un singolo file lib.rs, anche se in pratica potresti voler dividere programmi più grandi in più file.

Parte 1: Scrivere il programma

Iniziamo costruendo il programma contatore. Creeremo un programma che può inizializzare un contatore con un valore iniziale e incrementarlo.

Crea un nuovo programma

Prima di tutto, creiamo un nuovo progetto Rust per il nostro programma Solana.

Terminal
$
cargo new counter_program --lib
$
cd counter_program

Dovresti vedere i file predefiniti src/lib.rs e Cargo.toml.

Aggiorna il campo edition in Cargo.toml a 2021. Altrimenti, potresti incontrare un errore durante la compilazione del programma.

Aggiungere le dipendenze

Ora aggiungiamo le dipendenze necessarie per costruire un programma Solana. Abbiamo bisogno di solana-program per l'SDK principale e borsh per la serializzazione.

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

Non è obbligatorio utilizzare Borsh. Tuttavia, è una libreria di serializzazione comunemente utilizzata per i programmi Solana.

Configurare crate-type

I programmi Solana devono essere compilati come librerie dinamiche. Aggiungi la sezione [lib] per configurare come Cargo compila il programma.

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

Se non includi questa configurazione, la directory target/deploy non verrà generata quando compili il programma.

Configurare il punto di ingresso del programma

Ogni programma Solana ha un punto di ingresso, che è la funzione che viene chiamata quando il programma viene invocato. Iniziamo aggiungendo le importazioni necessarie per il programma e configurando il punto di ingresso.

Aggiungi il seguente codice a 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(())
}

La macro entrypoint gestisce la deserializzazione dei dati input nei parametri della funzione process_instruction.

Un entrypoint di un programma Solana ha la seguente firma di funzione. Gli sviluppatori sono liberi di creare la propria implementazione della funzione entrypoint.

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

Definire lo stato del programma

Ora definiamo la struttura dati che sarà memorizzata nei nostri account contatore. Questi sono i dati che saranno memorizzati nel campo data dell'account.

Aggiungi il seguente codice a lib.rs:

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

Definire l'enum delle istruzioni

Definiamo le istruzioni che il nostro programma può eseguire. Useremo un enum dove ogni variante rappresenta un'istruzione diversa.

Aggiungi il seguente codice a lib.rs:

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

Implementare la deserializzazione delle istruzioni

Ora dobbiamo deserializzare il instruction_data (byte grezzi) in una delle nostre varianti dell'enum CounterInstruction. Il metodo Borsh try_from_slice gestisce questa conversione automaticamente.

Aggiorna la funzione process_instruction per utilizzare la deserializzazione Borsh:

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

Instradare le istruzioni ai gestori

Ora aggiorniamo la funzione principale process_instruction per instradare le istruzioni ai loro gestori appropriati.

Questo modello di instradamento è comune nei programmi Solana. Il instruction_data viene deserializzato in una variante di un enum che rappresenta l'istruzione, quindi viene chiamata la funzione gestore appropriata. Ogni funzione gestore include l'implementazione per quella istruzione.

Aggiungi il seguente codice a lib.rs aggiornando la funzione process_instruction e aggiungendo i gestori per le istruzioni InitializeCounter e IncrementCounter:

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

Implementare il gestore di inizializzazione

Implementiamo il gestore per creare e inizializzare un nuovo account contatore. Poiché solo il System Program può creare account su Solana, useremo una Cross Program Invocation (CPI), essenzialmente chiamando un altro programma dal nostro programma.

Il nostro programma effettua una CPI per chiamare l'istruzione create_account del System Program. Il nuovo account viene creato con il nostro programma come proprietario, dando al nostro programma la capacità di scrivere nell'account e inizializzare i dati.

Aggiungi il seguente codice a lib.rs aggiornando la funzione 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(())
}

Questa istruzione è solo a scopo dimostrativo. Non include controlli di sicurezza e validazione necessari per i programmi in produzione.

Implementa il gestore di incremento

Ora implementiamo il gestore che incrementa un contatore esistente. Questa istruzione:

  • Legge il campo data dell'account per il counter_account
  • Lo deserializza in una struttura CounterAccount
  • Incrementa il campo count di 1
  • Serializza nuovamente la struttura CounterAccount nel campo data dell'account

Aggiungi il seguente codice a lib.rs aggiornando la funzione 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(())
}

Questa istruzione è solo a scopo dimostrativo. Non include controlli di sicurezza e validazione necessari per i programmi in produzione.

Programma completato

Congratulazioni! Hai creato un programma Solana completo che dimostra la struttura di base condivisa da tutti i programmi Solana:

  • Entrypoint: Definisce dove inizia l'esecuzione del programma e indirizza tutte le richieste in arrivo ai gestori di istruzioni appropriati
  • Gestione delle istruzioni: Definisce le istruzioni e le relative funzioni di gestione
  • Gestione dello stato: Definisce le strutture dei dati degli account e gestisce il loro stato negli account di proprietà del programma
  • Cross Program Invocation (CPI): Chiama il System Program per creare nuovi account di proprietà del programma

Il prossimo passo è testare il programma per assicurarsi che tutto funzioni correttamente.

Crea un nuovo programma

Prima di tutto, creiamo un nuovo progetto Rust per il nostro programma Solana.

Terminal
$
cargo new counter_program --lib
$
cd counter_program

Dovresti vedere i file predefiniti src/lib.rs e Cargo.toml.

Aggiorna il campo edition in Cargo.toml a 2021. Altrimenti, potresti incontrare un errore durante la compilazione del programma.

Aggiungere le dipendenze

Ora aggiungiamo le dipendenze necessarie per costruire un programma Solana. Abbiamo bisogno di solana-program per l'SDK principale e borsh per la serializzazione.

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

Non è obbligatorio utilizzare Borsh. Tuttavia, è una libreria di serializzazione comunemente utilizzata per i programmi Solana.

Configurare crate-type

I programmi Solana devono essere compilati come librerie dinamiche. Aggiungi la sezione [lib] per configurare come Cargo compila il programma.

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

Se non includi questa configurazione, la directory target/deploy non verrà generata quando compili il programma.

Configurare il punto di ingresso del programma

Ogni programma Solana ha un punto di ingresso, che è la funzione che viene chiamata quando il programma viene invocato. Iniziamo aggiungendo le importazioni necessarie per il programma e configurando il punto di ingresso.

Aggiungi il seguente codice a 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(())
}

La macro entrypoint gestisce la deserializzazione dei dati input nei parametri della funzione process_instruction.

Un entrypoint di un programma Solana ha la seguente firma di funzione. Gli sviluppatori sono liberi di creare la propria implementazione della funzione entrypoint.

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

Definire lo stato del programma

Ora definiamo la struttura dati che sarà memorizzata nei nostri account contatore. Questi sono i dati che saranno memorizzati nel campo data dell'account.

Aggiungi il seguente codice a lib.rs:

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

Definire l'enum delle istruzioni

Definiamo le istruzioni che il nostro programma può eseguire. Useremo un enum dove ogni variante rappresenta un'istruzione diversa.

Aggiungi il seguente codice a lib.rs:

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

Implementare la deserializzazione delle istruzioni

Ora dobbiamo deserializzare il instruction_data (byte grezzi) in una delle nostre varianti dell'enum CounterInstruction. Il metodo Borsh try_from_slice gestisce questa conversione automaticamente.

Aggiorna la funzione process_instruction per utilizzare la deserializzazione Borsh:

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

Instradare le istruzioni ai gestori

Ora aggiorniamo la funzione principale process_instruction per instradare le istruzioni ai loro gestori appropriati.

Questo modello di instradamento è comune nei programmi Solana. Il instruction_data viene deserializzato in una variante di un enum che rappresenta l'istruzione, quindi viene chiamata la funzione gestore appropriata. Ogni funzione gestore include l'implementazione per quella istruzione.

Aggiungi il seguente codice a lib.rs aggiornando la funzione process_instruction e aggiungendo i gestori per le istruzioni InitializeCounter e IncrementCounter:

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

Implementare il gestore di inizializzazione

Implementiamo il gestore per creare e inizializzare un nuovo account contatore. Poiché solo il System Program può creare account su Solana, useremo una Cross Program Invocation (CPI), essenzialmente chiamando un altro programma dal nostro programma.

Il nostro programma effettua una CPI per chiamare l'istruzione create_account del System Program. Il nuovo account viene creato con il nostro programma come proprietario, dando al nostro programma la capacità di scrivere nell'account e inizializzare i dati.

Aggiungi il seguente codice a lib.rs aggiornando la funzione 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(())
}

Questa istruzione è solo a scopo dimostrativo. Non include controlli di sicurezza e validazione necessari per i programmi in produzione.

Implementa il gestore di incremento

Ora implementiamo il gestore che incrementa un contatore esistente. Questa istruzione:

  • Legge il campo data dell'account per il counter_account
  • Lo deserializza in una struttura CounterAccount
  • Incrementa il campo count di 1
  • Serializza nuovamente la struttura CounterAccount nel campo data dell'account

Aggiungi il seguente codice a lib.rs aggiornando la funzione 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(())
}

Questa istruzione è solo a scopo dimostrativo. Non include controlli di sicurezza e validazione necessari per i programmi in produzione.

Programma completato

Congratulazioni! Hai creato un programma Solana completo che dimostra la struttura di base condivisa da tutti i programmi Solana:

  • Entrypoint: Definisce dove inizia l'esecuzione del programma e indirizza tutte le richieste in arrivo ai gestori di istruzioni appropriati
  • Gestione delle istruzioni: Definisce le istruzioni e le relative funzioni di gestione
  • Gestione dello stato: Definisce le strutture dei dati degli account e gestisce il loro stato negli account di proprietà del programma
  • Cross Program Invocation (CPI): Chiama il System Program per creare nuovi account di proprietà del programma

Il prossimo passo è testare il programma per assicurarsi che tutto funzioni correttamente.

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

Parte 2: Testare il programma

Ora testiamo il nostro programma contatore. Useremo LiteSVM, un framework di testing che ci permette di testare i programmi senza doverli distribuire su un cluster.

Aggiungi le dipendenze per i test

Prima di tutto, aggiungiamo le dipendenze necessarie per i test. Useremo litesvm per i test e solana-sdk.

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

Creare un modulo di test

Ora aggiungiamo un modulo di test al nostro programma. Inizieremo con la struttura di base e le importazioni.

Aggiungi il seguente codice a lib.rs, direttamente sotto il codice del programma:

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

L'attributo #[cfg(test)] assicura che questo codice venga compilato solo durante l'esecuzione dei test.

Inizializzare l'ambiente di test

Configuriamo l'ambiente di test con LiteSVM e finanziamo un account pagante.

LiteSVM simula l'ambiente di runtime di Solana, permettendoci di testare il nostro programma senza doverlo distribuire su un cluster reale.

Aggiungi il seguente codice a lib.rs aggiornando la funzione 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");

Caricare il programma

Ora dobbiamo compilare e caricare il nostro programma nell'ambiente di test. Esegui il comando cargo build-sbf per compilare il programma. Questo genererà il file counter_program.so nella directory target/deploy.

Terminal
$
cargo build-sbf

Assicurati che edition in Cargo.toml sia impostato su 2021.

Dopo la compilazione, possiamo caricare il programma.

Aggiorna la funzione test_counter_program per caricare il programma nell'ambiente di test.

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

Devi eseguire cargo build-sbf prima di eseguire i test per generare il file .so. Il test carica il programma compilato.

Testare l'istruzione di inizializzazione

Testiamo l'istruzione di inizializzazione creando un nuovo account contatore con un valore iniziale.

Aggiungi il seguente codice a lib.rs aggiornando la funzione 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);

Verifica dell'inizializzazione

Dopo l'inizializzazione, verifichiamo che l'account del contatore sia stato creato correttamente con il valore previsto.

Aggiungi il seguente codice a lib.rs aggiornando la funzione 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);

Test dell'istruzione di incremento

Ora testiamo l'istruzione di incremento per assicurarci che aggiorni correttamente il valore del contatore.

Aggiungi il seguente codice a lib.rs aggiornando la funzione 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);

Verifica dei risultati finali

Infine, verifichiamo che l'incremento abbia funzionato correttamente controllando il valore aggiornato del contatore.

Aggiungi il seguente codice a lib.rs aggiornando la funzione 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);

Esegui i test con il seguente comando. Il flag --nocapture mostra l'output del test.

Terminal
$
cargo test -- --nocapture

Output previsto:

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

Aggiungi le dipendenze per i test

Prima di tutto, aggiungiamo le dipendenze necessarie per i test. Useremo litesvm per i test e solana-sdk.

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

Creare un modulo di test

Ora aggiungiamo un modulo di test al nostro programma. Inizieremo con la struttura di base e le importazioni.

Aggiungi il seguente codice a lib.rs, direttamente sotto il codice del programma:

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

L'attributo #[cfg(test)] assicura che questo codice venga compilato solo durante l'esecuzione dei test.

Inizializzare l'ambiente di test

Configuriamo l'ambiente di test con LiteSVM e finanziamo un account pagante.

LiteSVM simula l'ambiente di runtime di Solana, permettendoci di testare il nostro programma senza doverlo distribuire su un cluster reale.

Aggiungi il seguente codice a lib.rs aggiornando la funzione 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");

Caricare il programma

Ora dobbiamo compilare e caricare il nostro programma nell'ambiente di test. Esegui il comando cargo build-sbf per compilare il programma. Questo genererà il file counter_program.so nella directory target/deploy.

Terminal
$
cargo build-sbf

Assicurati che edition in Cargo.toml sia impostato su 2021.

Dopo la compilazione, possiamo caricare il programma.

Aggiorna la funzione test_counter_program per caricare il programma nell'ambiente di test.

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

Devi eseguire cargo build-sbf prima di eseguire i test per generare il file .so. Il test carica il programma compilato.

Testare l'istruzione di inizializzazione

Testiamo l'istruzione di inizializzazione creando un nuovo account contatore con un valore iniziale.

Aggiungi il seguente codice a lib.rs aggiornando la funzione 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);

Verifica dell'inizializzazione

Dopo l'inizializzazione, verifichiamo che l'account del contatore sia stato creato correttamente con il valore previsto.

Aggiungi il seguente codice a lib.rs aggiornando la funzione 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);

Test dell'istruzione di incremento

Ora testiamo l'istruzione di incremento per assicurarci che aggiorni correttamente il valore del contatore.

Aggiungi il seguente codice a lib.rs aggiornando la funzione 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);

Verifica dei risultati finali

Infine, verifichiamo che l'incremento abbia funzionato correttamente controllando il valore aggiornato del contatore.

Aggiungi il seguente codice a lib.rs aggiornando la funzione 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);

Esegui i test con il seguente comando. Il flag --nocapture mostra l'output del test.

Terminal
$
cargo test -- --nocapture

Output previsto:

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"

Parte 3: Invocazione del programma

Ora aggiungiamo uno script client per invocare il programma.

Crea esempio di client

Creiamo un client Rust per interagire con il nostro programma distribuito.

Terminal
$
mkdir examples
$
touch examples/client.rs

Aggiungi la seguente configurazione a Cargo.toml:

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

Installa le dipendenze del client:

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

Implementa il codice del client

Ora implementiamo il client che invocherà il nostro programma distribuito.

Esegui il seguente comando per ottenere l'ID del tuo programma dal file keypair:

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

Aggiungi il codice del client a examples/client.rs e sostituisci il program_id con l'output del comando precedente:

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

Crea esempio di client

Creiamo un client Rust per interagire con il nostro programma distribuito.

Terminal
$
mkdir examples
$
touch examples/client.rs

Aggiungi la seguente configurazione a Cargo.toml:

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

Installa le dipendenze del client:

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

Implementa il codice del client

Ora implementiamo il client che invocherà il nostro programma distribuito.

Esegui il seguente comando per ottenere l'ID del tuo programma dal file keypair:

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

Aggiungi il codice del client a examples/client.rs e sostituisci il program_id con l'output del comando precedente:

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"

Parte 4: Distribuzione del programma

Ora che abbiamo il nostro programma e il client pronti, costruiamo, distribuiamo e invochiamo il programma.

Costruisci il programma

Prima di tutto, costruiamo il nostro programma.

Terminal
$
cargo build-sbf

Questo comando compila il tuo programma e genera due file importanti in target/deploy/:

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

Puoi visualizzare l'ID del tuo programma eseguendo il seguente comando:

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

Esempio di output:

HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

Avvia il validator locale

Per lo sviluppo, utilizzeremo un validator di test locale.

Prima, configura la CLI di Solana per utilizzare localhost:

Terminal
$
solana config set -ul

Esempio di output:

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

Ora avvia il validator di test in un terminale separato:

Terminal
$
solana-test-validator

Distribuisci il programma

Con il validator in esecuzione, distribuisci il tuo programma sul cluster locale:

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

Esempio di output:

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Signature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h

Puoi verificare la distribuzione utilizzando il comando solana program show con l'ID del tuo programma:

Terminal
$
solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

Output di esempio:

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

Esegui il client

Con il validator locale ancora in esecuzione, esegui il client:

Terminal
$
cargo run --example client

Output previsto:

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

Con il validator locale in esecuzione, puoi visualizzare le transazioni su Solana Explorer utilizzando le firme delle transazioni nell'output. Nota che il cluster su Solana Explorer deve essere impostato su "Custom RPC URL", che per impostazione predefinita è http://localhost:8899 su cui il solana-test-validator è in esecuzione.

Is this page helpful?

Indice

Modifica Pagina