Solana-dokumentaatioOhjelmien kehittäminenRust-ohjelmat

Rust-ohjelman rakenne

Rustilla kirjoitetuilla Solana-ohjelmilla on minimaaliset rakennevaatimukset, mikä mahdollistaa joustavuuden koodin organisoinnissa. Ainoa vaatimus on, että ohjelmalla on oltava entrypoint, joka määrittää mistä ohjelman suoritus alkaa.

Ohjelman rakenne

Vaikka tiedostorakenteelle ei ole tiukkoja sääntöjä, Solana-ohjelmat noudattavat tyypillisesti yleistä kaavaa:

  • entrypoint.rs: Määrittää sisääntulokodan, joka ohjaa saapuvat käskyt.
  • state.rs: Määrittää ohjelmalle ominaisen tilan (tilin tiedot).
  • instructions.rs: Määrittää käskyt, joita ohjelma voi suorittaa.
  • processor.rs: Määrittää käskyjen käsittelijät (funktiot), jotka toteuttavat liiketoimintalogiikan kullekin käskylle.
  • error.rs: Määrittää mukautetut virheet, joita ohjelma voi palauttaa.

Löydät esimerkkejä Solana Program Library -kirjastosta.

Esimerkkiohjelma

Havainnollistaaksemme, kuinka rakennetaan natiivi Rust-ohjelma useilla käskyillä, käymme läpi yksinkertaisen laskuriohjelma, joka toteuttaa kaksi käskyä:

  1. InitializeCounter: Luo ja alustaa uuden tilin alkuarvolla.
  2. IncrementCounter: Kasvattaa olemassa olevaan tiliin tallennettua arvoa.

Yksinkertaisuuden vuoksi ohjelma toteutetaan yhdessä lib.rs-tiedostossa, vaikka käytännössä saatat haluta jakaa suuremmat ohjelmat useisiin tiedostoihin.

Luo uusi ohjelma

Ensin luo uusi Rust-projekti käyttäen standardia cargo init-komentoa --lib-lipulla.

Terminal
cargo init counter_program --lib

Siirry projektin hakemistoon. Sinun pitäisi nähdä oletusarvoiset src/lib.rs ja Cargo.toml -tiedostot

Terminal
cd counter_program

Seuraavaksi lisää solana-program riippuvuus. Tämä on vähimmäisriippuvuus, joka vaaditaan Solana-ohjelman rakentamiseen.

Terminal
cargo add solana-program@1.18.26

Seuraavaksi lisää seuraava katkelma tiedostoon Cargo.toml. Jos et sisällytä tätä konfiguraatiota, target/deploy hakemistoa ei luoda, kun rakennat ohjelman.

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

Cargo.toml tiedostosi pitäisi näyttää seuraavalta:

Cargo.toml
[package]
name = "counter_program"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[dependencies]
solana-program = "1.18.26"

Ohjelman sisäänkäyntipiste

Solana-ohjelman sisäänkäyntipiste on funktio, jota kutsutaan, kun ohjelma käynnistetään. Sisäänkäyntipisteellä on seuraava raaka määritelmä, ja kehittäjät voivat vapaasti luoda oman toteutuksensa sisäänkäyntipistefunktiosta.

Yksinkertaisuuden vuoksi käytä entrypoint! makroa solana_program paketista määrittääksesi sisäänkäyntipisteen ohjelmaasi.

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

Korvaa oletuskoodi tiedostossa lib.rs seuraavalla koodilla. Tämä katkelma:

  1. Tuo tarvittavat riippuvuudet paketista solana_program
  2. Määrittää ohjelman sisäänkäyntipisteen käyttäen entrypoint! makroa
  3. Toteuttaa process_instruction funktion, joka ohjaa käskyt asianmukaisiin käsittelijäfunktioihin
lib.rs
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 logic
Ok(())
}

entrypoint! makro vaatii funktion, jolla on seuraava tyyppiallekirjoitus argumenttina:

pub type ProcessInstruction =
fn(program_id: &Pubkey, accounts: &[AccountInfo], instruction_data: &[u8]) -> ProgramResult;

Kun Solana-ohjelma käynnistetään, sisäänkäyntipiste purkaa syötetiedot (jotka on annettu tavuina) kolmeksi arvoksi ja välittää ne process_instruction funktiolle:

  • program_id: Käynnistettävän ohjelman (nykyisen ohjelman) julkinen avain
  • accounts: AccountInfo tileille, joita käynnistettävä käsky vaatii
  • instruction_data: Ohjelmalle välitettävät lisätiedot, jotka määrittävät suoritettavan käskyn ja sen vaatimat argumentit

Nämä kolme parametria vastaavat suoraan tietoja, jotka asiakkaiden on annettava rakentaessaan käskyä ohjelman käynnistämiseksi.

Määrittele ohjelman tila

Kun rakennat Solana-ohjelmaa, aloitat tyypillisesti määrittelemällä ohjelmasi tilan - tiedot, jotka tallennetaan ohjelmasi luomiin ja omistamiin tileihin.

Ohjelman tila määritellään Rust-struktuurien avulla, jotka kuvaavat ohjelmasi tilien tietorakennetta. Voit määritellä useita struktuureja edustamaan erilaisia tilityyppejä ohjelmaasi varten.

Kun työskentelet tilien kanssa, tarvitset tavan muuntaa ohjelmasi tietotyyppejä tavuiksi ja takaisin tilin data-kentässä:

  • Serialisointi: Tietotyyppien muuntaminen tavuiksi tallennettavaksi tilin data-kenttään
  • Deserialisointi: Tiliin tallennettujen tavujen muuntaminen takaisin tietotyypeiksi

Vaikka voit käyttää mitä tahansa serialisointiformaattia Solana-ohjelmien kehityksessä, Borsh on yleisesti käytetty. Käyttääksesi Borshia Solana-ohjelmassasi:

  1. Lisää borsh crate riippuvuudeksi Cargo.toml:
Terminal
cargo add borsh
  1. Tuo Borsh-traitit ja käytä derive-makroa toteuttaaksesi traitit struktuureillesi:
use borsh::{BorshSerialize, BorshDeserialize};
// Define struct representing our counter account's data
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
count: u64,
}

Lisää CounterAccount struktuuri lib.rs määritelläksesi ohjelman tilan. Tätä struktuuria käytetään sekä alustus- että kasvatusohjeissa.

lib.rs
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 logic
Ok(())
}
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct CounterAccount {
count: u64,
}

Määrittele ohjeet

Ohjeet viittaavat eri toimintoihin, joita Solana-ohjelmasi voi suorittaa. Ajattele niitä ohjelmasi julkisina API:eina - ne määrittelevät, mitä toimintoja käyttäjät voivat tehdä ollessaan vuorovaikutuksessa ohjelmasi kanssa.

Ohjeet määritellään tyypillisesti Rust-enumina, jossa:

  • Jokainen enum-variantti edustaa eri ohjetta
  • Variantin hyötykuorma edustaa ohjeen parametreja

Huomaa, että Rust enum -variantit numeroidaan implisiittisesti alkaen numerosta 0.

Alla on esimerkki enum-määrittelystä, joka määrittelee kaksi ohjetta:

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 }, // variant 0
IncrementCounter, // variant 1
}

Kun asiakas kutsuu ohjelmaasi, heidän täytyy tarjota instruction data (tavujen puskurina), jossa:

  • Ensimmäinen tavu määrittää, mikä ohjevariantti suoritetaan (0, 1, jne.)
  • Loput tavut sisältävät serialisoidut ohjeparametrit (jos tarpeen)

Instruction datan (tavujen) muuntamiseksi enum-variantiksi on yleistä toteuttaa apumetodi. Tämä metodi:

  1. Erottaa ensimmäisen tavun saadakseen ohjevariantin
  2. Tekee täsmäytyksen variantin perusteella ja jäsentää mahdolliset lisäparametrit jäljellä olevista tavuista
  3. Palauttaa vastaavan enum-variantin

Esimerkiksi unpack -metodi CounterInstruction -enumille:

impl CounterInstruction {
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
// Get the instruction variant from the first byte
let (&variant, rest) = input
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
// Match instruction type and parse the remaining bytes based on the variant
match variant {
0 => {
// For InitializeCounter, parse a u64 from the remaining bytes
let 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),
}
}
}

Lisää seuraava koodi tiedostoon lib.rs määrittääksesi ohjeet laskuriohjelmalle.

lib.rs
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 logic
Ok(())
}
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum CounterInstruction {
InitializeCounter { initial_value: u64 }, // variant 0
IncrementCounter, // variant 1
}
impl CounterInstruction {
pub fn unpack(input: &[u8]) -> Result<Self, ProgramError> {
// Get the instruction variant from the first byte
let (&variant, rest) = input
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
// Match instruction type and parse the remaining bytes based on the variant
match variant {
0 => {
// For InitializeCounter, parse a u64 from the remaining bytes
let 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),
}
}
}

Ohjekäsittelijät

Ohjekäsittelijät viittaavat funktioihin, jotka sisältävät liiketoimintalogiikan kullekin ohjeelle. On yleistä nimetä käsittelijäfunktiot muodossa process_<instruction_name>, mutta voit vapaasti valita minkä tahansa nimeämiskäytännön.

Lisää seuraava koodi tiedostoon lib.rs. Tämä koodi käyttää CounterInstruction -enumia ja unpack -metodia, jotka määriteltiin edellisessä vaiheessa, ohjatakseen saapuvat ohjeet asianmukaisiin käsittelijäfunktioihin:

lib.rs
entrypoint!(process_instruction);
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
// Unpack instruction data
let instruction = CounterInstruction::unpack(instruction_data)?;
// Match instruction type
match 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(())
}

Seuraavaksi lisää toteutus process_initialize_counter -funktiolle. Tämä ohjekäsittelijä:

  1. Luo ja varaa tilan uudelle tilille laskuridatan tallentamista varten
  2. Alustaa tilin datan ohjeelle välitetyllä initial_value -arvolla

lib.rs
// Initialize a new counter account
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)?;
// Size of our counter account
let account_space = 8; // Size in bytes to store a u64
// Calculate minimum balance for rent exemption
let rent = Rent::get()?;
let required_lamports = rent.minimum_balance(account_space);
// Create the counter account
invoke(
&system_instruction::create_account(
payer_account.key, // Account paying for the new account
counter_account.key, // Account to be created
required_lamports, // Amount of lamports to transfer to the new account
account_space as u64, // Size in bytes to allocate for the data field
program_id, // Set program owner to our program
),
&[
payer_account.clone(),
counter_account.clone(),
system_program.clone(),
],
)?;
// Create a new CounterAccount struct with the initial value
let counter_data = CounterAccount {
count: initial_value,
};
// Get a mutable reference to the counter account's data
let mut account_data = &mut counter_account.data.borrow_mut()[..];
// Serialize the CounterAccount struct into the account's data
counter_data.serialize(&mut account_data)?;
msg!("Counter initialized with value: {}", initial_value);
Ok(())
}

Seuraavaksi lisää toteutus process_increment_counter -funktiolle. Tämä ohje kasvattaa olemassa olevan laskuritilin arvoa.

lib.rs
// Update an existing counter's value
fn 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 ownership
if counter_account.owner != program_id {
return Err(ProgramError::IncorrectProgramId);
}
// Mutable borrow the account data
let mut data = counter_account.data.borrow_mut();
// Deserialize the account data into our CounterAccount struct
let mut counter_data: CounterAccount = CounterAccount::try_from_slice(&data)?;
// Increment the counter value
counter_data.count = counter_data
.count
.checked_add(1)
.ok_or(ProgramError::InvalidAccountData)?;
// Serialize the updated counter data back into the account
counter_data.serialize(&mut &mut data[..])?;
msg!("Counter incremented to: {}", counter_data.count);
Ok(())
}

Ohjeiden testaus

Testataksesi ohjelman ohjeita, lisää seuraavat riippuvuudet tiedostoon Cargo.toml.

Terminal
cargo add solana-program-test@1.18.26 --dev
cargo add solana-sdk@1.18.26 --dev
cargo add tokio --dev

Lisää sitten seuraava testimoduuli tiedostoon lib.rs ja suorita cargo test-sbf testien suorittamiseksi. Halutessasi voit käyttää lippua --nocapture nähdäksesi tulosteet.

Terminal
cargo test-sbf -- --nocapture

lib.rs
#[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 account
let counter_keypair = Keypair::new();
let initial_value: u64 = 42;
// Step 1: Initialize the counter
println!("Testing counter initialization...");
// Create initialization instruction
let mut init_instruction_data = vec![0]; // 0 = initialize instruction
init_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 instruction
let 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 data
let 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 counter
println!("Testing counter increment...");
// Create increment instruction
let increment_instruction = Instruction::new_with_bytes(
program_id,
&[1], // 1 = increment instruction
vec![AccountMeta::new(counter_keypair.pubkey(), true)],
);
// Send transaction with increment instruction
let 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 data
let 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);
}
}
}

Esimerkki tulostuksesta:

Terminal
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 success
test test::test_counter_program ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s

Is this page helpful?

Sisällysluettelo

Muokkaa sivua