Цей швидкий старт використовує стартовий проєкт
фреймворку 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, оскільки програма зберігається в акаунті, і акаунт повинен оплачувати використовуваний простір. У 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повертається, коли підписант не є authority, збереженим в акаунті лічильника.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 Context
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(())}
Виклик 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(())}
Запис повідомлення до журналу
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?