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, dat 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
: Definiëren programmaspecifieke status (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.
Je kunt voorbeelden vinden in de Solana Program Library.
Voorbeeldprogramma
Om te demonstreren hoe je een native Rust-programma bouwt met meerdere instructies, behandelen we een eenvoudig tellerprogramma dat twee instructies implementeert:
InitializeCounter
: Maakt een nieuw account aan en initialiseert het met een beginwaarde.IncrementCounter
: Verhoogt de waarde die is opgeslagen in een bestaand account.
Voor de eenvoud wordt het programma geïmplementeerd in één enkel
lib.rs
bestand, hoewel je in de praktijk grotere programma's misschien wilt
opsplitsen in meerdere bestanden.
Een nieuw programma maken
Maak eerst een nieuw Rust-project aan met het standaard cargo init
commando met
de --lib
vlag.
cargo init counter_program --lib
Navigeer naar de projectmap. Je zou de standaard src/lib.rs
en
Cargo.toml
bestanden moeten zien
cd counter_program
Voeg vervolgens de solana-program
dependency toe. Dit is de minimale
dependency die nodig is om een Solana-programma te bouwen.
cargo add solana-program@1.18.26
Voeg daarna het volgende fragment toe aan Cargo.toml
. Als je deze configuratie
niet opneemt, zal de target/deploy
map niet worden gegenereerd wanneer je het
programma bouwt.
[lib]crate-type = ["cdylib", "lib"]
Je Cargo.toml
bestand zou er als volgt uit moeten zien:
[package]name = "counter_program"version = "0.1.0"edition = "2021"[lib]crate-type = ["cdylib", "lib"][dependencies]solana-program = "1.18.26"
Programma-entrypoint
Een Solana-programma-entrypoint is de functie die wordt aangeroepen wanneer een programma wordt gestart. De entrypoint heeft de volgende ruwe definitie en ontwikkelaars zijn vrij om hun eigen implementatie van de entrypoint-functie te maken.
Voor de eenvoud, gebruik de
entrypoint!
macro uit de solana_program
crate om de entrypoint in je programma te
definiëren.
#[no_mangle]pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64;
Vervang de standaardcode in lib.rs
met de volgende code. Dit fragment:
- Importeert de vereiste dependencies van
solana_program
- Definieert de programma-entrypoint met behulp van de
entrypoint!
macro - Implementeert de
process_instruction
functie die instructies naar de juiste handlefuncties zal routeren
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 {// Your program logicOk(())}
De entrypoint!
macro vereist een functie met de volgende
type signature
als argument:
pub type ProcessInstruction =fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult;
Wanneer een Solana-programma wordt aangeroepen,
deserialiseert
de entrypoint de
invoergegevens
(aangeleverd als bytes) naar drie waarden en geeft deze door aan de
process_instruction
functie:
program_id
: De publieke sleutel van het programma dat wordt aangeroepen (huidige programma)accounts
: DeAccountInfo
voor accounts die vereist zijn door de instructie die wordt aangeroepeninstruction_data
: Aanvullende gegevens die aan het programma worden doorgegeven en die de uit te voeren instructie en de vereiste argumenten specificeren
Deze drie parameters komen rechtstreeks overeen met de gegevens die clients moeten verstrekken bij het bouwen van een instructie om een programma aan te roepen.
Programma-status definiëren
Bij het bouwen van een Solana-programma begin je meestal met het definiëren van de status van je programma - de gegevens die worden opgeslagen in accounts die door je programma zijn gemaakt en beheerd.
De programma-status wordt gedefinieerd met behulp van Rust-structs die de gegevensindeling van de accounts van je programma weergeven. Je kunt meerdere structs definiëren om verschillende soorten accounts voor je programma te representeren.
Bij het werken met accounts heb je een manier nodig om de gegevenstypen van je programma om te zetten naar en van de ruwe bytes die zijn opgeslagen in het gegevensveld van een account:
- Serialisatie: Het omzetten van je gegevenstypen naar bytes om op te slaan in het gegevensveld van een account
- Deserialisatie: Het omzetten van de bytes die zijn opgeslagen in een account terug naar je gegevenstypen
Hoewel je elk serialisatieformaat kunt gebruiken voor Solana-programmaontwikkeling, wordt Borsh vaak gebruikt. Om Borsh in je Solana-programma te gebruiken:
- Voeg de
borsh
crate toe als een afhankelijkheid aan jeCargo.toml
:
cargo add borsh
- Importeer de Borsh-traits en gebruik de derive-macro om de traits voor je structs te implementeren:
use borsh::{BorshSerialize, BorshDeserialize};// Define struct representing our counter account's data#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {count: u64,}
Voeg de CounterAccount
struct toe aan lib.rs
om de programma-status te
definiëren. Deze struct zal worden gebruikt in zowel de initialisatie- als de
increment-instructies.
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},};use borsh::{BorshSerialize, BorshDeserialize};entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Your program logicOk(())}#[derive(BorshSerialize, BorshDeserialize, Debug)]pub struct CounterAccount {count: u64,}
Instructies definiëren
Instructies verwijzen naar de verschillende operaties die je Solana-programma kan uitvoeren. Zie ze als openbare API's voor je programma - ze definiëren welke acties gebruikers kunnen ondernemen bij interactie met je programma.
Instructies worden meestal gedefinieerd met behulp van een Rust-enum waarbij:
- Elke enum-variant een andere instructie vertegenwoordigt
- De payload van de variant de parameters van de instructie vertegenwoordigt
Merk op dat Rust enum varianten impliciet genummerd worden vanaf 0.
Hieronder staat een voorbeeld van een enum die twee instructies definieert:
#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 }, // variant 0IncrementCounter, // variant 1}
Wanneer een client je programma aanroept, moeten ze instruction data (als een buffer van bytes) verstrekken waarbij:
- De eerste byte identificeert welke instructievariant moet worden uitgevoerd (0, 1, enz.)
- De overige bytes bevatten de geserialiseerde instructieparameters (indien vereist)
Om de instruction data (bytes) om te zetten naar een variant van de enum, is het gebruikelijk om een hulpmethode te implementeren. Deze methode:
- Splitst de eerste byte om de instructievariant te krijgen
- Matcht op de variant en parseert eventuele aanvullende parameters uit de resterende bytes
- Retourneert de corresponderende enum variant
Bijvoorbeeld, de unpack
methode voor de CounterInstruction
enum:
impl CounterInstruction {pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {// Get the instruction variant from the first bytelet (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;// Match instruction type and parse the remaining bytes based on the variantmatch variant {0 => {// For InitializeCounter, parse a u64 from the remaining byteslet initial_value = u64::from_le_bytes(rest.try_into().map_err(|_| ProgramError::InvalidInstructionData)?);Ok(Self::InitializeCounter { initial_value })}1 => Ok(Self::IncrementCounter), // No additional data needed_ => Err(ProgramError::InvalidInstructionData),}}}
Voeg de volgende code toe aan lib.rs
om de instructies voor het counter
programma te definiëren.
use borsh::{BorshDeserialize, BorshSerialize};use solana_program::{account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg,program_error::ProgramError, pubkey::Pubkey,};entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Your program logicOk(())}#[derive(BorshSerialize, BorshDeserialize, Debug)]pub enum CounterInstruction {InitializeCounter { initial_value: u64 }, // variant 0IncrementCounter, // variant 1}impl CounterInstruction {pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {// Get the instruction variant from the first bytelet (&variant, rest) = input.split_first().ok_or(ProgramError::InvalidInstructionData)?;// Match instruction type and parse the remaining bytes based on the variantmatch variant {0 => {// For InitializeCounter, parse a u64 from the remaining byteslet initial_value = u64::from_le_bytes(rest.try_into().map_err(|_| ProgramError::InvalidInstructionData)?,);Ok(Self::InitializeCounter { initial_value })}1 => Ok(Self::IncrementCounter), // No additional data needed_ => Err(ProgramError::InvalidInstructionData),}}}
Instructie handlers
Instructie handlers verwijzen naar de functies die de bedrijfslogica bevatten
voor elke instructie. Het is gebruikelijk om handlefuncties te benoemen als
process_<instruction_name>
, maar je bent vrij om elke naamgevingsconventie te
kiezen.
Voeg de volgende code toe aan lib.rs
. Deze code gebruikt de
CounterInstruction
enum en unpack
methode die in de vorige stap is
gedefinieerd om binnenkomende instructies naar de juiste handlefuncties te
routeren:
entrypoint!(process_instruction);pub fn process_instruction(program_id: &Pubkey,accounts: &[AccountInfo],instruction_data: &[u8],) -> ProgramResult {// Unpack instruction datalet instruction = CounterInstruction::unpack(instruction_data)?;// Match instruction typematch instruction {CounterInstruction::InitializeCounter { initial_value } => {process_initialize_counter(program_id, accounts, initial_value)?}CounterInstruction::IncrementCounter => process_increment_counter(program_id, accounts)?,};}fn process_initialize_counter(program_id: &Pubkey,accounts: &[AccountInfo],initial_value: u64,) -> ProgramResult {// Implementation details...Ok(())}fn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {// Implementation details...Ok(())}
Voeg vervolgens de implementatie van de process_initialize_counter
functie
toe. Deze instructie handler:
- Maakt een nieuw account aan en wijst ruimte toe om de countergegevens op te slaan
- Initialiseert de accountgegevens met
initial_value
die aan de instructie is doorgegeven
// Initialize a new counter accountfn 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)?;// Size of our counter accountlet account_space = 8; // Size in bytes to store a u64// Calculate minimum balance for rent exemptionlet rent = Rent::get()?;let required_lamports = rent.minimum_balance(account_space);// Create the counter accountinvoke(&system_instruction::create_account(payer_account.key, // Account paying for the new accountcounter_account.key, // Account to be createdrequired_lamports, // Amount of lamports to transfer to the new accountaccount_space as u64, // Size in bytes to allocate for the data fieldprogram_id, // Set program owner to our program),&[payer_account.clone(),counter_account.clone(),system_program.clone(),],)?;// Create a new CounterAccount struct with the initial valuelet counter_data = CounterAccount {count: initial_value,};// Get a mutable reference to the counter account's datalet mut account_data = &mut counter_account.data.borrow_mut()[..];// Serialize the CounterAccount struct into the account's datacounter_data.serialize(&mut account_data)?;msg!("Counter initialized with value: {}", initial_value);Ok(())}
Voeg vervolgens de implementatie van de process_increment_counter
functie toe.
Deze instructie verhoogt de waarde van een bestaand counter-account.
// Update an existing counter's valuefn process_increment_counter(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {let accounts_iter = &mut accounts.iter();let counter_account = next_account_info(accounts_iter)?;// Verify account ownershipif counter_account.owner != program_id {return Err(ProgramError::IncorrectProgramId);}// Mutable borrow the account datalet mut data = counter_account.data.borrow_mut();// Deserialize the account data into our CounterAccount structlet mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;// Increment the counter valuecounter_data.count = counter_data.count.checked_add(1).ok_or(ProgramError::InvalidAccountData)?;// Serialize the updated counter data back into the accountcounter_data.serialize(&mut &mut data[..])?;msg!("Counter incremented to: {}", counter_data.count);Ok(())}
Instructie testen
Om de programma-instructies te testen, voeg de volgende dependencies toe aan
Cargo.toml
.
cargo add solana-program-test@1.18.26 --devcargo add solana-sdk@1.18.26 --devcargo add tokio --dev
Voeg vervolgens de volgende testmodule toe aan lib.rs
en voer cargo test-sbf
uit om de tests uit te voeren. Optioneel kun je de --nocapture
flag gebruiken
om de print statements in de output te zien.
cargo test-sbf -- --nocapture
#[cfg(test)]mod test {use super::*;use solana_program_test::*;use solana_sdk::{instruction::{AccountMeta, Instruction},signature::{Keypair, Signer},system_program,transaction::Transaction,};#[tokio::test]async fn test_counter_program() {let program_id = Pubkey::new_unique();let (mut banks_client, payer, recent_blockhash) = ProgramTest::new("counter_program",program_id,processor!(process_instruction),).start().await;// Create a new keypair to use as the address for our counter accountlet counter_keypair = Keypair::new();let initial_value: u64 = 42;// Step 1: Initialize the counterprintln!("Testing counter initialization...");// Create initialization instructionlet mut init_instruction_data = vec![0]; // 0 = initialize instructioninit_instruction_data.extend_from_slice(&initial_value.to_le_bytes());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),],);// Send transaction with initialize instructionlet mut transaction =Transaction::new_with_payer(&[initialize_instruction], Some(&payer.pubkey()));transaction.sign(&[&payer, &counter_keypair], recent_blockhash);banks_client.process_transaction(transaction).await.unwrap();// Check account datalet account = banks_client.get_account(counter_keypair.pubkey()).await.expect("Failed to get counter account");if let Some(account_data) = account {let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data).expect("Failed to deserialize counter data");assert_eq!(counter.count, 42);println!("✅ Counter initialized successfully with value: {}",counter.count);}// Step 2: Increment the counterprintln!("Testing counter increment...");// Create increment instructionlet increment_instruction = Instruction::new_with_bytes(program_id,&[1], // 1 = increment instructionvec![AccountMeta::new(counter_keypair.pubkey(), true)],);// Send transaction with increment instructionlet mut transaction =Transaction::new_with_payer(&[increment_instruction], Some(&payer.pubkey()));transaction.sign(&[&payer, &counter_keypair], recent_blockhash);banks_client.process_transaction(transaction).await.unwrap();// Check account datalet account = banks_client.get_account(counter_keypair.pubkey()).await.expect("Failed to get counter account");if let Some(account_data) = account {let counter: CounterAccount = CounterAccount::try_from_slice(&account_data.data).expect("Failed to deserialize counter data");assert_eq!(counter.count, 43);println!("✅ Counter incremented successfully to: {}", counter.count);}}}
Voorbeelduitvoer:
running 1 test[2024-10-29T20:51:13.783708000Z INFO solana_program_test] "counter_program" SBF program from /counter_program/target/deploy/counter_program.so, modified 2 seconds, 169 ms, 153 µs and 461 ns ago[2024-10-29T20:51:13.855204000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1][2024-10-29T20:51:13.856052000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 invoke [2][2024-10-29T20:51:13.856135000Z DEBUG solana_runtime::message_processor::stable_log] Program 11111111111111111111111111111111 success[2024-10-29T20:51:13.856242000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Counter initialized with value: 42[2024-10-29T20:51:13.856285000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 3791 of 200000 compute units[2024-10-29T20:51:13.856307000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM success[2024-10-29T20:51:13.860038000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM invoke [1][2024-10-29T20:51:13.860333000Z DEBUG solana_runtime::message_processor::stable_log] Program log: Counter incremented to: 43[2024-10-29T20:51:13.860355000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM consumed 756 of 200000 compute units[2024-10-29T20:51:13.860375000Z DEBUG solana_runtime::message_processor::stable_log] Program 1111111QLbz7JHiBTspS962RLKV8GndWFwiEaqKM successtest test::test_counter_program ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s
Is this page helpful?