Rust-Programmstruktur

In Rust geschriebene Solana-Programme haben minimale strukturelle Anforderungen, was Flexibilität bei der Organisation des Codes ermöglicht. 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 (Kontodaten).
  • 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, gehen wir ein einfaches Zählerprogramm durch, 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 in mehrere Dateien aufteilen möchten.

Teil 1: Schreiben des Programms

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

Erstelle ein neues Programm

Erstellen wir zunächst ein neues Rust-Projekt für unser Solana-Programm.

Terminal
$
cargo new counter_program --lib
$
cd counter_program

Sie sollten die Standard-Dateien src/lib.rs und Cargo.toml sehen.

Aktualisieren Sie das Feld edition in Cargo.toml auf 2021. Andernfalls kann beim Erstellen des Programms ein Fehler auftreten.

Füge Abhängigkeiten hinzu

Fügen wir nun die notwendigen Abhängigkeiten für die Erstellung 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 besteht keine Verpflichtung, Borsh zu verwenden. Es ist jedoch eine häufig verwendete Serialisierungsbibliothek für Solana-Programme.

Konfiguriere crate-type

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

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

Wenn Sie diese Konfiguration nicht einschließen, wird das Verzeichnis target/deploy beim Erstellen des Programms nicht generiert.

Richte den Programm-Entrypoint ein

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

Fügen Sie 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 input Daten in die Parameter der process_instruction Funktion.

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

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

Definiere den Programmzustand

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üge den folgenden Code zu lib.rs hinzu:

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

Definiere das Anweisungs-Enum

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

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

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

Implementiere die Deserialisierung von Anweisungen

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

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

Leite Anweisungen an Handler weiter

Aktualisieren wir nun die Hauptfunktion process_instruction, um Anweisungen an die 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üge den folgenden Code zu lib.rs hinzu, aktualisiere die Funktion process_instruction und füge die Handler für die Anweisungen InitializeCounter und IncrementCounter hinzu:

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

Implementiere den Initialize-Handler

Implementieren wir den Handler zum Erstellen und Initialisieren eines neuen Counter-Kontos. Da nur das System Program Konten auf Solana erstellen kann, verwenden wir einen Cross Program Invocation (CPI), also den Aufruf eines anderen Programms aus unserem Programm heraus.

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, wodurch unser Programm die Möglichkeit erhält, in das Konto zu schreiben und die Daten zu initialisieren.

Fügen Sie den folgenden Code zu lib.rs hinzu und aktualisieren Sie die Funktion process_initialize_counter:

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 Produktionsprogramme erforderlich sind.

Inkrement-Handler implementieren

Implementieren wir nun den Handler, der einen bestehenden Zähler erhöht. Diese Anweisung:

  • Liest das Feld data des Kontos für counter_account
  • Deserialisiert es in eine CounterAccount-Struktur
  • Erhöht das Feld count um 1
  • Serialisiert die CounterAccount-Struktur zurück in das Feld data des Kontos

Fügen Sie den folgenden Code zu lib.rs hinzu und aktualisieren Sie die Funktion process_increment_counter:

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 Produktionsprogramme erforderlich sind.

Vollständiges Programm

Herzlichen Glückwunsch! Sie haben ein vollständiges Solana-Programm erstellt, das die grundlegende Struktur demonstriert, die alle Solana-Programme gemeinsam haben:

  • Entrypoint: Definiert, wo die Programmausführung beginnt, und leitet alle eingehenden Anfragen an die entsprechenden Anweisungs-Handler weiter
  • Anweisungsverarbeitung: Definiert Anweisungen und ihre zugehörigen Handler-Funktionen
  • Zustandsverwaltung: Definiert Kontodatenstrukturen 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 besteht darin, das Programm zu testen, um sicherzustellen, dass alles korrekt funktioniert.

Erstelle ein neues Programm

Erstellen wir zunächst ein neues Rust-Projekt für unser Solana-Programm.

Terminal
$
cargo new counter_program --lib
$
cd counter_program

Sie sollten die Standard-Dateien src/lib.rs und Cargo.toml sehen.

Aktualisieren Sie das Feld edition in Cargo.toml auf 2021. Andernfalls kann beim Erstellen des Programms ein Fehler auftreten.

Füge Abhängigkeiten hinzu

Fügen wir nun die notwendigen Abhängigkeiten für die Erstellung 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 besteht keine Verpflichtung, Borsh zu verwenden. Es ist jedoch eine häufig verwendete Serialisierungsbibliothek für Solana-Programme.

Konfiguriere crate-type

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

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

Wenn Sie diese Konfiguration nicht einschließen, wird das Verzeichnis target/deploy beim Erstellen des Programms nicht generiert.

Richte den Programm-Entrypoint ein

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

Fügen Sie 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 input Daten in die Parameter der process_instruction Funktion.

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

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

Definiere den Programmzustand

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üge den folgenden Code zu lib.rs hinzu:

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

Definiere das Anweisungs-Enum

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

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

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

Implementiere die Deserialisierung von Anweisungen

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

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

Leite Anweisungen an Handler weiter

Aktualisieren wir nun die Hauptfunktion process_instruction, um Anweisungen an die 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üge den folgenden Code zu lib.rs hinzu, aktualisiere die Funktion process_instruction und füge die Handler für die Anweisungen InitializeCounter und IncrementCounter hinzu:

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

Implementiere den Initialize-Handler

Implementieren wir den Handler zum Erstellen und Initialisieren eines neuen Counter-Kontos. Da nur das System Program Konten auf Solana erstellen kann, verwenden wir einen Cross Program Invocation (CPI), also den Aufruf eines anderen Programms aus unserem Programm heraus.

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, wodurch unser Programm die Möglichkeit erhält, in das Konto zu schreiben und die Daten zu initialisieren.

Fügen Sie den folgenden Code zu lib.rs hinzu und aktualisieren Sie die Funktion process_initialize_counter:

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 Produktionsprogramme erforderlich sind.

Inkrement-Handler implementieren

Implementieren wir nun den Handler, der einen bestehenden Zähler erhöht. Diese Anweisung:

  • Liest das Feld data des Kontos für counter_account
  • Deserialisiert es in eine CounterAccount-Struktur
  • Erhöht das Feld count um 1
  • Serialisiert die CounterAccount-Struktur zurück in das Feld data des Kontos

Fügen Sie den folgenden Code zu lib.rs hinzu und aktualisieren Sie die Funktion process_increment_counter:

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 Produktionsprogramme erforderlich sind.

Vollständiges Programm

Herzlichen Glückwunsch! Sie haben ein vollständiges Solana-Programm erstellt, das die grundlegende Struktur demonstriert, die alle Solana-Programme gemeinsam haben:

  • Entrypoint: Definiert, wo die Programmausführung beginnt, und leitet alle eingehenden Anfragen an die entsprechenden Anweisungs-Handler weiter
  • Anweisungsverarbeitung: Definiert Anweisungen und ihre zugehörigen Handler-Funktionen
  • Zustandsverwaltung: Definiert Kontodatenstrukturen 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 besteht darin, 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, mit dem wir Programme testen können, ohne sie auf einem Cluster bereitzustellen.

Test-Abhängigkeiten hinzufügen

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

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

Test-Modul erstellen

Jetzt fügen wir unserem Programm ein Test-Modul hinzu. Wir beginnen mit dem grundlegenden Gerüst und den Imports.

Fügen Sie den folgenden Code zu lib.rs direkt unterhalb des Programmcodes hinzu:

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.

Test-Umgebung initialisieren

Richten wir die Test-Umgebung mit LiteSVM ein und finanzieren ein Payer-Konto.

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

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

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 Test-Umgebung laden. Führen Sie den Befehl cargo build-sbf aus, um das Programm zu erstellen. Dadurch wird die Datei counter_program.so im Verzeichnis target/deploy generiert.

Terminal
$
cargo build-sbf

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

Nach dem Build können wir das Programm laden.

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

Sie müssen cargo build-sbf ausführen, bevor Sie Tests durchführen, um die Datei .so zu generieren. Der Test lädt das kompilierte Programm.

Initialisierungsanweisung testen

Testen wir die Initialisierungsanweisung, indem wir ein neues Counter-Konto mit einem Startwert erstellen.

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

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 überprüfen wir, ob das Counter-Konto korrekt mit dem erwarteten Wert erstellt wurde.

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

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

Increment-Anweisung testen

Testen wir nun die Increment-Anweisung, um sicherzustellen, dass sie den Counter-Wert korrekt aktualisiert.

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

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

Abschließend überprüfen wir, ob das Increment korrekt funktioniert hat, indem wir den aktualisierten Counter-Wert prüfen.

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

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ühre 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

Test-Abhängigkeiten hinzufügen

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

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

Test-Modul erstellen

Jetzt fügen wir unserem Programm ein Test-Modul hinzu. Wir beginnen mit dem grundlegenden Gerüst und den Imports.

Fügen Sie den folgenden Code zu lib.rs direkt unterhalb des Programmcodes hinzu:

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.

Test-Umgebung initialisieren

Richten wir die Test-Umgebung mit LiteSVM ein und finanzieren ein Payer-Konto.

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

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

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 Test-Umgebung laden. Führen Sie den Befehl cargo build-sbf aus, um das Programm zu erstellen. Dadurch wird die Datei counter_program.so im Verzeichnis target/deploy generiert.

Terminal
$
cargo build-sbf

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

Nach dem Build können wir das Programm laden.

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

Sie müssen cargo build-sbf ausführen, bevor Sie Tests durchführen, um die Datei .so zu generieren. Der Test lädt das kompilierte Programm.

Initialisierungsanweisung testen

Testen wir die Initialisierungsanweisung, indem wir ein neues Counter-Konto mit einem Startwert erstellen.

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

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 überprüfen wir, ob das Counter-Konto korrekt mit dem erwarteten Wert erstellt wurde.

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

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

Increment-Anweisung testen

Testen wir nun die Increment-Anweisung, um sicherzustellen, dass sie den Counter-Wert korrekt aktualisiert.

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

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

Abschließend überprüfen wir, ob das Increment korrekt funktioniert hat, indem wir den aktualisierten Counter-Wert prüfen.

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

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ühre 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: Aufruf des Programms

Fügen wir nun ein Client-Skript hinzu, um das Programm aufzurufen.

Client-Beispiel erstellen

Erstellen wir einen Rust-Client, um mit unserem bereitgestellten Programm zu interagieren.

Terminal
$
mkdir examples
$
touch examples/client.rs

Füge die folgende Konfiguration zu Cargo.toml hinzu:

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

Installiere die Client-Abhängigkeiten:

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

Client-Code implementieren

Implementieren wir nun den Client, der unser bereitgestelltes Programm aufruft.

Führe den folgenden Befehl aus, um deine Programm-ID aus der Keypair-Datei zu erhalten:

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

Füge den Client-Code zu examples/client.rs hinzu und ersetze die program_id durch die 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

Erstellen wir einen Rust-Client, um mit unserem bereitgestellten Programm zu interagieren.

Terminal
$
mkdir examples
$
touch examples/client.rs

Füge die folgende Konfiguration zu Cargo.toml hinzu:

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

Installiere die Client-Abhängigkeiten:

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

Client-Code implementieren

Implementieren wir nun den Client, der unser bereitgestelltes Programm aufruft.

Führe den folgenden Befehl aus, um deine Programm-ID aus der Keypair-Datei zu erhalten:

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

Füge den Client-Code zu examples/client.rs hinzu und ersetze die program_id durch die 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: Das Programm bereitstellen

Jetzt, da wir unser Programm und unseren Client fertig haben, bauen, deployen und rufen wir das Programm auf.

Das Programm bauen

Zuerst bauen wir unser Programm.

Terminal
$
cargo build-sbf

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

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

Sie können die ID Ihres Programms anzeigen, indem Sie den folgenden Befehl ausführen:

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.

Konfigurieren Sie 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

Starten Sie nun den Test-Validator in einem separaten Terminal:

Terminal
$
solana-test-validator

Deployment des Programms

Während der Validator läuft, deployen Sie Ihr Programm auf den lokalen Cluster:

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

Beispielausgabe:

Program Id: HQ5Q2XXqbTKKQsWPtLzMn7rDhM8v9UPYPe7DfSoFQqJF
Signature: 5xKdnh3dDFnZXB5UevYYkFBpCVcuqo5SaUPLnryFWY7eQD2CJxaeVDKjQ4ezQVJfkGNqZGYqMZBNqymPKwCQQx5h

Sie können das Deployment mit dem Befehl solana program show und Ihrer 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

Ausführen des Clients

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

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

Während der lokale Validator läuft, können Sie die Transaktionen im 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

Verwaltet von

© 2026 Solana Foundation.
Alle Rechte vorbehalten.
Verbinden Sie sich