Створіть свою першу програму Solana

Цей швидкий старт використовує стартовий проєкт фреймворку Anchor, згенерований командою anchor init. Ви створите проєкт локально, запустите його тести, зберете програму та ознайомитеся з кодом програми, який генерує Anchor.

Передумови

Перш ніж почати, встановіть інструменти розробки Solana. До встановлення входять Rust, Solana CLI та Anchor CLI.

Для цього шаблону використовуйте Anchor CLI версії 1.1.2 або вище. Перевірте встановлену версію:

Terminal
$
anchor --version

Створення проєкту

Виконайте наступні команди у вашому терміналі:

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

Стартовий проєкт містить одну програму Solana у programs/my-program. Програма включає дві інструкції: одну для ініціалізації облікового запису лічильника та одну для збільшення лічильника.

Деякі частини шаблону демонструють типові патерни програм Solana: виведення адрес облікових записів PDA, виконання Cross Program Invocation (CPI) для переказу SOL, а також використання власних перевірок помилок для зупинки інструкції у разі невиконання умови.

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

Збірка програми

Запустіть anchor build для компіляції стартової програми:

Terminal
$
anchor build

Скомпільовану програму записано до target/deploy/my_program.so. Під час розгортання програми вміст цього файлу .so зберігається в обліковому записі в мережі.

Запустити тест

Запустіть стандартний тест:

Terminal
$
anchor test

Anchor.toml цього шаблону використовує команду тестування Rust:

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

Тест завантажує скомпільовану програму в LiteSVM, створює платника, надсилає інструкції initialize та increment, а потім перевіряє стан рахунку лічильника.

Запуск anchor test також компілює програму, тому вам не потрібно запускати anchor build спочатку під час локального тестування.

Розгортання програми

Локальні тести — це найшвидший цикл зворотного зв'язку. Коли ви готові до розгортання в мережі, наприклад devnet, спочатку виконайте збірку, а потім розгорніть у кластері.

Розгортання програми Solana вимагає SOL, оскільки програма зберігається в акаунті, і акаунт повинен оплачувати використовуваний простір. У devnet запросіть безкоштовний devnet SOL із Solana Faucet або за допомогою Solana CLI:

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

Вихідні файли

Директорія src містить програму Solana. Документація Anchor щодо структури програми пояснює основні макроси, що використовуються тут, зокрема declare_id!, #[program], #[derive(Accounts)] та #[account]. У цьому розділі розглядаються файли шаблону.

lib.rs

lib.rs — це точка входу програми. Вона з'єднує вихідні файли, визначає адресу програми та визначає інструкції програми, які можуть викликати користувачі.

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

Та сама адреса програми з'являється в конфігурації та коді. Anchor.toml повідомляє Anchor, яку адресу використовувати для розгортання або виклику в кластері. declare_id! визначає адресу програми в самій програмі для перевірок безпеки.

constants.rs

constants.rs зберігає спільні значення в одному місці. У цьому шаблоні COUNTER_SEED використовується для виведення PDA лічильника, HELLO_WORLD_LAMPORTS передається під час ініціалізації, а MAX_COUNT перевіряється перед інкрементуванням.

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 використовує дві константи:

  • COUNTER_SEED отримує адресу PDA лічильника.
  • HELLO_WORLD_LAMPORTS встановлює суму, яка переводиться від платника на рахунок лічильника.
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 використовує MAX_COUNT як верхню межу лічильника. Якщо поточне значення вже досягло максимуму, require! повертає CounterOverflow і дані акаунту не змінюються.

state.rs

state.rs визначає власні типи даних для акаунтів, які програма створює та якими керує. Програма визначає інструкції для створення, ініціалізації та оновлення цих даних, але дані лічильника зберігаються не всередині самої програми. Вони зберігаються в окремому акаунті з власною адресою.

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 визначає дані акаунта Counter. Файли інструкцій використовують цей тип під час створення та оновлення акаунта:

  • Counter::INIT_SPACE визначає розмір акаунта для полів, описаних у state.rs.
  • count та authority — це значення полів, що записуються під час ініціалізації акаунта.
  • count += 1 оновлює збережене значення лічильника після успішної перевірки.

error.rs

error.rs визначає власні помилки програми. У цьому шаблоні помилки демонструють, як обробники інструкцій зупиняються, якщо виклик не має права оновлювати лічильник або лічильник вже досяг 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 називає помилки, які може повернути інструкція:

  • ErrorCode::Unauthorized повертається, коли підписант не є authority, збереженим в акаунті лічильника.
  • ErrorCode::CounterOverflow повертається, коли лічильник вже досяг MAX_COUNT.

instructions.rs

instructions.rs підключає файли інструкцій до крейту програми, щоб lib.rs міг отримати доступ до коду інструкцій initialize та increment. Кожен файл інструкції визначає облікові записи, необхідні для цієї інструкції, та логіку обробника, яка виконується після того, як Anchor перевірить ці облікові записи.

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

initialize.rs

initialize.rs визначає облікові записи, необхідні для створення облікового запису лічильника, а потім записує його початкові значення. Структура #[derive(Accounts)] використовує обмеження облікових записів Anchor, щоб вказати, які облікові записи є обов'язковими та як створюється новий обліковий запис лічильника.

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 визначає облікові записи, які мають бути включені, коли користувач викликає інструкцію initialize. Anchor перевіряє ці облікові записи до запуску обробника.

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 оплачує створення облікового запису лічильника. Тип Signer<'info> означає, що платник повинен підписати транзакцію, а #[account(mut)] означає, що обліковий запис платника може бути змінений, оскільки з нього будуть списані lamport.

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 зберігає дані Counter з state.rs. init вказує Anchor створити цей обліковий запис до запуску обробника, а payer = payer вказує Anchor, який обліковий запис оплачує створення.

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

Розмір акаунту

Обмеження space вказує Anchor, скільки даних акаунту потрібно виділити. Anchor спочатку зберігає 8-байтовий дискримінатор, а потім байти, необхідні для полів Counter. Дискримінатор дозволяє Anchor розпізнати цей акаунт як акаунт Counter перед десеріалізацією даних акаунту.

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

Адреса лічильника

Обмеження seeds та bump визначають очікувану PDA-адресу для акаунту лічильника. Anchor перевіряє, що наданий акаунт counter відповідає цій адресі. Шаблон використовує PDA, щоб користувачі могли отримати адресу лічильника з ідентифікатора програми та seed, роблячи адресу лічильника детермінованою.

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 є обов'язковим, оскільки створення нового акаунту використовує 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>,
}

Функція-обробник

Функція handle_initialize виконується після того, як Anchor перевірить акаунти у Initialize. Значення ctx надає обробнику доступ до цих перевірених акаунтів.

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

Початкові дані

Обробник записує перші значення до нового акаунту лічильника. Лічильник починається з 0, а платник стає авторизованою особою, яка може збільшувати лічильник надалі.

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-переказ включено лише для демонстрації того, як CPI передає акаунти до іншої програми. Структура 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(())
}

CPI Context

CpiContext::new поєднує програму, яку викликають, з акаунтами, що передаються до цієї програми. Це базова форма CPI: оберіть програму для виклику, зберіть акаунти, які вона очікує, а потім передайте обидва у виклик. Тут програма, яку викликають, — це 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

anchor_lang::system_program::transfer викликає інструкцію переказу System Program. У цьому шаблоні переказ є невеликим прикладом виклику іншої програми з вашої програми. Якщо CPI переказу завершується невдало, інструкція initialize також завершується невдало.

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

Запис повідомлення до журналу

msg! записує повідомлення до журналів програми. Ok(()) вказує на те, що інструкція виконалась успішно.

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 визначає облікові записи, які мають бути включені, коли користувач викликає інструкцію initialize. Anchor перевіряє ці облікові записи до запуску обробника.

Payer Account

Обліковий запис payer оплачує створення облікового запису лічильника. Тип Signer<'info> означає, що платник повинен підписати транзакцію, а #[account(mut)] означає, що обліковий запис платника може бути змінений, оскільки з нього будуть списані lamport.

Counter Account

Обліковий запис counter зберігає дані Counter з state.rs. init вказує Anchor створити цей обліковий запис до запуску обробника, а payer = payer вказує Anchor, який обліковий запис оплачує створення.

Розмір акаунту

Обмеження space вказує Anchor, скільки даних акаунту потрібно виділити. Anchor спочатку зберігає 8-байтовий дискримінатор, а потім байти, необхідні для полів Counter. Дискримінатор дозволяє Anchor розпізнати цей акаунт як акаунт Counter перед десеріалізацією даних акаунту.

Адреса лічильника

Обмеження seeds та bump визначають очікувану PDA-адресу для акаунту лічильника. Anchor перевіряє, що наданий акаунт counter відповідає цій адресі. Шаблон використовує PDA, щоб користувачі могли отримати адресу лічильника з ідентифікатора програми та seed, роблячи адресу лічильника детермінованою.

System Program

Акаунт system_program є обов'язковим, оскільки створення нового акаунту використовує System Program.

Функція-обробник

Функція handle_initialize виконується після того, як Anchor перевірить акаунти у Initialize. Значення ctx надає обробнику доступ до цих перевірених акаунтів.

Початкові дані

Обробник записує перші значення до нового акаунту лічильника. Лічильник починається з 0, а платник стає авторизованою особою, яка може збільшувати лічильник надалі.

Акаунти для переказу

Цей CPI-переказ включено лише для демонстрації того, як CPI передає акаунти до іншої програми. Структура Transfer містить перелік акаунтів, які використовуються при переказі через System Program.

CPI Context

CpiContext::new поєднує програму, яку викликають, з акаунтами, що передаються до цієї програми. Це базова форма CPI: оберіть програму для виклику, зберіть акаунти, які вона очікує, а потім передайте обидва у виклик. Тут програма, яку викликають, — це System Program.

Виклик Transfer

anchor_lang::system_program::transfer викликає інструкцію переказу System Program. У цьому шаблоні переказ є невеликим прикладом виклику іншої програми з вашої програми. Якщо CPI переказу завершується невдало, інструкція initialize також завершується невдало.

Запис повідомлення до журналу

msg! записує повідомлення до журналів програми. Ok(()) вказує на те, що інструкція виконалась успішно.

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 визначає акаунти, необхідні для оновлення існуючого акаунта лічильника. Обробник перевіряє, що підписант є збереженим власником, перевіряє, що лічильник не досяг зазначеного MAX_COUNT, а потім збільшує лічильник.

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

Account Context

Структура Increment визначає акаунти, які необхідно включити, коли користувач викликає інструкцію increment. Anchor перевіряє ці акаунти до запуску обробника.

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

Counter Account

Акаунт counter зберігає дані Counter. Обмеження mut дозволяє обробнику оновлювати збережений лічильник, а обмеження seeds та bump перевіряють адресу PDA лічильника.

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

Authority Signer

Акаунт authority повинен підписати транзакцію. Надалі обробник перевіряє, що цей підписант збігається з власником, збереженим в акаунті лічильника.

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

Функція обробника

Функція handle_increment виконується після того, як Anchor перевіряє акаунти в Increment. Значення ctx надає обробнику доступ до цих перевірених акаунтів.

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

Перевірка повноважень

Перша перевірка переконується, що підписант має право оновлювати цей лічильник. Якщо адреса підписанта не збігається з counter.authority, інструкція зупиняється з помилкою ErrorCode::Unauthorized. Це демонструє авторизацію на рівні застосунку: програма володіє даними лічильника, але реалізує правило, яке визначає, який підписант може змінювати ці дані.

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

Перевірка максимального значення

Друга перевірка не дозволяє лічильнику перевищити MAX_COUNT. Якщо лічильник вже досяг ліміту, інструкція зупиняється з помилкою ErrorCode::CounterOverflow. Цей ліміт є штучним правилом у шаблоні, щоб показати, як власні помилки зупиняють інструкцію до зміни даних акаунту.

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

Оновлення лічильника

Лише після того, як обидві перевірки пройдено, обробник оновлює дані акаунту. Цей рядок додає одиницю до збереженого значення лічильника.

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

Повідомлення журналу

msg! записує оновлене значення лічильника до журналів програми. Ok(()) вказує на те, що інструкція завершилась успішно.

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

Account Context

Структура Increment визначає акаунти, які необхідно включити, коли користувач викликає інструкцію increment. Anchor перевіряє ці акаунти до запуску обробника.

Counter Account

Акаунт counter зберігає дані Counter. Обмеження mut дозволяє обробнику оновлювати збережений лічильник, а обмеження seeds та bump перевіряють адресу PDA лічильника.

Authority Signer

Акаунт authority повинен підписати транзакцію. Надалі обробник перевіряє, що цей підписант збігається з власником, збереженим в акаунті лічильника.

Функція обробника

Функція handle_increment виконується після того, як Anchor перевіряє акаунти в Increment. Значення ctx надає обробнику доступ до цих перевірених акаунтів.

Перевірка повноважень

Перша перевірка переконується, що підписант має право оновлювати цей лічильник. Якщо адреса підписанта не збігається з counter.authority, інструкція зупиняється з помилкою ErrorCode::Unauthorized. Це демонструє авторизацію на рівні застосунку: програма володіє даними лічильника, але реалізує правило, яке визначає, який підписант може змінювати ці дані.

Перевірка максимального значення

Друга перевірка не дозволяє лічильнику перевищити MAX_COUNT. Якщо лічильник вже досяг ліміту, інструкція зупиняється з помилкою ErrorCode::CounterOverflow. Цей ліміт є штучним правилом у шаблоні, щоб показати, як власні помилки зупиняють інструкцію до зміни даних акаунту.

Оновлення лічильника

Лише після того, як обидві перевірки пройдено, обробник оновлює дані акаунту. Цей рядок додає одиницю до збереженого значення лічильника.

Повідомлення журналу

msg! записує оновлене значення лічильника до журналів програми. Ok(()) вказує на те, що інструкція завершилась успішно.

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

Тестовий файл

programs/my-program/tests/test_initialize.rs є інтеграційним тестом на Rust. Він не запускає локальний validator. Натомість завантажує скомпільований файл .so у LiteSVM, будує транзакції, що викликають програму, та зчитує обліковий запис лічильника після кожної транзакції. Тест будує інструкції для транзакції Solana, вказуючи ідентифікатор програми, яку потрібно викликати, надаючи instruction data та передаючи необхідні облікові записи.

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 визначає акаунти, необхідні для інструкції initialize. Тест передає ті самі акаунти до згенерованого допоміжного методу my_program::accounts::Initialize:

  • payer передається як payer: payer.pubkey().
  • counter передається як counter.
  • system_program передається як system_program::ID.

my_program::instruction::Initialize {}.data() створює instruction data. Саме тут кодуються аргументи інструкції, однак ця інструкція initialize не потребує жодних аргументів.

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 визначає акаунти, необхідні для інструкції increment. Тест передає ті самі акаунти до згенерованого допоміжного методу my_program::accounts::Increment:

  • counter передається як counter.
  • authority передається як authority: payer.pubkey().

my_program::instruction::Increment {}.data() створює instruction data. Саме тут кодуються аргументи інструкції, однак ця інструкція increment не потребує жодних аргументів.

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

Конфігурація проєкту

Кореневі файли проєкту вказують Anchor та Cargo, як збирати, тестувати та розгортати програму. Для повного довідника дивіться документацію Anchor для конфігурації Anchor.toml та 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?