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:
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 lib.rs
bestand,
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.
$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.
$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.
[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
:
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
:
#[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
:
#[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:
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:
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:
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 decounter_account
- Deserialiseert het naar een
CounterAccount
struct - Verhoogt het
count
veld met 1 - Serialiseert de
CounterAccount
struct terug naar hetdata
veld van het account
Voeg de volgende code toe aan lib.rs
om de process_increment_counter
functie
bij te werken:
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.
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
.
$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:
#[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:
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
.
$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.
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:
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:
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:
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:
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.
$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: 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
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.
$mkdir examples$touch examples/client.rs
Voeg de volgende configuratie toe aan Cargo.toml
:
[[example]]name = "client"path = "examples/client.rs"
Installeer de client-afhankelijkheden:
$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:
$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:
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);}}}
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.
$cargo build-sbf
Dit commando compileert je programma en genereert twee belangrijke bestanden in
target/deploy/
:
counter_program.so # The compiled programcounter_program-keypair.json # Keypair for the program ID
Je kunt de ID van je programma bekijken door het volgende commando uit te voeren:
$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:
$solana config set -ul
Voorbeelduitvoer:
Config File: ~/.config/solana/cli/config.ymlRPC URL: http://localhost:8899WebSocket URL: ws://localhost:8900/ (computed)Keypair Path: ~/.config/solana/id.jsonCommitment: confirmed
Start nu de testvalidator in een aparte terminal:
$solana-test-validator
Programma implementeren
Met de validator actief, implementeer je programma op de lokale cluster:
$solana program deploy ./target/deploy/counter_program.so
Voorbeelduitvoer:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFSignature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h
Je kunt de implementatie verifiëren met het solana program show
commando met
je programma-ID:
$solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Voorbeelduitvoer:
Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJFOwner: BPFLoaderUpgradeab1e11111111111111111111111ProgramData Address: 47MVf5tRZ4zWXQMX7ydrkgcFQr8XTk1QBjohwsUzaiuMAuthority: 4kh6HxYZiAebF8HWLsUWod2EaQQ6iWHpHYCz8UcmFbM1Last Deployed In Slot: 16Data Length: 82696 (0x14308) bytesBalance: 0.57676824 SOL
Voer de client uit
Voer de client uit terwijl de lokale validator nog steeds actief is:
$cargo run --example client
Verwachte uitvoer:
Requesting airdrop...Airdrop confirmedInitializing counter...Counter initialized!Transaction: 2uenChtqNeLC1fitqoVE2LBeygSBTDchMZ4gGqs7AiDvZZVJguLDE5PfxsfkgY7xs6zFWnYsbEtb82dWv9tDT14kCounter address: EppPAmwqD42u4SCPWpPT7wmWKdFad5VnM9J4R9ZfofcyIncrementing 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?