Dein erstes Solana-Programm erstellen

Dieses Schnellstart-Tutorial verwendet das Anchor-Framework Starterprojekt, das von anchor init generiert wird. Du erstellst das Projekt lokal, führst seine Tests aus, baust das Programm und gehst den Programmcode durch, den Anchor generiert.

Voraussetzungen

Bevor du beginnst, installiere die Solana-Entwicklungswerkzeuge. Die Installation umfasst Rust, die Solana CLI und die Anchor CLI.

Verwende Anchor CLI Version 1.1.2 oder höher für diese Vorlage. Überprüfe deine installierte Version:

Terminal
$
anchor --version

Projekt erstellen

Führe die folgenden Befehle in deinem Terminal aus:

Terminal
$
anchor init my-program
$
cd my-program

Das Starterprojekt enthält ein Solana-Programm unter programs/my-program. Das Programm enthält zwei Anweisungen: eine zum Initialisieren eines Zähler-Konten und eine zum Erhöhen des Zählers.

Einige Teile der Vorlage veranschaulichen gängige Solana-Programmierungsmuster: Ableiten von PDA-Konten-Adressen, Durchführen einer Cross Program Invocation (CPI) zum Übertragen von SOL und Verwenden benutzerdefinierter Fehlerprüfungen, um eine Anweisung zu stoppen, wenn eine Bedingung nicht erfüllt ist.

Anchor.toml
Cargo.toml
Cargo.toml
lib.rs
constants.rs
error.rs
instructions.rs
initialize.rs
increment.rs
state.rs
test_initialize.rs

Programm bauen

Führe anchor build aus, um das Starterprogramm zu kompilieren:

Terminal
$
anchor build

Das kompilierte Programm wird in target/deploy/my_program.so geschrieben. Wenn das Programm bereitgestellt wird, werden die Inhalte dieser .so-Datei in einem Konten onchain gespeichert.

Test ausführen

Führen Sie den Standardtest aus:

Terminal
$
anchor test

Das Anchor.toml dieses Templates verwendet den Rust-Testbefehl:

Anchor.toml
skip_local_validator = true
[scripts]
test = "cargo test"

Der Test lädt das kompilierte Programm in LiteSVM, erstellt einen Zahler, sendet die initialize- und increment Anweisungen und überprüft dann den Zustand des Zähler-Konten.

Das Ausführen von anchor test kompiliert das Programm ebenfalls, sodass Sie beim lokalen Testen anchor build nicht zuerst ausführen müssen.

Programm deployen

Lokale Tests bieten die schnellste Feedbackschleife. Wenn Sie bereit sind, auf einem Netzwerk zu deployen, beispielsweise auf dem Devnet, erstellen Sie zunächst einen Build und deployen Sie dann auf einem Cluster.

Das Deployment eines Solana-Programms erfordert SOL, da das Programm in einem program account gespeichert wird und der program account für den genutzten Speicherplatz bezahlen muss. Auf dem Devnet können Sie kostenloses Devnet-SOL über den Solana Faucet oder mit der Solana CLI anfordern:

Terminal
$
solana airdrop 2 --url devnet
Terminal
$
anchor build
$
anchor deploy --provider.cluster devnet

Quelldateien

Das Verzeichnis src enthält das Solana-Programm. Anchors Programmstruktur Dokumentation erklärt die hier verwendeten Kern-Makros, einschließlich declare_id!, #[program], #[derive(Accounts)] und #[account]. Dieser Abschnitt führt durch die Template-Dateien.

lib.rs

lib.rs ist der Einstiegspunkt des Programms. Es verbindet die Quelldateien, definiert die Programmadresse und definiert die Programm- Anweisungen, die Benutzer aufrufen können.

programs/my-program/src/lib.rs
pub mod constants;
pub mod error;
pub mod instructions;
pub mod state;
use anchor_lang::prelude::*;
pub use constants::*;
pub use instructions::*;
pub use state::*;
declare_id!("82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd");
#[program]
pub mod my_program {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
crate::instructions::initialize::handle_initialize(ctx)
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
crate::instructions::increment::handle_increment(ctx)
}
}
Anchor.toml
[programs.localnet]
my_program = "82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd"
lib.rs
declare_id!("82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd");

Dieselbe Programmadresse erscheint in der Konfiguration und im Code. Anchor.toml teilt Anchor mit, welche Adresse für ein Cluster deployed oder aufgerufen werden soll. declare_id! definiert die Programmadresse im Programm für Sicherheitsprüfungen.

constants.rs

constants.rs hält gemeinsam genutzte Werte an einem Ort. In dieser Vorlage leitet COUNTER_SEED die Counter-PDA ab, HELLO_WORLD_LAMPORTS wird bei der Initialisierung übertragen, und MAX_COUNT wird vor dem Inkrementieren geprüft.

programs/my-program/src/constants.rs
use anchor_lang::prelude::*;
#[constant]
pub const COUNTER_SEED: &[u8] = b"counter";
#[constant]
pub const HELLO_WORLD_LAMPORTS: u64 = 1;
#[constant]
pub const MAX_COUNT: u64 = 10;
initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
// ...
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
// ...
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
// ...
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
Ok(())
}

initialize.rs verwendet zwei Konstanten:

  • COUNTER_SEED leitet die PDA-Adresse des Zählers ab.
  • HELLO_WORLD_LAMPORTS legt den Betrag fest, der vom Zahler auf das Zählerkonto übertragen wird.
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
// ...
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
Ok(())
}

increment.rs verwendet MAX_COUNT als obere Grenze des Zählers. Wenn der aktuelle Zählerstand bereits das Maximum erreicht hat, gibt require! den Wert CounterOverflow zurück und die Kontodaten werden nicht geändert.

state.rs

state.rs definiert benutzerdefinierte Datentypen für Konten, die das Programm erstellt und verwaltet. Das Programm definiert Anweisungen zum Erstellen, Initialisieren und Aktualisieren dieser Daten, die Zählerdaten werden jedoch nicht im Programm selbst gespeichert. Sie werden in einem separaten Konto mit eigener Adresse gespeichert.

programs/my-program/src/state.rs
use anchor_lang::prelude::*;
#[account]
#[derive(InitSpace)]
pub struct Counter {
pub count: u64,
pub authority: Pubkey,
}
initialize.rs
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
// ...
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
Ok(())
}
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
// ...
ctx.accounts.counter.count += 1;
Ok(())
}

state.rs definiert die Counter Kontendaten. Die Anweisungsdateien verwenden diesen Typ beim Erstellen und Aktualisieren der Konten:

  • Counter::INIT_SPACE legt die Größe der Konten für die in state.rs definierten Felder fest.
  • count und authority sind die Feldwerte, die beim Initialisieren der Konten geschrieben werden.
  • count += 1 aktualisiert den gespeicherten Zählerwert, nachdem die Validatoren erfolgreich abgeschlossen wurden.

error.rs

error.rs definiert die benutzerdefinierten Fehler des Programms. In dieser Vorlage zeigen die Fehler, wie Anweisungs-Handler abbrechen, wenn ein Aufrufer nicht berechtigt ist, den Zähler zu aktualisieren, oder der Zähler bereits MAX_COUNT erreicht hat.

programs/my-program/src/error.rs
use anchor_lang::prelude::*;
#[error_code]
pub enum ErrorCode {
#[msg("Only the counter authority can update this counter")]
Unauthorized,
#[msg("Counter has reached the maximum value")]
CounterOverflow,
}
increment.rs
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

error.rs benennt die Fehler, die die Anweisung zurückgeben kann:

  • ErrorCode::Unauthorized wird zurückgegeben, wenn der Signer nicht die im Zähler-Konten gespeicherte Autorität ist.
  • ErrorCode::CounterOverflow wird zurückgegeben, wenn der Zähler bereits MAX_COUNT erreicht hat.

Anweisungen.rs

instructions.rs verbindet die Anweisungen-Dateien mit der Programm-Crate, damit lib.rs auf den initialize- und increment-Anweisungs-Code zugreifen kann. Jede Anweisungs-Datei definiert die Konten, die von dieser Anweisung benötigt werden, sowie die Handler-Logik, die ausgeführt wird, nachdem Anchor diese Konten validiert hat.

programs/my-program/src/instructions.rs
pub mod initialize;
pub mod increment;
pub use initialize::*;
pub use increment::*;

initialize.rs

initialize.rs definiert die Konten, die zum Erstellen des Zähler-Kontos erforderlich sind, und schreibt dann die ersten Werte des Kontos. Die #[derive(Accounts)]-Struktur verwendet Anchor account constraints, um anzugeben, welche Konten erforderlich sind und wie das neue Zähler-Konto erstellt wird.

programs/my-program/src/instructions/initialize.rs
use anchor_lang::prelude::*;
use crate::{constants::*, state::Counter};
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Account Context

Die Initialize-Struktur definiert die Konten, die angegeben werden müssen, wenn ein Benutzer die initialize- Anweisung aufruft. Anchor prüft diese Konten, bevor der Handler ausgeführt wird.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Payer Account

Das payer-Konto bezahlt die Erstellung des Zähler-Kontos. Der Typ Signer<'info> bedeutet, dass der Zahler die Transaktion unterzeichnen muss, und #[account(mut)] bedeutet, dass das Zahler-Konto geändert werden kann, da lamports abgezogen werden.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Counter Account

Das counter-Konto speichert die Counter-Daten aus state.rs. init weist Anchor an, dieses Konto vor der Ausführung des Handlers zu erstellen, und payer = payer teilt Anchor mit, welches Konto die Erstellung bezahlt.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Konten-Größe

Der space Constraint teilt Anchor mit, wie viel Kontenspeicher allokiert werden soll. Anchor speichert zuerst einen 8-Byte-Diskriminator, dann die für die Counter Felder benötigten Bytes. Der Diskriminator ermöglicht es Anchor, dieses Konto als Counter Konten zu erkennen, bevor die Kontendaten deserialisiert werden.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Zähler-Adresse

Die Constraints seeds und bump definieren die erwartete PDA-Adresse für das Zähler-Konten. Anchor überprüft, ob das angegebene counter Konten mit dieser Adresse übereinstimmt. Das Template verwendet eine PDA, damit Nutzer die Zähleradresse aus der Programm-ID und dem seed ableiten können, was die Zähleradresse deterministisch macht.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

System Program

Das system_program Konten wird benötigt, da das Erstellen eines neuen Kontos das System Program verwendet.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

Handler-Funktion

Die Funktion handle_initialize wird ausgeführt, nachdem Anchor die Konten in Initialize validiert hat. Der Wert ctx gibt dem Handler Zugriff auf diese geprüften Konten.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Anfangsdaten

Der Handler schreibt die ersten Werte in das neue Zähler-Konten. Der Zähler startet bei 0, und der Zahler wird zur Autorität, die berechtigt ist, den Zähler später zu erhöhen.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Übertragungs-Konten

Dieser Transfer-CPI ist nur enthalten, um zu demonstrieren, wie ein CPI Konten an ein anderes Programm weitergibt. Das Transfer Struct listet die vom System Program Transfer verwendeten Konten auf.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

CPI-Kontext

CpiContext::new kombiniert das aufgerufene Programm mit den Konten, die an dieses Programm übergeben werden. Dies ist die grundlegende Struktur eines CPI: das aufzurufende Programm auswählen, die von diesem Programm erwarteten Konten sammeln und dann beides in den Aufruf übergeben. Hier ist das aufgerufene Programm das System Program.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Transfer aufrufen

anchor_lang::system_program::transfer ruft die Transfer- Anweisungen des System Program auf. In dieser Vorlage ist der Transfer ein einfaches Beispiel für den Aufruf eines anderen Programms aus Ihrem Programm heraus. Wenn der Transfer-CPI fehlschlägt, schlägt auch die initialize- Anweisungen fehl.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Nachricht protokollieren

msg! schreibt eine Nachricht in die Programmprotokolle. Ok(()) gibt an, dass die Anweisungen erfolgreich zurückgegeben wurde.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
pub fn handle_initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.payer.key();
let cpi_accounts = anchor_lang::system_program::Transfer {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.counter.to_account_info(),
};
let cpi_ctx = CpiContext::new(anchor_lang::system_program::ID, cpi_accounts);
anchor_lang::system_program::transfer(cpi_ctx, HELLO_WORLD_LAMPORTS)?;
msg!("Hello, world! Counter initialized");
Ok(())
}

Account Context

Die Initialize-Struktur definiert die Konten, die angegeben werden müssen, wenn ein Benutzer die initialize- Anweisung aufruft. Anchor prüft diese Konten, bevor der Handler ausgeführt wird.

Payer Account

Das payer-Konto bezahlt die Erstellung des Zähler-Kontos. Der Typ Signer<'info> bedeutet, dass der Zahler die Transaktion unterzeichnen muss, und #[account(mut)] bedeutet, dass das Zahler-Konto geändert werden kann, da lamports abgezogen werden.

Counter Account

Das counter-Konto speichert die Counter-Daten aus state.rs. init weist Anchor an, dieses Konto vor der Ausführung des Handlers zu erstellen, und payer = payer teilt Anchor mit, welches Konto die Erstellung bezahlt.

Konten-Größe

Der space Constraint teilt Anchor mit, wie viel Kontenspeicher allokiert werden soll. Anchor speichert zuerst einen 8-Byte-Diskriminator, dann die für die Counter Felder benötigten Bytes. Der Diskriminator ermöglicht es Anchor, dieses Konto als Counter Konten zu erkennen, bevor die Kontendaten deserialisiert werden.

Zähler-Adresse

Die Constraints seeds und bump definieren die erwartete PDA-Adresse für das Zähler-Konten. Anchor überprüft, ob das angegebene counter Konten mit dieser Adresse übereinstimmt. Das Template verwendet eine PDA, damit Nutzer die Zähleradresse aus der Programm-ID und dem seed ableiten können, was die Zähleradresse deterministisch macht.

System Program

Das system_program Konten wird benötigt, da das Erstellen eines neuen Kontos das System Program verwendet.

Handler-Funktion

Die Funktion handle_initialize wird ausgeführt, nachdem Anchor die Konten in Initialize validiert hat. Der Wert ctx gibt dem Handler Zugriff auf diese geprüften Konten.

Anfangsdaten

Der Handler schreibt die ersten Werte in das neue Zähler-Konten. Der Zähler startet bei 0, und der Zahler wird zur Autorität, die berechtigt ist, den Zähler später zu erhöhen.

Übertragungs-Konten

Dieser Transfer-CPI ist nur enthalten, um zu demonstrieren, wie ein CPI Konten an ein anderes Programm weitergibt. Das Transfer Struct listet die vom System Program Transfer verwendeten Konten auf.

CPI-Kontext

CpiContext::new kombiniert das aufgerufene Programm mit den Konten, die an dieses Programm übergeben werden. Dies ist die grundlegende Struktur eines CPI: das aufzurufende Programm auswählen, die von diesem Programm erwarteten Konten sammeln und dann beides in den Aufruf übergeben. Hier ist das aufgerufene Programm das System Program.

Transfer aufrufen

anchor_lang::system_program::transfer ruft die Transfer- Anweisungen des System Program auf. In dieser Vorlage ist der Transfer ein einfaches Beispiel für den Aufruf eines anderen Programms aus Ihrem Programm heraus. Wenn der Transfer-CPI fehlschlägt, schlägt auch die initialize- Anweisungen fehl.

Nachricht protokollieren

msg! schreibt eine Nachricht in die Programmprotokolle. Ok(()) gibt an, dass die Anweisungen erfolgreich zurückgegeben wurde.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}

increment.rs

increment.rs definiert die Konten, die zum Aktualisieren eines bestehenden Zähler-Kontos erforderlich sind. Der Handler prüft, ob der Signer die gespeicherte Autorität ist, ob der Zählerstand das angegebene MAX_COUNT noch nicht erreicht hat, und erhöht dann den Zählerstand.

programs/my-program/src/instructions/increment.rs
use anchor_lang::prelude::*;
use crate::{constants::*, error::ErrorCode, state::Counter};
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Konto-Kontext

Das Increment-Struct definiert die Konten, die angegeben werden müssen, wenn ein Benutzer die increment Anweisungen aufruft. Anchor prüft diese Konten, bevor der Handler ausgeführt wird.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Zähler-Konto

Das counter-Konto speichert die Counter-Daten. Die mut-Einschränkung erlaubt dem Handler, den gespeicherten Zählerstand zu aktualisieren, und die Einschränkungen seeds und bump überprüfen die PDA-Adresse des Zählers.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Autorität-Signer

Das authority-Konto muss die Transaktion signieren. Der Handler prüft anschließend, ob dieser Signer mit der im Zähler-Konto gespeicherten Autorität übereinstimmt.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Handler-Funktion

Die Funktion handle_increment wird ausgeführt, nachdem Anchor die Konten in Increment validiert hat. Der Wert ctx gibt dem Handler Zugriff auf diese geprüften Konten.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Autorisierungsprüfung

Die erste Prüfung stellt sicher, dass der Signer berechtigt ist, diesen Zähler zu aktualisieren. Wenn die Adresse des Signers nicht mit counter.authority übereinstimmt, wird die Anweisung mit ErrorCode::Unauthorized abgebrochen. Dies demonstriert die Autorisierung auf Anwendungsebene: Das Programm besitzt die Zählerdaten, implementiert aber eine Regel, welcher Signer diese Daten ändern darf.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Prüfung des Maximalwerts

Die zweite Prüfung verhindert, dass der Zähler MAX_COUNT überschreitet. Wenn der Zähler bereits den Grenzwert erreicht hat, wird die Anweisung mit ErrorCode::CounterOverflow abgebrochen. Dieser Grenzwert ist eine künstliche Regel in der Vorlage, damit Sie sehen können, wie benutzerdefinierte Fehler eine Anweisung stoppen, bevor Kontodaten geändert werden.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Zähler aktualisieren

Erst nachdem beide Prüfungen bestanden wurden, aktualisiert der Handler die Kontodaten. Diese Zeile addiert eins zum gespeicherten Zählerwert.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Log-Nachricht

msg! schreibt den aktualisierten Zählerstand in die Programm-Logs. Ok(()) zeigt an, dass die Anweisung erfolgreich zurückgegeben wurde.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
pub fn handle_increment(ctx: Context<Increment>) -> Result<()> {
require_keys_eq!(
ctx.accounts.counter.authority,
ctx.accounts.authority.key(),
ErrorCode::Unauthorized,
);
require!(
ctx.accounts.counter.count < MAX_COUNT,
ErrorCode::CounterOverflow,
);
ctx.accounts.counter.count += 1;
msg!("Hello, world! Counter is now {}", ctx.accounts.counter.count);
Ok(())
}

Konto-Kontext

Das Increment-Struct definiert die Konten, die angegeben werden müssen, wenn ein Benutzer die increment Anweisungen aufruft. Anchor prüft diese Konten, bevor der Handler ausgeführt wird.

Zähler-Konto

Das counter-Konto speichert die Counter-Daten. Die mut-Einschränkung erlaubt dem Handler, den gespeicherten Zählerstand zu aktualisieren, und die Einschränkungen seeds und bump überprüfen die PDA-Adresse des Zählers.

Autorität-Signer

Das authority-Konto muss die Transaktion signieren. Der Handler prüft anschließend, ob dieser Signer mit der im Zähler-Konto gespeicherten Autorität übereinstimmt.

Handler-Funktion

Die Funktion handle_increment wird ausgeführt, nachdem Anchor die Konten in Increment validiert hat. Der Wert ctx gibt dem Handler Zugriff auf diese geprüften Konten.

Autorisierungsprüfung

Die erste Prüfung stellt sicher, dass der Signer berechtigt ist, diesen Zähler zu aktualisieren. Wenn die Adresse des Signers nicht mit counter.authority übereinstimmt, wird die Anweisung mit ErrorCode::Unauthorized abgebrochen. Dies demonstriert die Autorisierung auf Anwendungsebene: Das Programm besitzt die Zählerdaten, implementiert aber eine Regel, welcher Signer diese Daten ändern darf.

Prüfung des Maximalwerts

Die zweite Prüfung verhindert, dass der Zähler MAX_COUNT überschreitet. Wenn der Zähler bereits den Grenzwert erreicht hat, wird die Anweisung mit ErrorCode::CounterOverflow abgebrochen. Dieser Grenzwert ist eine künstliche Regel in der Vorlage, damit Sie sehen können, wie benutzerdefinierte Fehler eine Anweisung stoppen, bevor Kontodaten geändert werden.

Zähler aktualisieren

Erst nachdem beide Prüfungen bestanden wurden, aktualisiert der Handler die Kontodaten. Diese Zeile addiert eins zum gespeicherten Zählerwert.

Log-Nachricht

msg! schreibt den aktualisierten Zählerstand in die Programm-Logs. Ok(()) zeigt an, dass die Anweisung erfolgreich zurückgegeben wurde.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}

Testdatei

programs/my-program/tests/test_initialize.rs ist ein Rust-Integrationstest. Es wird kein lokaler validator gestartet. Stattdessen wird die kompilierte .so-Datei in LiteSVM geladen, Transaktionen erstellt, die das Programm aufrufen, und das Zählerkonto nach jeder Transaktion ausgelesen. Der Test erstellt Anweisungen für eine Solana-Transaktion, indem die Programm-ID des aufzurufenden Programms angegeben, instruction data bereitgestellt und die erforderlichen Konten übergeben werden.

initialize.rs
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + Counter::INIT_SPACE,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
test_initialize.rs
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Initialize {}.data(),
my_program::accounts::Initialize {
payer: payer.pubkey(),
counter,
system_program: system_program::ID,
}
.to_account_metas(None),
);

Der Initialize Kontokontex definiert die Konten, die von der initialize Anweisung benötigt werden. Der Test übergibt dieselben Konten an den generierten my_program::accounts::Initialize Helfer:

  • payer wird als payer: payer.pubkey() übergeben.
  • counter wird als counter übergeben.
  • system_program wird als system_program::ID übergeben.

my_program::instruction::Initialize {}.data() erstellt die instruction data. Hier würden Anweisungsargumente kodiert werden, aber diese initialize Anweisung erfordert keine Argumente.

increment.rs
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(
mut,
seeds = [COUNTER_SEED],
bump
)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
test_initialize.rs
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Increment {}.data(),
my_program::accounts::Increment {
counter,
authority: payer.pubkey(),
}
.to_account_metas(None),
);

Der Increment Kontokontex definiert die Konten, die von der increment Anweisung benötigt werden. Der Test übergibt dieselben Konten an den generierten my_program::accounts::Increment Helfer:

  • counter wird als counter übergeben.
  • authority wird als authority: payer.pubkey() übergeben.

my_program::instruction::Increment {}.data() erstellt die instruction data. Hier würden Anweisungsargumente kodiert werden, aber diese increment Anweisung erfordert keine Argumente.

programs/my-program/tests/test_initialize.rs
use {
anchor_lang::{
prelude::Pubkey,
solana_program::{instruction::Instruction, system_program},
AccountDeserialize, InstructionData, ToAccountMetas,
},
litesvm::LiteSVM,
solana_keypair::Keypair,
solana_message::{Message, VersionedMessage},
solana_signer::Signer,
solana_transaction::versioned::VersionedTransaction,
};
#[test]
fn test_initialize() {
let program_id = my_program::id();
let payer = Keypair::new();
let counter = Pubkey::find_program_address(
&[my_program::constants::COUNTER_SEED],
&program_id,
)
.0;
let mut svm = LiteSVM::new();
let bytes = include_bytes!(concat!(
env!("CARGO_TARGET_TMPDIR"),
"/../deploy/my_program.so"
));
svm.add_program(program_id, bytes).unwrap();
svm.airdrop(&payer.pubkey(), 1_000_000_000).unwrap();
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Initialize {}.data(),
my_program::accounts::Initialize {
payer: payer.pubkey(),
counter,
system_program: system_program::ID,
}
.to_account_metas(None),
);
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
let res = svm.send_transaction(tx);
assert!(res.is_ok());
let counter_account = svm.get_account(&counter).unwrap();
let mut data: &[u8] = &counter_account.data;
let counter_state = my_program::state::Counter::try_deserialize(&mut data).unwrap();
assert_eq!(counter_state.count, 0);
assert_eq!(counter_state.authority, payer.pubkey());
let instruction = Instruction::new_with_bytes(
program_id,
&my_program::instruction::Increment {}.data(),
my_program::accounts::Increment {
counter,
authority: payer.pubkey(),
}
.to_account_metas(None),
);
let blockhash = svm.latest_blockhash();
let msg = Message::new_with_blockhash(&[instruction], Some(&payer.pubkey()), &blockhash);
let tx = VersionedTransaction::try_new(VersionedMessage::Legacy(msg), &[&payer]).unwrap();
let res = svm.send_transaction(tx);
assert!(res.is_ok());
let counter_account = svm.get_account(&counter).unwrap();
let mut data: &[u8] = &counter_account.data;
let counter_state = my_program::state::Counter::try_deserialize(&mut data).unwrap();
assert_eq!(counter_state.count, 1);
assert_eq!(counter_state.authority, payer.pubkey());
}

Projektkonfiguration

Die Stammprojektdateien teilen Anchor und Cargo mit, wie das Programm gebaut, getestet und deployed werden soll. Eine vollständige Referenz findest du in der Anchor-Dokumentation für die Anchor.toml-Konfiguration und die Anchor CLI.

Anchor.toml
skip_local_validator = true
[toolchain]
[features]
resolution = true
skip-lint = false
[programs.localnet]
my_program = "82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd"
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
[scripts]
test = "cargo test"
[hooks]

Is this page helpful?

Inhaltsverzeichnis

Seite bearbeiten
© 2026 Solana Foundation. Alle Rechte vorbehalten.