I programmi Solana scritti in Rust hanno requisiti strutturali minimi,
consentendo 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 esistano regole rigide per la struttura dei file, i programmi Solana seguono tipicamente un pattern comune:
entrypoint.rs: definisce l'entrypoint che instrada 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 delle istruzioni (funzioni) che implementano la logica di business per ogni istruzione.error.rs: definisce gli errori personalizzati che il programma può restituire.
Ad esempio, vedi il Token Program.
Programma di esempio
Per dimostrare come costruire un programma Rust nativo con più istruzioni, esamineremo un semplice programma contatore che implementa due istruzioni:
InitializeCounter: crea e inizializza un nuovo account con un valore iniziale.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 suddividere 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
Per prima cosa, creiamo un nuovo progetto Rust per il nostro programma Solana.
$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
riscontrare un errore durante la compilazione del programma.
Aggiungi 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.
$cargo add solana-program@2.2.0$cargo add borsh
Non è obbligatorio usare Borsh. Tuttavia, è una libreria di serializzazione comunemente utilizzata per i programmi Solana.
Configura crate-type
I programmi Solana devono essere compilati come librerie dinamiche. Aggiungi la
sezione [lib] per configurare come Cargo compila il programma.
[lib]crate-type = ["cdylib", "lib"]
Se non includi questa configurazione, la directory target/deploy non verrà generata quando compili il programma.
Configura l'entrypoint del programma
Ogni programma Solana ha un entrypoint, che è la funzione che viene chiamata quando il programma viene invocato. Iniziamo aggiungendo gli import necessari per il programma e configurando l'entrypoint.
Aggiungi il seguente codice a 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;
Definisci lo stato del programma
Ora definiamo la struttura dati che verrà memorizzata nei nostri account
counter. Questi sono i dati che verranno memorizzati nel campo data
dell'account.
Aggiungi il seguente codice a lib.rs:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {pub count: u64,}
Definisci 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:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 },IncrementCounter,}
Implementa la deserializzazione delle istruzioni
Ora dobbiamo deserializzare i instruction_data (byte grezzi) in una delle
varianti del nostro enum CounterInstruction. Il metodo try_from_slice di
Borsh gestisce automaticamente questa conversione.
Aggiorna la funzione process_instruction per utilizzare la deserializzazione
Borsh:
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(())}
Indirizza le istruzioni ai gestori
Ora aggiorniamo la funzione principale process_instruction per indirizzare le
istruzioni alle rispettive funzioni di gestione.
Questo pattern di routing è comune nei programmi Solana. I instruction_data
vengono deserializzati in una variante di un enum che rappresenta l'istruzione,
quindi viene chiamata la funzione di gestione appropriata. Ogni funzione di
gestione include l'implementazione per quella specifica istruzione.
Aggiungi il seguente codice a lib.rs aggiornando la funzione
process_instruction e aggiungendo i gestori per le istruzioni
InitializeCounter e IncrementCounter:
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(())}
Implementa il gestore di inizializzazione
Implementiamo il gestore per creare e inizializzare un nuovo account counter. 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, conferendo 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:
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 i controlli di sicurezza e validazione richiesti per i programmi in produzione.
Implementare il gestore di incremento
Ora implementiamo il gestore che incrementa un contatore esistente. Questa istruzione:
- Legge il campo
datadell'account percounter_account - Lo deserializza in una struct
CounterAccount - Incrementa il campo
countdi 1 - Serializza la struct
CounterAccountnuovamente nel campodatadell'account
Aggiungi il seguente codice a lib.rs aggiornando la funzione
process_increment_counter:
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 i controlli di sicurezza e validazione richiesti per i programmi in produzione.
Programma completato
Congratulazioni! Hai costruito 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 instrada 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 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.
Parte 2: testare il programma
Ora testiamo il nostro programma counter. Useremo LiteSVM, un framework di testing che ci permette di testare i programmi senza deployarli su un cluster.
Aggiungere le dipendenze di test
Prima di tutto, aggiungiamo le dipendenze necessarie per il testing. Useremo
litesvm per il testing e solana-sdk.
$cargo add litesvm@0.6.1 --dev$cargo add solana-sdk@2.2.0 --dev
Creare il modulo di test
Ora aggiungiamo un modulo di test al nostro programma. Inizieremo con la struttura di base e gli import.
Aggiungi il seguente codice a lib.rs, direttamente sotto il codice del
programma:
#[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
quando si eseguono i test.
Inizializzare l'ambiente di test
Configuriamo l'ambiente di test con LiteSVM e finanziamo un account payer.
LiteSVM simula l'ambiente runtime di Solana, permettendoci di testare il nostro programma senza deployarlo su un cluster reale.
Aggiungi il seguente codice a lib.rs aggiornando la funzione
test_counter_program:
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 buildare e caricare il nostro programma nell'ambiente di test.
Esegui il comando cargo build-sbf per buildare il programma. Questo genererà
il file counter_program.so nella directory target/deploy.
$cargo build-sbf
Assicurati che edition in Cargo.toml sia impostato su 2021.
Dopo la build, possiamo caricare il programma.
Aggiorna la funzione test_counter_program per caricare il programma
nell'ambiente di test.
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:
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);
Verificare l'inizializzazione
Dopo l'inizializzazione, verifichiamo che l'account contatore sia stato creato correttamente con il valore previsto.
Aggiungi il seguente codice a lib.rs aggiornando la funzione
test_counter_program:
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);
Testare l'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:
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);
Verificare i 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:
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 stampa l'output del
test.
$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: 42Testing 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
Parte 3: Invocazione del programma
Ora aggiungiamo uno script client per invocare il programma.
Crea esempio client
Creiamo un client Rust per interagire con il nostro programma distribuito.
$mkdir examples$touch examples/client.rs
Aggiungi la seguente configurazione a Cargo.toml:
[[example]]name = "client"path = "examples/client.rs"
Installa le dipendenze del client:
$cargo add solana-client@2.2.0 --dev$cargo add tokio --dev
Implementa il codice 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:
$solana address -k ./target/deploy/counter_program-keypair.json
Aggiungi il codice client a examples/client.rs e sostituisci program_id con
l'output del comando precedente:
let program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH").expect("Invalid program ID");
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 deploymentlet program_id = Pubkey::from_str("BDLLezrtFEXVGYqG3aS7eAC7GVeojJ4JHhKJM6pAFCDH").expect("Invalid program ID");// Connect to local clusterlet rpc_url = String::from("http://localhost:8899");let client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed());// Generate a new keypair for paying feeslet payer = Keypair::new();// Request airdrop of 1 SOL for transaction feesprintln!("Requesting airdrop...");let airdrop_signature = client.request_airdrop(&payer.pubkey(), 1_000_000_000).expect("Failed to request airdrop");// Wait for airdrop confirmationloop {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 datalet 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 datalet 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);}}}
Parte 4: Distribuzione del programma
Ora che abbiamo il nostro programma e il client pronti, compiliamo, distribuiamo e invochiamo il programma.
Compilare il programma
Prima di tutto, compiliamo il nostro programma.
$cargo build-sbf
Questo comando compila il tuo programma e genera due file importanti in
target/deploy/:
counter_program.so # The compiled programcounter_program-keypair.json # Keypair for the program ID
Puoi visualizzare l'ID del tuo programma eseguendo il seguente comando:
$solana address -k ./target/deploy/counter_program-keypair.json
Esempio di output:
HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Avviare il validator locale
Per lo sviluppo, utilizzeremo un validator di test locale.
Prima di tutto, configura la CLI di Solana per utilizzare localhost:
$solana config set -ul
Esempio di output:
Config File: ~/.config/solana/cli/config.ymlRPC URL: http://localhost:8899WebSocket URL: ws://localhost:8900/ (computed)Keypair Path: ~/.config/solana/id.jsonCommitment: confirmed
Ora avvia il validator di test in un terminale separato:
$solana-test-validator
Distribuire il programma
Con il validator in esecuzione, distribuisci il tuo programma sul cluster locale:
$solana program deploy ./target/deploy/counter_program.so
Esempio di output:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFSignature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h
Puoi verificare la distribuzione utilizzando il comando solana program show
con il tuo program ID:
$solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Esempio di output:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFOwner: BPFLoaderUpgradeab1e11111111111111111111111ProgramData Address: 47MVf5tRZ4zWXQMX7ydrkgcFQr8XTk1QBjohwsUzaiuMAuthority: 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1Last Deployed In Slot: 16Data Length: 82696 (0x14308) bytesBalance: 0.57676824 SOL
Eseguire il client
Con il validator locale ancora in esecuzione, esegui il client:
$cargo run --example client
Output previsto:
Requesting airdrop...Airdrop confirmedInitializing counter...Counter initialized!Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14kCounter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9ZfofcyIncrementing counter...Counter incremented!Transaction: 4qv1Rx6FHu1M3woVgDQ6KtYUaJgBzGcHnhej76ZpaKGCgsTorbcHnPKxoH916UENw7X5ppnQ8PkPnhXxEwrYuUxS
Con il validator locale in esecuzione, puoi visualizzare le transazioni su
Solana Explorer utilizzando le
signature delle transazioni nell'output. Nota che il cluster su Solana Explorer
deve essere impostato su "Custom RPC URL", che per default è
http://localhost:8899 su cui il solana-test-validator è in esecuzione.
Is this page helpful?