В этом кратком руководстве используется стартовый проект
фреймворка Anchor, сгенерированный командой
anchor init. Вы создадите проект локально, запустите его тесты, соберёте
программу и изучите код программы, который генерирует Anchor.
Предварительные требования
Прежде чем начать, установите инструменты разработки Solana. В установку входят Rust, Solana CLI и Anchor CLI.
Используйте Anchor CLI версии 1.1.2 или выше для этого шаблона. Проверьте установленную версию:
$anchor --version
Создание проекта
Выполните следующие команды в терминале:
$anchor init my-program$cd my-program
Стартовый проект включает одну программу Solana в programs/my-program.
Программа содержит две инструкции: одна инициализирует аккаунт счётчика, другая
увеличивает значение счётчика.
Некоторые части шаблона демонстрируют распространённые паттерны программ Solana: получение адресов аккаунтов PDA, выполнение Cross Program Invocation (CPI) для перевода SOL, а также использование пользовательских проверок ошибок для остановки инструкции при невыполнении условия.
Сборка программы
Выполните anchor build для компиляции стартовой программы:
$anchor build
Скомпилированная программа записывается в target/deploy/my_program.so. После
развёртывания программы содержимое этого файла .so сохраняется в аккаунте в
блокчейне.
Запустить тест
Запустите тест по умолчанию:
$anchor test
Anchor.toml этого шаблона использует команду тестирования Rust:
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:
$solana airdrop 2 --url devnet
$anchor build$anchor deploy --provider.cluster devnet
Исходные файлы
Директория src содержит программу Solana. Документация Anchor по
структуре программы
объясняет основные макросы, используемые здесь, включая declare_id!,
#[program], #[derive(Accounts)] и #[account]. В этом разделе
рассматриваются файлы шаблона.
lib.rs
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)}}
[programs.localnet]my_program = "82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd"
declare_id!("82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd");
Один и тот же адрес программы указан в конфигурации и в коде. Anchor.toml
сообщает Anchor, на какой адрес развёртывать программу или обращаться к ней в
кластере. declare_id! задаёт адрес программы внутри самой программы для
проверок безопасности.
constants.rs
constants.rs хранит общие значения в одном месте. В этом шаблоне
COUNTER_SEED используется для получения PDA счётчика,
HELLO_WORLD_LAMPORTS переводится при инициализации, а MAX_COUNT
проверяется перед инкрементом.
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;
#[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задаёт количество лампортов, переводимых от плательщика на аккаунт счётчика.
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 определяет пользовательские типы данных для аккаунтов, которые
программа создаёт и которыми владеет. Программа определяет инструкции для
создания, инициализации и обновления этих данных, однако данные счётчика
хранятся не внутри самой программы, а в отдельном аккаунте с собственным
адресом.
use anchor_lang::prelude::*;#[account]#[derive(InitSpace)]pub struct Counter {pub count: u64,pub authority: Pubkey,}
#[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(())}
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.
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,}
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 проверит эти аккаунты.
pub mod initialize;pub mod increment;pub use initialize::*;pub use increment::*;
initialize.rs
initialize.rs определяет аккаунты, необходимые для создания аккаунта счётчика,
и затем записывает первоначальные значения аккаунта. Структура
#[derive(Accounts)] использует
ограничения аккаунтов
Anchor, чтобы указать, какие аккаунты требуются и как создаётся новый аккаунт
счётчика.
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 проверяет эти
аккаунты до запуска обработчика.
#[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.
#[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, какой аккаунт оплачивает создание.
#[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 перед десериализацией данных аккаунта.
#[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, делая адрес счётчика
детерминированным.
#[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.
#[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 предоставляет обработчику доступ
к этим проверенным аккаунтам.
#[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, а плательщик становится владельцем, которому разрешено
увеличивать значение счётчика в дальнейшем.
#[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.
#[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.
#[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 также завершается с ошибкой.
#[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(()) указывает на
то, что инструкция выполнена успешно.
#[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(())}
increment.rs
increment.rs определяет аккаунты, необходимые для обновления существующего
аккаунта счётчика. Обработчик проверяет, что подписант является сохранённым
владельцем, проверяет, что счётчик не достиг указанного MAX_COUNT, а затем
увеличивает значение счётчика.
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 проверяет эти аккаунты
до запуска обработчика.
#[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-адрес счётчика.
#[derive(Accounts)]pub struct Increment<'info> {#[account(mut,seeds = [COUNTER_SEED],bump)]pub counter: Account<'info, Counter>,pub authority: Signer<'info>,}
Authority Signer
Аккаунт authority должен подписать транзакцию. Впоследствии обработчик
проверяет, что этот подписант совпадает с владельцем, сохранённым в аккаунте
счётчика.
#[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 предоставляет обработчику доступ
к этим проверенным аккаунтам.
#[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. Это
демонстрирует авторизацию на уровне приложения: программа владеет данными
счётчика, но реализует правило, определяющее, какой подписант может изменять эти
данные.
#[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. Этот лимит является искусственным ограничением
в шаблоне, чтобы показать, как пользовательские ошибки останавливают выполнение
инструкции до изменения данных аккаунта.
#[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(())}
Обновление счётчика
Только после прохождения обеих проверок обработчик обновляет данные аккаунта. Эта строка прибавляет единицу к сохранённому значению счётчика.
#[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(()) означает, что инструкция выполнена успешно.
#[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(())}
Тестовый файл
programs/my-program/tests/test_initialize.rs — это интеграционный тест на
Rust. Он не запускает локальный validator. Вместо этого он загружает
скомпилированный файл .so в LiteSVM, формирует транзакции, вызывающие
программу, и считывает аккаунт счётчика после каждой транзакции. Тест формирует
инструкции для транзакции Solana, указывая идентификатор вызываемой программы,
предоставляя instruction data и передавая необходимые аккаунты.
#[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>,}
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 не требует никаких аргументов.
#[derive(Accounts)]pub struct Increment<'info> {#[account(mut,seeds = [COUNTER_SEED],bump)]pub counter: Account<'info, Counter>,pub authority: Signer<'info>,}
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 не требует никаких аргументов.
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.
skip_local_validator = true[toolchain][features]resolution = trueskip-lint = false[programs.localnet]my_program = "82sFkffP9wxwpyfZyeaKHH2chQoJPUGsJZSPi9mrUuXd"[provider]cluster = "localnet"wallet = "~/.config/solana/id.json"[scripts]test = "cargo test"[hooks]
Is this page helpful?