Anchor: проверки как декларация
В прошлом уроке мы держали в голове гору проверок: владение, размеры, кто создаёт аккаунт, скоро добавится подпись. Всё это ручное, и каждую забытую проверку злоумышленник превращает в дыру. Anchor это фреймворк, который говорит: опиши, каким должен быть аккаунт, а проверки я сгенерирую сам. Перепишем на него тот же счётчик и посмотрим, что уходит из кода и куда.
Идея: описать, а не проверять
Anchor переносит центр тяжести с императивного «проверь то, проверь это» на декларативное «аккаунт должен быть таким». Ты пишешь структуру аккаунтов с атрибутами, а макрос #[derive(Accounts)] разворачивает её в код, который до вызова твоего обработчика уже сверил владельца, подпись, размер и что аккаунт вообще тот. Твоя функция получает готовые, проверенные аккаунты.
Сравним лоб в лоб. Вот наш счётчик из прошлого урока, но на Anchor. Примеры проверены на Anchor 0.31 (актуальная мажорная версия на момент написания), API в этом виде стабилен начиная с 0.30.
Программа: модуль вместо match
use anchor_lang::prelude::*;
declare_id!("Cnt1111111111111111111111111111111111111111");
#[program]
pub mod counter {
use super::*;
pub fn init(ctx: Context<Init>) -> Result<()> {
ctx.accounts.counter.count = 0;
msg!("счётчик инициализирован нулём");
Ok(())
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
counter.count = counter.count.checked_add(1).ok_or(CounterError::Overflow)?;
msg!("счётчик теперь {}", counter.count);
Ok(())
}
}
Пара слов про declare_id!, пока адрес выглядит игрушечным. Настоящий адрес не выдумывают: при первом anchor build Anchor генерирует keypair программы в target/deploy/counter-keypair.json, и публичный ключ этой пары станет адресом деплоя. Команда anchor keys sync подставляет его в declare_id! и в Anchor.toml, чтобы код, конфиг и реальный деплой указывали на один адрес.
Заметь, что пропало. Нет match по инструкции: каждый публичный метод модуля #[program] это отдельная команда, Anchor сам разберёт байты и позовёт нужный метод. Нет ручной десериализации состояния: ctx.accounts.counter уже готовый типизированный аккаунт. Нет проверки owner: она уедет в описание аккаунтов.
Context<Init> это те самые три аргумента нативной программы, только причёсанные: вместо сырого массива accounts у тебя типизированная структура полей.
Аккаунты: где живут проверки
Вот куда ушли проверки. Каждая инструкция объявляет свою структуру аккаунтов с атрибутами.
#[derive(Accounts)]
pub struct Init<'info> {
// init: создать аккаунт (Anchor сам зовёт System Program create_account).
// payer: кто платит rent-exempt залог. space: сколько байт выделить
// (8 байт дискриминатор Anchor + 8 байт под u64).
#[account(init, payer = signer, space = 8 + 8)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut)]
pub counter: Account<'info, Counter>,
}
// Состояние аккаунта. #[account] добавляет 8-байтовый дискриминатор,
// по которому Anchor отличает Counter от других типов и ловит подмену.
#[account]
pub struct Counter {
pub count: u64,
}
Разберём, что каждый атрибут делает за тебя.
Account<'info, Counter>это не просто ссылка. Сам типAccountпри разборе проверяет, чтоownerаккаунта равен нашей программе, и десериализует данные вCounter. Ручная проверка владения из прошлого урока встроена в тип.init, payer = signer, space = ...делает то, что в нативной программе было отдельной инструкциейcreate_accountк System Program и следующей за нейInit. Anchor вызывает System Program сам, кладёт залог отsignerи выделяетspaceбайт. Оттого рядом обязателенsystem_program.Signer<'info>проверяет, что этот аккаунт подписал транзакцию. Проверкуis_signer, которую в токене мы будем писать руками, тут даёт сам тип.#[account(mut)]говорит, что аккаунт изменится, и Anchor пометит его как writable и запишет обратно после обработчика.
8-байтовый дискриминатор это то, ради чего space = 8 + 8: первые восемь байт Anchor тратит на метку типа и потом ловит подмену одного типа состояния другим.
Ограничения посерьёзнее: PDA, has_one, свои условия
На счётчике проверок мало. Настоящая сила видна на связанных аккаунтах. Несколько частых ограничений (пригодятся в уроке 35 и 36). Это эскиз, а не полный листинг: типы Vault, Config и Receipt тут не объявлены, смотри только на атрибуты.
#[derive(Accounts)]
pub struct Withdraw<'info> {
// seeds + bump: аккаунт обязан быть PDA, выведенным из этих сидов.
// Anchor сам проверит адрес и положит bump в ctx.bumps.
#[account(
mut,
seeds = [b"vault", owner.key().as_ref()],
bump,
)]
pub vault: Account<'info, Vault>,
// has_one: поле vault.owner обязано равняться аккаунту owner ниже.
// Одна строка вместо ручного сравнения ключей.
#[account(has_one = owner)]
pub config: Account<'info, Config>,
pub owner: Signer<'info>,
// constraint: любое своё условие, с адресным сообщением об ошибке.
// Атрибут висит на receipt, но условие внутри может смотреть на поля
// любого аккаунта структуры: здесь оно проверяет поле vault.balance.
#[account(constraint = vault.balance > 0 @ CounterError::EmptyVault)]
pub receipt: Account<'info, Receipt>,
}
Каждая строка тут это проверка, которую иначе ты писал бы в теле обработчика руками и мог бы забыть. Anchor превращает забытую проверку в отсутствующий атрибут, а не в тихую дыру.
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.