Rakenna ensimmäinen Solana-ohjelmasi

Tämä pikakäynnistysopas käyttää Anchor-sovelluskehyksen aloitusprojektia, jonka anchor init luo. Luot projektin paikallisesti, suoritat sen testit, rakennat ohjelman ja käyt läpi ohjelmakoodin, jonka Anchor generoi.

Edellytykset

Ennen kuin aloitat, asenna Solana-kehitystyökalut. Asennus sisältää Rustin, Solana CLI:n ja Anchor CLI:n.

Käytä tähän malliin Anchor CLI -versiota 1.1.2 tai uudempaa. Tarkista asennettu versio:

Terminal
$
anchor --version

Luo projekti

Suorita seuraavat komennot terminaalissasi:

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

Aloitusprojekti sisältää yhden Solana-ohjelman hakemistossa programs/my-program. Ohjelma sisältää kaksi käskyä: yhden laskuritilin alustamiseen ja yhden laskurin kasvattamiseen.

Osa mallin osioista havainnollistaa yleisiä Solana-ohjelmointimalleja: PDA-tilin osoitteiden johtaminen, Cross Program Invocation (CPI) SOL:n siirtämiseksi sekä mukautettujen virheentarkistusten käyttö käskyn pysäyttämiseksi, kun ehto ei täyty.

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

Rakenna ohjelma

Suorita anchor build kääntääksesi aloitusohjelman:

Terminal
$
anchor build

Käännetty ohjelma kirjoitetaan kohteeseen target/deploy/my_program.so. Kun ohjelma otetaan käyttöön, tämän .so-tiedoston sisältö tallennetaan ketjussa olevalle tilille.

Suorita testi

Suorita oletustesti:

Terminal
$
anchor test

Tämän mallin Anchor.toml käyttää Rust-testikomentoa:

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

Testi lataa käännetyn ohjelman LiteSVM-ympäristöön, luo maksajan, lähettää initialize- ja increment-käskyt, jonka jälkeen tarkistaa laskuritilin tilan.

anchor test-komennon suorittaminen kääntää myös ohjelman, joten sinun ei tarvitse ajaa anchor build-komentoa ensin paikallista testausta varten.

Ota ohjelma käyttöön

Paikalliset testit tarjoavat nopeimman palautesilmukan. Kun olet valmis ottamaan käyttöön verkossa, esimerkiksi devnetissä, käännä ensin ja ota sitten käyttöön klusterissa.

Solana-ohjelman käyttöönottovaatimuksena on SOL, koska ohjelma tallennetaan program account -tilille, jonka täytyy maksaa käyttämästään tilasta. Devnetissä voit pyytää ilmaista devnet-SOL:ia Solana Faucetista tai Solana CLI:llä:

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

Lähdetiedostot

src-hakemisto sisältää Solana-ohjelman. Anchor'in Ohjelmarakenne -dokumentaatio selittää tässä käytetyt keskeiset makrot, mukaan lukien declare_id!, #[program], #[derive(Accounts)] ja #[account]. Tässä osiossa käydään läpi mallin tiedostot.

lib.rs

lib.rs on ohjelman sisääntulopiste. Se yhdistää lähdetiedostot, määrittelee ohjelman osoitteen ja määrittelee ohjelmakomennot, joita käyttäjät voivat kutsua.

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

Sama ohjelma-osoite esiintyy sekä konfiguraatiossa että koodissa. Anchor.toml kertoo Anchorille, mihin osoitteeseen ohjelma otetaan käyttöön tai kutsutaan klusterissa. declare_id! määrittää ohjelma-osoitteen ohjelmassa turvatarkistuksia varten.

constants.rs

constants.rs pitää jaetut arvot yhdessä paikassa. Tässä mallissa COUNTER_SEED johtaa laskurin PDA:n, HELLO_WORLD_LAMPORTS siirretään alustuksen aikana ja MAX_COUNT tarkistetaan ennen kasvattamista.

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 käyttää kahta vakiota:

  • COUNTER_SEED johtaa laskurin PDA-osoitteen.
  • HELLO_WORLD_LAMPORTS määrittää summan, joka siirretään maksajalta laskuritilille.
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 käyttää MAX_COUNT laskurin ylänä rajana. Jos nykyinen arvo on jo maksimissa, require! palauttaa CounterOverflow eikä tilin tietoja muuteta.

state.rs

state.rs määrittää mukautetut tietotyypit tileille, jotka ohjelma luo ja omistaa. Ohjelma määrittää ohjeet tietojen luomiseen, alustamiseen ja päivittämiseen, mutta laskuridata ei ole tallennettu itse ohjelmaan. Se on tallennettu erilliselle tilille, jolla on oma osoitteensa.

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 määrittelee Counter-tilin tiedot. Ohjetiedostot käyttävät tätä tyyppiä luodessaan ja päivittäessään tilin:

  • Counter::INIT_SPACE määrittää tilin koon state.rs:ssä määriteltyjen kenttien mukaan.
  • count ja authority ovat kenttien arvoja, jotka kirjoitetaan tilin alustuksen yhteydessä.
  • count += 1 päivittää tallennetun laskuriarvon validoinnin läpäisemisen jälkeen.

error.rs

error.rs määrittelee ohjelman mukautetut virheet. Tässä mallissa virheet havainnollistavat, kuinka ohjetoiminnot pysähtyvät, kun kutsujalla ei ole oikeutta päivittää laskuria tai laskuri on jo saavuttanut MAX_COUNT-arvon.

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 nimeää virheet, joita ohje voi palauttaa:

  • ErrorCode::Unauthorized palautetaan, kun allekirjoittaja ei ole laskuritiliin tallennettu auktoriteetti.
  • ErrorCode::CounterOverflow palautetaan, kun laskuri on jo saavuttanut MAX_COUNT-arvon.

instructions.rs

instructions.rs yhdistää ohjetiedostot ohjelmapaketille, jotta lib.rs voi käyttää initialize- ja increment-ohjeistuksen koodia. Jokainen ohjetiedosto määrittelee kyseisen ohjeen vaatimat tilit sekä käsittelijälogiikan, joka suoritetaan sen jälkeen, kun Anchor on vahvistanut nämä tilit.

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

initialize.rs

initialize.rs määrittelee laskuritilin luomiseen tarvittavat tilit ja kirjoittaa sitten tilin ensimmäiset arvot. #[derive(Accounts)]-rakenne käyttää Anchorin tiliehtojen määrityksiä ilmaisemaan, mitkä tilit vaaditaan ja miten uusi laskuritili luodaan.

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

Initialize-rakenne määrittelee tilit, jotka on sisällytettävä, kun käyttäjä kutsuu initialize-ohjetta. Anchor tarkistaa nämä tilit ennen käsittelijän suorittamista.

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

payer-tili maksaa laskuritilin luomisesta. Signer<'info>-tyyppi tarkoittaa, että maksajan on allekirjoitettava transaktio, ja #[account(mut)] tarkoittaa, että maksajatiliä voidaan muuttaa, koska lamport-yksiköitä vähennetään siitä.

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

counter-tili tallentaa state.rs:n Counter-datan. init käskee Anchoria luomaan tämän tilin ennen käsittelijän suorittamista, ja payer = payer kertoo Anchorille, mikä tili maksaa luomisesta.

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

Tilin koko

space -rajoite kertoo Anchorille, kuinka paljon tilidataa varataan. Anchor tallentaa ensin 8-tavuisen erottelijan, sitten Counter -kenttien tarvitsemat tavut. Erottelija antaa Anchorille mahdollisuuden tunnistaa tämä tili Counter -tiliksi ennen kuin tilin data deserialisoidaan.

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

Laskurin osoite

seeds ja bump -rajoitteet määrittävät odotetun PDA-osoitteen laskuritilille. Anchor varmistaa, että annettu counter -tili vastaa kyseistä osoitetta. Malli käyttää PDA:ta, jotta käyttäjät voivat johtaa laskurin osoitteen ohjelmatunnuksesta ja seed-arvosta, mikä tekee laskurin osoitteesta deterministisen.

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

system_program -tili vaaditaan, koska uuden tilin luominen käyttää System Program -ohjelmaa.

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

Käsittelijäfunktio

handle_initialize -funktio suoritetaan sen jälkeen, kun Anchor on validoinut tilit Initialize:ssa. ctx -arvo antaa käsittelijälle pääsyn näihin tarkistettuihin tileihin.

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

Alkudata

Käsittelijä kirjoittaa ensimmäiset arvot uudelle laskuritilille. Laskuri alkaa arvosta 0, ja maksajasta tulee valtuutettu, jolla on oikeus kasvattaa laskuria myöhemmin.

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

Siirtotilit

Tämä siirto-CPI on sisällytetty vain havainnollistamaan, kuinka CPI välittää tilejä toiselle ohjelmalle. Transfer -rakenne luettelee System Program -siirron käyttämät tilit.

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

CpiContext::new yhdistää kutsuttavan ohjelman ja sille välitetyt tilit. Tämä on CPI:n perusrakenne: valitaan kutsuttava ohjelma, kerätään kyseisen ohjelman odottamat tilit ja välitetään molemmat kutsulle. Tässä kutsuttava ohjelma on 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(())
}

Invoke Transfer

anchor_lang::system_program::transfer kutsuu System Program -siirto-ohjetta. Tässä mallissa siirto on pieni esimerkki toisen ohjelman kutsumisesta omasta ohjelmastasi. Jos siirto-CPI epäonnistuu, myös initialize-ohje epäonnistuu.

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

Log Message

msg! kirjoittaa viestin ohjelman lokeihin. Ok(()) ilmaisee, että ohje palautui onnistuneesti.

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

Initialize-rakenne määrittelee tilit, jotka on sisällytettävä, kun käyttäjä kutsuu initialize-ohjetta. Anchor tarkistaa nämä tilit ennen käsittelijän suorittamista.

Payer Account

payer-tili maksaa laskuritilin luomisesta. Signer<'info>-tyyppi tarkoittaa, että maksajan on allekirjoitettava transaktio, ja #[account(mut)] tarkoittaa, että maksajatiliä voidaan muuttaa, koska lamport-yksiköitä vähennetään siitä.

Counter Account

counter-tili tallentaa state.rs:n Counter-datan. init käskee Anchoria luomaan tämän tilin ennen käsittelijän suorittamista, ja payer = payer kertoo Anchorille, mikä tili maksaa luomisesta.

Tilin koko

space -rajoite kertoo Anchorille, kuinka paljon tilidataa varataan. Anchor tallentaa ensin 8-tavuisen erottelijan, sitten Counter -kenttien tarvitsemat tavut. Erottelija antaa Anchorille mahdollisuuden tunnistaa tämä tili Counter -tiliksi ennen kuin tilin data deserialisoidaan.

Laskurin osoite

seeds ja bump -rajoitteet määrittävät odotetun PDA-osoitteen laskuritilille. Anchor varmistaa, että annettu counter -tili vastaa kyseistä osoitetta. Malli käyttää PDA:ta, jotta käyttäjät voivat johtaa laskurin osoitteen ohjelmatunnuksesta ja seed-arvosta, mikä tekee laskurin osoitteesta deterministisen.

System Program

system_program -tili vaaditaan, koska uuden tilin luominen käyttää System Program -ohjelmaa.

Käsittelijäfunktio

handle_initialize -funktio suoritetaan sen jälkeen, kun Anchor on validoinut tilit Initialize:ssa. ctx -arvo antaa käsittelijälle pääsyn näihin tarkistettuihin tileihin.

Alkudata

Käsittelijä kirjoittaa ensimmäiset arvot uudelle laskuritilille. Laskuri alkaa arvosta 0, ja maksajasta tulee valtuutettu, jolla on oikeus kasvattaa laskuria myöhemmin.

Siirtotilit

Tämä siirto-CPI on sisällytetty vain havainnollistamaan, kuinka CPI välittää tilejä toiselle ohjelmalle. Transfer -rakenne luettelee System Program -siirron käyttämät tilit.

CPI-konteksti

CpiContext::new yhdistää kutsuttavan ohjelman ja sille välitetyt tilit. Tämä on CPI:n perusrakenne: valitaan kutsuttava ohjelma, kerätään kyseisen ohjelman odottamat tilit ja välitetään molemmat kutsulle. Tässä kutsuttava ohjelma on System Program.

Invoke Transfer

anchor_lang::system_program::transfer kutsuu System Program -siirto-ohjetta. Tässä mallissa siirto on pieni esimerkki toisen ohjelman kutsumisesta omasta ohjelmastasi. Jos siirto-CPI epäonnistuu, myös initialize-ohje epäonnistuu.

Log Message

msg! kirjoittaa viestin ohjelman lokeihin. Ok(()) ilmaisee, että ohje palautui onnistuneesti.

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 määrittelee tilit, joita tarvitaan olemassa olevan laskuritilin päivittämiseen. Käsittelijä tarkistaa, että allekirjoittaja on tallennettu auktoriteetti, tarkistaa, että laskuri ei ole saavuttanut määritettyä MAX_COUNT-arvoa, ja kasvattaa sitten laskuria.

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

Tilikonteksti

Increment -rakenne määrittelee tilit, jotka on sisällytettävä, kun käyttäjä kutsuu increment -instruktiota. Anchor tarkistaa nämä tilit ennen kuin käsittelijä suoritetaan.

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

Laskuritili

counter -tili tallentaa Counter -datan. mut -rajoite sallii käsittelijän päivittää tallennetun laskurin, ja seeds sekä bump -rajoitteet varmistavat laskurin PDA-osoitteen.

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

Auktoriteettiallekirjoittaja

authority -tilin on allekirjoitettava transaktio. Käsittelijä tarkistaa myöhemmin, että tämä allekirjoittaja vastaa laskuritilille tallennettua auktoriteettia.

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

Käsittelijäfunktio

Funktio handle_increment suoritetaan sen jälkeen, kun Anchor on validoinut tilit Increment-rakenteessa. Arvo ctx antaa käsittelijälle pääsyn näihin tarkistettuihin tileihin.

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

Valtuutustarkistus

Ensimmäinen tarkistus varmistaa, että allekirjoittajalla on oikeus päivittää tätä laskuria. Jos allekirjoittajan osoite ei vastaa arvoa counter.authority, ohje pysähtyy virheeseen ErrorCode::Unauthorized. Tämä havainnollistaa sovellustason valtuutusta: ohjelma omistaa laskurin tiedot, mutta se toteuttaa säännön siitä, kuka allekirjoittaja saa muuttaa kyseisiä tietoja.

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

Maksimilukeman tarkistus

Toinen tarkistus estää laskuria ylittämästä arvoa MAX_COUNT. Jos laskuri on jo saavuttanut rajan, ohje pysähtyy virheeseen ErrorCode::CounterOverflow. Tämä raja on mallipohjaan lisätty keinotekoinen sääntö, jotta näet, miten mukautetut virheet pysäyttävät ohjeen ennen kuin tilitiedot muuttuvat.

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

Lukeman päivitys

Vasta kun molemmat tarkistukset läpäistään, käsittelijä päivittää tilitiedot. Tämä rivi lisää yhden tallennettuun laskurin arvoon.

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

Lokiviesti

msg! kirjoittaa päivitetyn lukeman ohjelmalokiin. Ok(()) ilmaisee, että ohje palautui onnistuneesti.

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

Tilikonteksti

Increment -rakenne määrittelee tilit, jotka on sisällytettävä, kun käyttäjä kutsuu increment -instruktiota. Anchor tarkistaa nämä tilit ennen kuin käsittelijä suoritetaan.

Laskuritili

counter -tili tallentaa Counter -datan. mut -rajoite sallii käsittelijän päivittää tallennetun laskurin, ja seeds sekä bump -rajoitteet varmistavat laskurin PDA-osoitteen.

Auktoriteettiallekirjoittaja

authority -tilin on allekirjoitettava transaktio. Käsittelijä tarkistaa myöhemmin, että tämä allekirjoittaja vastaa laskuritilille tallennettua auktoriteettia.

Käsittelijäfunktio

Funktio handle_increment suoritetaan sen jälkeen, kun Anchor on validoinut tilit Increment-rakenteessa. Arvo ctx antaa käsittelijälle pääsyn näihin tarkistettuihin tileihin.

Valtuutustarkistus

Ensimmäinen tarkistus varmistaa, että allekirjoittajalla on oikeus päivittää tätä laskuria. Jos allekirjoittajan osoite ei vastaa arvoa counter.authority, ohje pysähtyy virheeseen ErrorCode::Unauthorized. Tämä havainnollistaa sovellustason valtuutusta: ohjelma omistaa laskurin tiedot, mutta se toteuttaa säännön siitä, kuka allekirjoittaja saa muuttaa kyseisiä tietoja.

Maksimilukeman tarkistus

Toinen tarkistus estää laskuria ylittämästä arvoa MAX_COUNT. Jos laskuri on jo saavuttanut rajan, ohje pysähtyy virheeseen ErrorCode::CounterOverflow. Tämä raja on mallipohjaan lisätty keinotekoinen sääntö, jotta näet, miten mukautetut virheet pysäyttävät ohjeen ennen kuin tilitiedot muuttuvat.

Lukeman päivitys

Vasta kun molemmat tarkistukset läpäistään, käsittelijä päivittää tilitiedot. Tämä rivi lisää yhden tallennettuun laskurin arvoon.

Lokiviesti

msg! kirjoittaa päivitetyn lukeman ohjelmalokiin. Ok(()) ilmaisee, että ohje palautui onnistuneesti.

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

Testitiedosto

programs/my-program/tests/test_initialize.rs on Rustin integraatiotesti. Se ei käynnistä paikallista validator:ia. Sen sijaan se lataa käännetyn .so-tiedoston LiteSVM:ään, rakentaa transaktioita, jotka kutsuvat ohjelmaa, ja lukee laskuritilin jokaisen transaktion jälkeen. Testi rakentaa ohjeet Solana-transaktiota varten määrittelemällä kutsuttavan ohjelman tunnuksen, antamalla instruction data ja välittämällä vaaditut tilit.

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

Initialize-tilirakenne määrittelee tilit, joita initialize-instruktio vaatii. Testi välittää samat tilit generoidulle my_program::accounts::Initialize-apufunktiolle:

  • payer välitetään payer: payer.pubkey()-parametrina.
  • counter välitetään counter-parametrina.
  • system_program välitetään system_program::ID-parametrina.

my_program::instruction::Initialize {}.data() luo instruction data. Tänne koodattaisiin instruktion argumentit, mutta tämä initialize-instruktio ei vaadi yhtään argumenttia.

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

Increment-tilirakenne määrittelee tilit, joita increment-instruktio vaatii. Testi välittää samat tilit generoidulle my_program::accounts::Increment-apufunktiolle:

  • counter välitetään counter-parametrina.
  • authority välitetään authority: payer.pubkey()-parametrina.

my_program::instruction::Increment {}.data() luo instruction data. Tänne koodattaisiin instruktion argumentit, mutta tämä increment-instruktio ei vaadi yhtään argumenttia.

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

Projektin konfigurointi

Projektin juuritiedostot kertovat Anchorille ja Cargolle, miten ohjelma rakennetaan, testataan ja otetaan käyttöön. Täydellinen viite löytyy Anchorin dokumentaatiosta: Anchor.toml-konfigurointi ja 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?

Sisällysluettelo

Muokkaa sivua
© 2026 Solana Foundation. Kaikki oikeudet pidätetään.