Rust-Programmstruktur

In Rust geschriebene Solana-Programme haben minimale strukturelle Anforderungen und bieten Flexibilität bei der Organisation des Codes. Die einzige Anforderung ist, dass ein Programm einen entrypoint haben muss, der definiert, wo die Ausführung eines Programms beginnt.

Programmstruktur

Obwohl es keine strengen Regeln für die Dateistruktur gibt, folgen Solana-Programme typischerweise einem gemeinsamen Muster:

  • entrypoint.rs: Definiert den Einstiegspunkt, der eingehende Anweisungen weiterleitet.
  • state.rs: Definiert den Programmzustand (Kontendaten).
  • instructions.rs: Definiert die Anweisungen, die das Programm ausführen kann.
  • processor.rs: Definiert die Anweisungshandler (Funktionen), die die Geschäftslogik für jede Anweisung implementieren.
  • error.rs: Definiert benutzerdefinierte Fehler, die das Programm zurückgeben kann.

Siehe zum Beispiel das Token-Programm.

Beispielprogramm

Um zu demonstrieren, wie man ein natives Rust-Programm mit mehreren Anweisungen erstellt, werden wir ein einfaches Zählerprogramm durchgehen, das zwei Anweisungen implementiert:

  1. InitializeCounter: Erstellt und initialisiert ein neues Konto mit einem Anfangswert.
  2. IncrementCounter: Erhöht den in einem bestehenden Konto gespeicherten Wert.

Der Einfachheit halber wird das Programm in einer einzigen lib.rs-Datei implementiert, obwohl Sie in der Praxis größere Programme möglicherweise auf mehrere Dateien aufteilen möchten.

Teil 1: Das Programm schreiben

Beginnen wir mit dem Aufbau des Zählerprogramms. Wir erstellen ein Programm, das einen Zähler mit einem Startwert initialisieren und erhöhen kann.

Erstellen eines neuen Programms

Zuerst erstellen wir ein neues Rust-Projekt für unser Solana-Programm.

Terminal
$
cargo new counter_program --lib
$
cd counter_program

Du solltest die Standard-Dateien src/lib.rs und Cargo.toml sehen.

Aktualisiere das Feld edition in Cargo.toml auf 2021. Andernfalls könntest du beim Erstellen des Programms auf einen Fehler stoßen.

Abhängigkeiten hinzufügen

Jetzt fügen wir die notwendigen Abhängigkeiten für die Entwicklung eines Solana-Programms hinzu. Wir benötigen solana-program für das Core SDK und borsh für die Serialisierung.

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

Es gibt keine Verpflichtung, Borsh zu verwenden. Es ist jedoch eine häufig verwendete Serialisierungsbibliothek für Solana-Programme.

Crate-Type konfigurieren

Solana-Programme müssen als dynamische Bibliotheken kompiliert werden. Füge den Abschnitt [lib] hinzu, um zu konfigurieren, wie Cargo das Programm erstellt.

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

Wenn du diese Konfiguration nicht einschließt, wird das Verzeichnis target/deploy nicht generiert, wenn du das Programm erstellst.

Programm-Einstiegspunkt einrichten

Jedes Solana-Programm hat einen Einstiegspunkt, also die Funktion, die aufgerufen wird, wenn das Programm ausgeführt wird. Beginnen wir damit, die Imports hinzuzufügen, die wir für das Programm benötigen, und den Einstiegspunkt einzurichten.

Füge den folgenden Code zu lib.rs hinzu:

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

Das entrypoint Makro übernimmt die Deserialisierung der inputDaten in die Parameter der process_instructionFunktion.

Ein Solana-Programm entrypoint hat die folgende Funktionssignatur. Entwickler können ihre eigene Implementierung der entrypointFunktion erstellen.

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

Programmzustand definieren

Jetzt definieren wir die Datenstruktur, die in unseren Counter-Konten gespeichert wird. Dies sind die Daten, die im Feld data des Kontos gespeichert werden.

Fügen Sie den folgenden Code zu lib.rs hinzu:

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

Anweisungs-Enum definieren

Definieren wir die Anweisungen, die unser Programm ausführen kann. Wir verwenden ein Enum, bei dem jede Variante eine andere Anweisung darstellt.

Fügen Sie den folgenden Code zu lib.rs hinzu:

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

Implementierung der Anweisungs-Deserialisierung

Jetzt müssen wir die instruction_data (Rohbytes) in eine unserer CounterInstruction Enum-Varianten deserialisieren. Die Borsh-Methode try_from_slice übernimmt diese Umwandlung automatisch.

Aktualisieren Sie die Funktion process_instruction, um die Borsh-Deserialisierung zu verwenden:

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

Anweisungen an Handler weiterleiten

Jetzt aktualisieren wir die Hauptfunktion process_instruction, um Anweisungen an ihre entsprechenden Handler-Funktionen weiterzuleiten.

Dieses Routing-Muster ist in Solana-Programmen üblich. Die instruction_data wird in eine Variante eines Enums deserialisiert, das die Anweisung repräsentiert, dann wird die entsprechende Handler-Funktion aufgerufen. Jede Handler-Funktion enthält die Implementierung für diese Anweisung.

Fügen Sie den folgenden Code zu lib.rs hinzu, um die Funktion process_instruction zu aktualisieren und die Handler für die Anweisungen InitializeCounter und IncrementCounter hinzuzufügen:

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

Initialisierungs-Handler implementieren

Implementieren wir den Handler, um ein neues Counter-Konto zu erstellen und zu initialisieren. Da nur das System Program Konten auf Solana erstellen kann, verwenden wir eine Cross Program Invocation (CPI), im Wesentlichen rufen wir ein anderes Programm von unserem Programm aus auf.

Unser Programm führt einen CPI aus, um die Anweisung create_account des System Program aufzurufen. Das neue Konto wird mit unserem Programm als Eigentümer erstellt, was unserem Programm die Möglichkeit gibt, in das Konto zu schreiben und die Daten zu initialisieren.

Füge den folgenden Code zu lib.rs hinzu, um die process_initialize_counter Funktion zu aktualisieren:

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

Diese Anweisung dient nur zu Demonstrationszwecken. Sie enthält keine Sicherheits- und Validierungsprüfungen, die für Programme im Produktivbetrieb erforderlich sind.

Implementiere den Increment-Handler

Jetzt implementieren wir den Handler, der einen vorhandenen Zähler erhöht. Diese Anweisung:

  • Liest das dataFeld des Konten für das counter_account
  • Deserialisiert es in eine CounterAccountStruktur
  • Erhöht das countFeld um 1
  • Serialisiert die CounterAccountStruktur zurück in das dataFeld des Konten

Füge den folgenden Code zu lib.rs hinzu, um die process_increment_counter Funktion zu aktualisieren:

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

Diese Anweisung dient nur zu Demonstrationszwecken. Sie enthält keine Sicherheits- und Validierungsprüfungen, die für Programme im Produktivbetrieb erforderlich sind.

Fertiges Programm

Glückwunsch! Du hast ein vollständiges Solana-Programm erstellt, das die grundlegende Struktur zeigt, die allen Solana-Programmen gemeinsam ist:

  • Entrypoint: Definiert, wo die Programmausführung beginnt und leitet alle eingehenden Anfragen an die entsprechenden Anweisungshandler weiter
  • Anweisungsverarbeitung: Definiert Anweisungen und ihre zugehörigen Handler- Funktionen
  • Zustandsverwaltung: Definiert Datenstrukturen für Konten und verwaltet deren Zustand in programmeigenen Konten
  • Cross Program Invocation (CPI): Ruft das System Program auf, um neue programmeigene Konten zu erstellen

Der nächste Schritt ist, das Programm zu testen, um sicherzustellen, dass alles korrekt funktioniert.

Erstellen eines neuen Programms

Zuerst erstellen wir ein neues Rust-Projekt für unser Solana-Programm.

Terminal
$
cargo new counter_program --lib
$
cd counter_program

Du solltest die Standard-Dateien src/lib.rs und Cargo.toml sehen.

Aktualisiere das Feld edition in Cargo.toml auf 2021. Andernfalls könntest du beim Erstellen des Programms auf einen Fehler stoßen.

Abhängigkeiten hinzufügen

Jetzt fügen wir die notwendigen Abhängigkeiten für die Entwicklung eines Solana-Programms hinzu. Wir benötigen solana-program für das Core SDK und borsh für die Serialisierung.

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

Es gibt keine Verpflichtung, Borsh zu verwenden. Es ist jedoch eine häufig verwendete Serialisierungsbibliothek für Solana-Programme.

Crate-Type konfigurieren

Solana-Programme müssen als dynamische Bibliotheken kompiliert werden. Füge den Abschnitt [lib] hinzu, um zu konfigurieren, wie Cargo das Programm erstellt.

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

Wenn du diese Konfiguration nicht einschließt, wird das Verzeichnis target/deploy nicht generiert, wenn du das Programm erstellst.

Programm-Einstiegspunkt einrichten

Jedes Solana-Programm hat einen Einstiegspunkt, also die Funktion, die aufgerufen wird, wenn das Programm ausgeführt wird. Beginnen wir damit, die Imports hinzuzufügen, die wir für das Programm benötigen, und den Einstiegspunkt einzurichten.

Füge den folgenden Code zu lib.rs hinzu:

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

Das entrypoint Makro übernimmt die Deserialisierung der inputDaten in die Parameter der process_instructionFunktion.

Ein Solana-Programm entrypoint hat die folgende Funktionssignatur. Entwickler können ihre eigene Implementierung der entrypointFunktion erstellen.

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

Programmzustand definieren

Jetzt definieren wir die Datenstruktur, die in unseren Counter-Konten gespeichert wird. Dies sind die Daten, die im Feld data des Kontos gespeichert werden.

Fügen Sie den folgenden Code zu lib.rs hinzu:

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

Anweisungs-Enum definieren

Definieren wir die Anweisungen, die unser Programm ausführen kann. Wir verwenden ein Enum, bei dem jede Variante eine andere Anweisung darstellt.

Fügen Sie den folgenden Code zu lib.rs hinzu:

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

Implementierung der Anweisungs-Deserialisierung

Jetzt müssen wir die instruction_data (Rohbytes) in eine unserer CounterInstruction Enum-Varianten deserialisieren. Die Borsh-Methode try_from_slice übernimmt diese Umwandlung automatisch.

Aktualisieren Sie die Funktion process_instruction, um die Borsh-Deserialisierung zu verwenden:

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

Anweisungen an Handler weiterleiten

Jetzt aktualisieren wir die Hauptfunktion process_instruction, um Anweisungen an ihre entsprechenden Handler-Funktionen weiterzuleiten.

Dieses Routing-Muster ist in Solana-Programmen üblich. Die instruction_data wird in eine Variante eines Enums deserialisiert, das die Anweisung repräsentiert, dann wird die entsprechende Handler-Funktion aufgerufen. Jede Handler-Funktion enthält die Implementierung für diese Anweisung.

Fügen Sie den folgenden Code zu lib.rs hinzu, um die Funktion process_instruction zu aktualisieren und die Handler für die Anweisungen InitializeCounter und IncrementCounter hinzuzufügen:

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

Initialisierungs-Handler implementieren

Implementieren wir den Handler, um ein neues Counter-Konto zu erstellen und zu initialisieren. Da nur das System Program Konten auf Solana erstellen kann, verwenden wir eine Cross Program Invocation (CPI), im Wesentlichen rufen wir ein anderes Programm von unserem Programm aus auf.

Unser Programm führt einen CPI aus, um die Anweisung create_account des System Program aufzurufen. Das neue Konto wird mit unserem Programm als Eigentümer erstellt, was unserem Programm die Möglichkeit gibt, in das Konto zu schreiben und die Daten zu initialisieren.

Füge den folgenden Code zu lib.rs hinzu, um die process_initialize_counter Funktion zu aktualisieren:

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

Diese Anweisung dient nur zu Demonstrationszwecken. Sie enthält keine Sicherheits- und Validierungsprüfungen, die für Programme im Produktivbetrieb erforderlich sind.

Implementiere den Increment-Handler

Jetzt implementieren wir den Handler, der einen vorhandenen Zähler erhöht. Diese Anweisung:

  • Liest das dataFeld des Konten für das counter_account
  • Deserialisiert es in eine CounterAccountStruktur
  • Erhöht das countFeld um 1
  • Serialisiert die CounterAccountStruktur zurück in das dataFeld des Konten

Füge den folgenden Code zu lib.rs hinzu, um die process_increment_counter Funktion zu aktualisieren:

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

Diese Anweisung dient nur zu Demonstrationszwecken. Sie enthält keine Sicherheits- und Validierungsprüfungen, die für Programme im Produktivbetrieb erforderlich sind.

Fertiges Programm

Glückwunsch! Du hast ein vollständiges Solana-Programm erstellt, das die grundlegende Struktur zeigt, die allen Solana-Programmen gemeinsam ist:

  • Entrypoint: Definiert, wo die Programmausführung beginnt und leitet alle eingehenden Anfragen an die entsprechenden Anweisungshandler weiter
  • Anweisungsverarbeitung: Definiert Anweisungen und ihre zugehörigen Handler- Funktionen
  • Zustandsverwaltung: Definiert Datenstrukturen für Konten und verwaltet deren Zustand in programmeigenen Konten
  • Cross Program Invocation (CPI): Ruft das System Program auf, um neue programmeigene Konten zu erstellen

Der nächste Schritt ist, das Programm zu testen, um sicherzustellen, dass alles korrekt funktioniert.

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

Teil 2: Testen des Programms

Jetzt testen wir unser Counter-Programm. Wir verwenden LiteSVM, ein Test-Framework, das uns ermöglicht, Programme zu testen, ohne sie auf einem Cluster zu deployen.

Füge Test-Abhängigkeiten hinzu

Zuerst fügen wir die für das Testen benötigten Abhängigkeiten hinzu. Wir verwenden litesvm für das Testen und solana-sdk.

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

Testmodul erstellen

Jetzt fügen wir unserem Programm ein Testmodul hinzu. Wir beginnen mit dem grundlegenden Gerüst und den Importen.

Füge den folgenden Code zu lib.rs hinzu, direkt unter dem Programmcode:

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

Das Attribut #[cfg(test)] stellt sicher, dass dieser Code nur beim Ausführen von Tests kompiliert wird.

Testumgebung initialisieren

Richten wir die Testumgebung mit LiteSVM ein und finanzieren ein Zahlerkonto.

LiteSVM simuliert die Solana-Laufzeitumgebung und ermöglicht es uns, unser Programm zu testen, ohne es auf einem echten Cluster bereitzustellen.

Füge den folgenden Code zu lib.rs hinzu und aktualisiere die Funktion 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");

Programm laden

Jetzt müssen wir unser Programm erstellen und in die Testumgebung laden. Führe den Befehl cargo build-sbf aus, um das Programm zu erstellen. Dies generiert die Datei counter_program.so im Verzeichnis target/deploy.

Terminal
$
cargo build-sbf

Stelle sicher, dass edition in Cargo.toml auf 2021 gesetzt ist.

Nach dem Erstellen können wir das Programm laden.

Aktualisiere die Funktion test_counter_program, um das Programm in die Testumgebung zu laden.

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

Du musst cargo build-sbf ausführen, bevor du Tests startest, um die Datei .so zu generieren. Der Test lädt das kompilierte Programm.

Initialisierungs-Anweisung testen

Testen wir die Initialisierungs-Anweisung, indem wir ein neues Zählerkonto mit einem Startwert erstellen.

Fügen Sie den folgenden Code zu lib.rs hinzu, um die Funktion test_counter_program zu aktualisieren:

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

Initialisierung überprüfen

Nach der Initialisierung sollten wir überprüfen, ob das Konten-Konto korrekt mit dem erwarteten Wert erstellt wurde.

Fügen Sie den folgenden Code zu lib.rs hinzu, um die Funktion test_counter_program zu aktualisieren:

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

Inkrement-Anweisung testen

Jetzt testen wir die Inkrement-Anweisung, um sicherzustellen, dass sie den Zählerwert korrekt aktualisiert.

Fügen Sie den folgenden Code zu lib.rs hinzu, um die Funktion test_counter_program zu aktualisieren:

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

Endergebnisse überprüfen

Zum Schluss überprüfen wir, ob die Inkrementierung korrekt funktioniert hat, indem wir den aktualisierten Zählerwert kontrollieren.

Fügen Sie den folgenden Code zu lib.rs hinzu, um die Funktion test_counter_program zu aktualisieren:

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

Führen Sie die Tests mit dem folgenden Befehl aus. Das Flag --nocapture gibt die Ausgabe des Tests aus.

Terminal
$
cargo test -- --nocapture

Erwartete Ausgabe:

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

Füge Test-Abhängigkeiten hinzu

Zuerst fügen wir die für das Testen benötigten Abhängigkeiten hinzu. Wir verwenden litesvm für das Testen und solana-sdk.

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

Testmodul erstellen

Jetzt fügen wir unserem Programm ein Testmodul hinzu. Wir beginnen mit dem grundlegenden Gerüst und den Importen.

Füge den folgenden Code zu lib.rs hinzu, direkt unter dem Programmcode:

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

Das Attribut #[cfg(test)] stellt sicher, dass dieser Code nur beim Ausführen von Tests kompiliert wird.

Testumgebung initialisieren

Richten wir die Testumgebung mit LiteSVM ein und finanzieren ein Zahlerkonto.

LiteSVM simuliert die Solana-Laufzeitumgebung und ermöglicht es uns, unser Programm zu testen, ohne es auf einem echten Cluster bereitzustellen.

Füge den folgenden Code zu lib.rs hinzu und aktualisiere die Funktion 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");

Programm laden

Jetzt müssen wir unser Programm erstellen und in die Testumgebung laden. Führe den Befehl cargo build-sbf aus, um das Programm zu erstellen. Dies generiert die Datei counter_program.so im Verzeichnis target/deploy.

Terminal
$
cargo build-sbf

Stelle sicher, dass edition in Cargo.toml auf 2021 gesetzt ist.

Nach dem Erstellen können wir das Programm laden.

Aktualisiere die Funktion test_counter_program, um das Programm in die Testumgebung zu laden.

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

Du musst cargo build-sbf ausführen, bevor du Tests startest, um die Datei .so zu generieren. Der Test lädt das kompilierte Programm.

Initialisierungs-Anweisung testen

Testen wir die Initialisierungs-Anweisung, indem wir ein neues Zählerkonto mit einem Startwert erstellen.

Fügen Sie den folgenden Code zu lib.rs hinzu, um die Funktion test_counter_program zu aktualisieren:

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

Initialisierung überprüfen

Nach der Initialisierung sollten wir überprüfen, ob das Konten-Konto korrekt mit dem erwarteten Wert erstellt wurde.

Fügen Sie den folgenden Code zu lib.rs hinzu, um die Funktion test_counter_program zu aktualisieren:

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

Inkrement-Anweisung testen

Jetzt testen wir die Inkrement-Anweisung, um sicherzustellen, dass sie den Zählerwert korrekt aktualisiert.

Fügen Sie den folgenden Code zu lib.rs hinzu, um die Funktion test_counter_program zu aktualisieren:

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

Endergebnisse überprüfen

Zum Schluss überprüfen wir, ob die Inkrementierung korrekt funktioniert hat, indem wir den aktualisierten Zählerwert kontrollieren.

Fügen Sie den folgenden Code zu lib.rs hinzu, um die Funktion test_counter_program zu aktualisieren:

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

Führen Sie die Tests mit dem folgenden Befehl aus. Das Flag --nocapture gibt die Ausgabe des Tests aus.

Terminal
$
cargo test -- --nocapture

Erwartete Ausgabe:

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"

Teil 3: Das Programm aufrufen

Jetzt fügen wir ein Client-Skript hinzu, um das Programm aufzurufen.

Client-Beispiel erstellen

Lassen Sie uns einen Rust-Client erstellen, um mit unserem bereitgestellten Programm zu interagieren.

Terminal
$
mkdir examples
$
touch examples/client.rs

Fügen Sie die folgende Konfiguration zu Cargo.toml hinzu:

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

Installieren Sie die Client-Abhängigkeiten:

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

Client-Code implementieren

Jetzt implementieren wir den Client, der unser bereitgestelltes Programm aufrufen wird.

Führen Sie den folgenden Befehl aus, um Ihre Programm-ID aus der keypair-Datei zu erhalten:

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

Fügen Sie den Client-Code zu examples/client.rs hinzu und ersetzen Sie die program_id mit der Ausgabe des vorherigen Befehls:

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

Client-Beispiel erstellen

Lassen Sie uns einen Rust-Client erstellen, um mit unserem bereitgestellten Programm zu interagieren.

Terminal
$
mkdir examples
$
touch examples/client.rs

Fügen Sie die folgende Konfiguration zu Cargo.toml hinzu:

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

Installieren Sie die Client-Abhängigkeiten:

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

Client-Code implementieren

Jetzt implementieren wir den Client, der unser bereitgestelltes Programm aufrufen wird.

Führen Sie den folgenden Befehl aus, um Ihre Programm-ID aus der keypair-Datei zu erhalten:

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

Fügen Sie den Client-Code zu examples/client.rs hinzu und ersetzen Sie die program_id mit der Ausgabe des vorherigen Befehls:

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"

Teil 4: Bereitstellung des Programms

Nachdem wir unser Programm und den Client vorbereitet haben, lassen Sie uns das Programm bauen, bereitstellen und aufrufen.

Programm bauen

Zuerst bauen wir unser Programm.

Terminal
$
cargo build-sbf

Dieser Befehl kompiliert dein Programm und generiert zwei wichtige Dateien in target/deploy/:

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

Du kannst die ID deines Programms anzeigen, indem du folgenden Befehl ausführst:

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

Beispielausgabe:

HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

Lokalen Validator starten

Für die Entwicklung verwenden wir einen lokalen Test-Validator.

Konfiguriere zunächst die Solana CLI für die Verwendung von localhost:

Terminal
$
solana config set -ul

Beispielausgabe:

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

Starte nun den Test-Validator in einem separaten Terminal:

Terminal
$
solana-test-validator

Programm deployen

Wenn der Validator läuft, deploye dein Programm auf den lokalen Cluster:

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

Beispielausgabe:

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Signature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h

Du kannst das Deployment mit dem Befehl solana program show und deiner Programm-ID überprüfen:

Terminal
$
solana program show HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF

Beispielausgabe:

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

Client ausführen

Führen Sie den Client aus, während der lokale Validator noch läuft:

Terminal
$
cargo run --example client

Erwartete Ausgabe:

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

Mit dem laufenden lokalen Validator können Sie die Transaktionen auf dem Solana Explorer anhand der ausgegebenen Transaktionssignaturen einsehen. Beachten Sie, dass der Cluster im Solana Explorer auf "Custom RPC URL" eingestellt sein muss, was standardmäßig auf http://localhost:8899 verweist, auf dem der solana-test-validator läuft.

Is this page helpful?

Inhaltsverzeichnis

Seite bearbeiten