Создайте свою первую программу на 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, поскольку программа хранится в program account, и этот аккаунт должен оплачивать используемое пространство. В 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 возвращается, когда подписант не является авторизованной стороной, сохранённой в аккаунте счётчика.
  • 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

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

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

Log Message

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

CpiContext::new объединяет вызываемую программу с аккаунтами, передаваемыми в эту программу. Это базовая структура CPI: выбрать программу для вызова, собрать аккаунты, которые ожидает эта программа, затем передать и то, и другое в вызов. Здесь вызываемая программа — это System Program.

Invoke Transfer

anchor_lang::system_program::transfer вызывает инструкцию передачи System Program. В этом шаблоне передача — это небольшой пример вызова другой программы из вашей программы. Если CPI передачи завершается с ошибкой, инструкция initialize также завершается с ошибкой.

Log Message

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?