Zbuduj Swój Pierwszy Program Solana

Ten szybki start wykorzystuje projekt startowy frameworka Anchor wygenerowany przez anchor init. Utworzysz projekt lokalnie, uruchomisz jego testy, zbudujesz program i przejrzysz kod programu, który generuje Anchor.

Wymagania wstępne

Zanim zaczniesz, zainstaluj narzędzia deweloperskie Solana. Instalacja obejmuje Rust, Solana CLI oraz Anchor CLI.

Użyj Anchor CLI w wersji 1.1.2 lub wyższej dla tego szablonu. Sprawdź zainstalowaną wersję:

Terminal
$
anchor --version

Utwórz projekt

Uruchom następujące polecenia w swoim terminalu:

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

Projekt startowy zawiera jeden program Solana w katalogu programs/my-program. Program zawiera dwie instrukcje: jedną do inicjalizacji konta licznika i jedną do inkrementacji licznika.

Niektóre części szablonu demonstrują typowe wzorce programów Solana: wyprowadzanie adresów kont PDA, wykonywanie Cross Program Invocation (CPI) w celu transferu SOL oraz używanie niestandardowych sprawdzeń błędów do zatrzymania instrukcji, gdy warunek nie jest spełniony.

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

Zbuduj Program

Uruchom anchor build, aby skompilować program startowy:

Terminal
$
anchor build

Skompilowany program jest zapisywany do target/deploy/my_program.so. Gdy program zostanie wdrożony, zawartość tego pliku .so jest przechowywana na koncie w łańcuchu.

Uruchom test

Uruchom domyślny test:

Terminal
$
anchor test

Anchor.toml tego szablonu używa polecenia testowego Rust:

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

Test ładuje skompilowany program do LiteSVM, tworzy płatnika, wysyła instrukcje initialize i increment, a następnie sprawdza stan konta licznika.

Uruchomienie anchor test kompiluje również program, więc nie musisz uruchamiać anchor build przed testowaniem lokalnym.

Wdróż program

Testy lokalne zapewniają najszybszą pętlę informacji zwrotnej. Gdy będziesz gotowy do wdrożenia w sieci, na przykład devnet, najpierw zbuduj projekt, a następnie wdróż go do klastra.

Wdrożenie programu Solana wymaga SOL, ponieważ program jest przechowywany na koncie, a konto musi opłacić zajmowaną przestrzeń. W sieci devnet możesz bezpłatnie pozyskać devnet SOL z Solana Faucet lub za pomocą Solana CLI:

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

Pliki źródłowe

Katalog src zawiera program Solana. Dokumentacja Anchor dotycząca struktury programu omawiа podstawowe makra używane tutaj, w tym declare_id!, #[program], #[derive(Accounts)] i #[account]. Ta sekcja przechodzi przez pliki szablonu.

lib.rs

lib.rs jest punktem wejścia programu. Łączy pliki źródłowe, definiuje adres programu oraz instrukcje programu, które użytkownicy mogą wywoływać.

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

Ten sam adres programu pojawia się w konfiguracji i kodzie. Anchor.toml informuje Anchor, który adres wdrożyć lub wywołać dla klastra. declare_id! definiuje adres programu w programie na potrzeby kontroli bezpieczeństwa.

constants.rs

constants.rs przechowuje wspólne wartości w jednym miejscu. W tym szablonie COUNTER_SEED wyprowadza PDA licznika, HELLO_WORLD_LAMPORTS jest przekazywany podczas inicjalizacji, a MAX_COUNT jest sprawdzany przed inkrementacją.

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 używa dwóch stałych:

  • COUNTER_SEED wyznacza adres PDA licznika.
  • HELLO_WORLD_LAMPORTS określa kwotę przenoszoną od płatnika na konto licznika.
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 używa MAX_COUNT jako górnej granicznej wartości licznika. Jeśli bieżąca wartość osiągnęła już maksimum, require! zwraca CounterOverflow i dane konta nie są zmieniane.

state.rs

state.rs definiuje niestandardowe typy danych dla kont tworzonych i posiadanych przez program. Program definiuje instrukcje tworzenia, inicjowania i aktualizowania tych danych, jednak dane licznika nie są przechowywane w samym programie. Są przechowywane w oddzielnym koncie z własnym adresem.

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 definiuje dane konta Counter. Pliki instrukcji używają tego typu podczas tworzenia i aktualizowania konta:

  • Counter::INIT_SPACE określa rozmiar konta dla pól zdefiniowanych w state.rs.
  • count i authority to wartości pól zapisywane podczas inicjalizacji konta.
  • count += 1 aktualizuje przechowywaną wartość licznika po pomyślnej weryfikacji.

error.rs

error.rs definiuje niestandardowe błędy programu. W tym szablonie błędy przedstawiają, jak handlery instrukcji zatrzymują się, gdy wywołujący nie ma uprawnień do aktualizacji licznika lub licznik osiągnął już MAX_COUNT.

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 określa błędy, które instrukcja może zwrócić:

  • ErrorCode::Unauthorized jest zwracany, gdy podpisujący nie jest authority przechowywanym na koncie licznika.
  • ErrorCode::CounterOverflow jest zwracany, gdy licznik osiągnął już MAX_COUNT.

instructions.rs

instructions.rs łączy pliki instrukcji z kratem programu, dzięki czemu lib.rs może uzyskać dostęp do kodu instrukcji initialize i increment. Każdy plik instrukcji definiuje konta wymagane przez daną instrukcję oraz logikę obsługi wykonywaną po tym, jak Anchor zweryfikuje te konta.

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

initialize.rs

initialize.rs definiuje konta wymagane do utworzenia konta licznika, a następnie zapisuje jego pierwsze wartości. Struktura #[derive(Accounts)] korzysta z ograniczeń kont Anchor, aby określić, które konta są wymagane i w jaki sposób tworzone jest nowe konto licznika.

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

Kontekst konta

Struktura Initialize definiuje konta, które muszą być uwzględnione, gdy użytkownik wywołuje instrukcję initialize. Anchor weryfikuje te konta przed uruchomieniem obsługi.

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

Konto płatnika

Konto payer pokrywa koszty utworzenia konta licznika. Typ Signer<'info> oznacza, że płatnik musi podpisać transakcję, a #[account(mut)] oznacza, że konto płatnika może być modyfikowane, ponieważ lamport zostaną pobrane.

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

Konto licznika

Konto counter przechowuje dane Counter z state.rs. init informuje Anchor, aby utworzył to konto przed uruchomieniem obsługi, a payer = payer wskazuje Anchor, które konto pokrywa koszty utworzenia.

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

Rozmiar konta

Ograniczenie space informuje Anchor, ile miejsca na dane konta należy przydzielić. Anchor najpierw przechowuje 8-bajtowy dyskryminator, a następnie bajty potrzebne dla pól Counter. Dyskryminator pozwala Anchor rozpoznać to konto jako konto Counter przed deserializacją danych konta.

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

Adres licznika

Ograniczenia seeds i bump definiują oczekiwany adres PDA dla konta licznika. Anchor weryfikuje, czy podane konto counter pasuje do tego adresu. Szablon używa PDA, aby użytkownicy mogli wyprowadzić adres licznika z identyfikatora programu i seed, co sprawia, że adres licznika jest deterministyczny.

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

Konto system_program jest wymagane, ponieważ tworzenie nowego konta korzysta z 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>,
}

Funkcja obsługi

Funkcja handle_initialize jest uruchamiana po tym, jak Anchor zweryfikuje konta w Initialize. Wartość ctx zapewnia funkcji obsługi dostęp do tych zweryfikowanych kont.

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

Dane początkowe

Funkcja obsługi zapisuje pierwsze wartości do nowego konta licznika. Licznik zaczyna od 0, a płatnik staje się uprawnionym do późniejszego inkrementowania licznika.

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

Konta transferu

Ten transfer CPI jest dołączony wyłącznie w celu zademonstrowania, jak CPI przekazuje konta do innego programu. Struktura Transfer zawiera listę kont używanych przez transfer 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(())
}

Kontekst CPI

CpiContext::new łączy wywoływany program z kontami przekazywanymi do tego programu. To jest podstawowa struktura CPI: wybierz program do wywołania, zbierz konta, których ten program oczekuje, a następnie przekaż oba do wywołania. Tutaj wywoływanym programem jest 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(())
}

Wywołaj Transfer

anchor_lang::system_program::transfer wywołuje instrukcję transferu System Program. W tym szablonie transfer jest prostym przykładem wywoływania innego programu z twojego programu. Jeśli transfer CPI się nie powiedzie, instrukcja initialize również zakończy się niepowodzeniem.

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

Zapisz Wiadomość

msg! zapisuje wiadomość do logów programu. Ok(()) oznacza, że instrukcja zakończyła się pomyślnie.

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

Kontekst konta

Struktura Initialize definiuje konta, które muszą być uwzględnione, gdy użytkownik wywołuje instrukcję initialize. Anchor weryfikuje te konta przed uruchomieniem obsługi.

Konto płatnika

Konto payer pokrywa koszty utworzenia konta licznika. Typ Signer<'info> oznacza, że płatnik musi podpisać transakcję, a #[account(mut)] oznacza, że konto płatnika może być modyfikowane, ponieważ lamport zostaną pobrane.

Konto licznika

Konto counter przechowuje dane Counter z state.rs. init informuje Anchor, aby utworzył to konto przed uruchomieniem obsługi, a payer = payer wskazuje Anchor, które konto pokrywa koszty utworzenia.

Rozmiar konta

Ograniczenie space informuje Anchor, ile miejsca na dane konta należy przydzielić. Anchor najpierw przechowuje 8-bajtowy dyskryminator, a następnie bajty potrzebne dla pól Counter. Dyskryminator pozwala Anchor rozpoznać to konto jako konto Counter przed deserializacją danych konta.

Adres licznika

Ograniczenia seeds i bump definiują oczekiwany adres PDA dla konta licznika. Anchor weryfikuje, czy podane konto counter pasuje do tego adresu. Szablon używa PDA, aby użytkownicy mogli wyprowadzić adres licznika z identyfikatora programu i seed, co sprawia, że adres licznika jest deterministyczny.

System Program

Konto system_program jest wymagane, ponieważ tworzenie nowego konta korzysta z System Program.

Funkcja obsługi

Funkcja handle_initialize jest uruchamiana po tym, jak Anchor zweryfikuje konta w Initialize. Wartość ctx zapewnia funkcji obsługi dostęp do tych zweryfikowanych kont.

Dane początkowe

Funkcja obsługi zapisuje pierwsze wartości do nowego konta licznika. Licznik zaczyna od 0, a płatnik staje się uprawnionym do późniejszego inkrementowania licznika.

Konta transferu

Ten transfer CPI jest dołączony wyłącznie w celu zademonstrowania, jak CPI przekazuje konta do innego programu. Struktura Transfer zawiera listę kont używanych przez transfer System Program.

Kontekst CPI

CpiContext::new łączy wywoływany program z kontami przekazywanymi do tego programu. To jest podstawowa struktura CPI: wybierz program do wywołania, zbierz konta, których ten program oczekuje, a następnie przekaż oba do wywołania. Tutaj wywoływanym programem jest System Program.

Wywołaj Transfer

anchor_lang::system_program::transfer wywołuje instrukcję transferu System Program. W tym szablonie transfer jest prostym przykładem wywoływania innego programu z twojego programu. Jeśli transfer CPI się nie powiedzie, instrukcja initialize również zakończy się niepowodzeniem.

Zapisz Wiadomość

msg! zapisuje wiadomość do logów programu. Ok(()) oznacza, że instrukcja zakończyła się pomyślnie.

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 definiuje konta wymagane do aktualizacji istniejącego konta licznika. Handler sprawdza, czy podpisujący jest zapisanym administratorem, sprawdza, czy licznik nie osiągnął określonego MAX_COUNT, a następnie inkrementuje licznik.

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

Kontekst konta

Struktura Increment definiuje konta, które muszą być uwzględnione, gdy użytkownik wywołuje instrukcję increment. Anchor weryfikuje te konta przed uruchomieniem handlera.

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

Konto licznika

Konto counter przechowuje dane Counter. Ograniczenie mut pozwala handlerowi na aktualizację zapisanego licznika, a ograniczenia seeds i bump weryfikują adres PDA licznika.

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

Podpisujący z uprawnieniami

Konto authority musi podpisać transakcję. Handler następnie sprawdza, czy ten podpisujący zgadza się z administratorem zapisanym na koncie licznika.

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

Funkcja obsługi

Funkcja handle_increment uruchamia się po tym, jak Anchor zweryfikuje konta w Increment. Wartość ctx daje procedurze obsługi dostęp do tych sprawdzonych kont.

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

Weryfikacja uprawnień

Pierwsza weryfikacja sprawdza, czy sygnatariusz ma prawo zaktualizować ten licznik. Jeśli adres sygnatariusza nie zgadza się z counter.authority, instrukcja zatrzymuje się z błędem ErrorCode::Unauthorized. Pokazuje to autoryzację na poziomie aplikacji: program jest właścicielem danych licznika, ale implementuje regułę określającą, który sygnatariusz może te dane zmieniać.

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

Weryfikacja maksymalnej wartości licznika

Druga weryfikacja zapobiega przekroczeniu przez licznik wartości MAX_COUNT. Jeśli licznik osiągnął już limit, instrukcja zatrzymuje się z błędem ErrorCode::CounterOverflow. Ten limit jest sztuczną regułą w szablonie, dzięki której możesz zobaczyć, jak niestandardowe błędy zatrzymują instrukcję przed zmianą danych konta.

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

Aktualizacja licznika

Dopiero po przejściu obu weryfikacji procedura obsługi aktualizuje dane konta. Ta linia dodaje jeden do przechowywanej wartości licznika.

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

Komunikat dziennika

msg! zapisuje zaktualizowaną wartość licznika do dzienników programu. Ok(()) oznacza, że instrukcja zakończyła się pomyślnie.

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

Kontekst konta

Struktura Increment definiuje konta, które muszą być uwzględnione, gdy użytkownik wywołuje instrukcję increment. Anchor weryfikuje te konta przed uruchomieniem handlera.

Konto licznika

Konto counter przechowuje dane Counter. Ograniczenie mut pozwala handlerowi na aktualizację zapisanego licznika, a ograniczenia seeds i bump weryfikują adres PDA licznika.

Podpisujący z uprawnieniami

Konto authority musi podpisać transakcję. Handler następnie sprawdza, czy ten podpisujący zgadza się z administratorem zapisanym na koncie licznika.

Funkcja obsługi

Funkcja handle_increment uruchamia się po tym, jak Anchor zweryfikuje konta w Increment. Wartość ctx daje procedurze obsługi dostęp do tych sprawdzonych kont.

Weryfikacja uprawnień

Pierwsza weryfikacja sprawdza, czy sygnatariusz ma prawo zaktualizować ten licznik. Jeśli adres sygnatariusza nie zgadza się z counter.authority, instrukcja zatrzymuje się z błędem ErrorCode::Unauthorized. Pokazuje to autoryzację na poziomie aplikacji: program jest właścicielem danych licznika, ale implementuje regułę określającą, który sygnatariusz może te dane zmieniać.

Weryfikacja maksymalnej wartości licznika

Druga weryfikacja zapobiega przekroczeniu przez licznik wartości MAX_COUNT. Jeśli licznik osiągnął już limit, instrukcja zatrzymuje się z błędem ErrorCode::CounterOverflow. Ten limit jest sztuczną regułą w szablonie, dzięki której możesz zobaczyć, jak niestandardowe błędy zatrzymują instrukcję przed zmianą danych konta.

Aktualizacja licznika

Dopiero po przejściu obu weryfikacji procedura obsługi aktualizuje dane konta. Ta linia dodaje jeden do przechowywanej wartości licznika.

Komunikat dziennika

msg! zapisuje zaktualizowaną wartość licznika do dzienników programu. Ok(()) oznacza, że instrukcja zakończyła się pomyślnie.

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

Plik testowy

programs/my-program/tests/test_initialize.rs to test integracyjny w języku Rust. Nie uruchamia lokalnego validator. Zamiast tego ładuje skompilowany plik .so do LiteSVM, buduje transakcje wywołujące program i odczytuje konto licznika po każdej transakcji. Test buduje instrukcje dla transakcji Solana poprzez podanie identyfikatora programu do wywołania, dostarczenie instruction data oraz przekazanie wymaganych kont.

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

Kontekst konta Initialize definiuje konta wymagane przez instrukcję initialize. Test przekazuje te same konta do wygenerowanego pomocnika my_program::accounts::Initialize:

  • payer jest przekazywany jako payer: payer.pubkey().
  • counter jest przekazywany jako counter.
  • system_program jest przekazywany jako system_program::ID.

my_program::instruction::Initialize {}.data() tworzy instruction data. W tym miejscu kodowane byłyby argumenty instrukcji, jednak ta instrukcja initialize nie wymaga żadnych argumentów.

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

Kontekst konta Increment definiuje konta wymagane przez instrukcję increment. Test przekazuje te same konta do wygenerowanego pomocnika my_program::accounts::Increment:

  • counter jest przekazywany jako counter.
  • authority jest przekazywany jako authority: payer.pubkey().

my_program::instruction::Increment {}.data() tworzy instruction data. W tym miejscu kodowane byłyby argumenty instrukcji, jednak ta instrukcja increment nie wymaga żadnych argumentów.

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

Konfiguracja projektu

Pliki główne projektu informują Anchor i Cargo, jak budować, testować i wdrażać program. Pełną dokumentację znajdziesz w dokumentacji Anchor dla konfiguracji Anchor.toml oraz dla 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?

Spis treści

Edytuj stronę