У Solana кожна програма зберігає стан не «всередині себе», а в окремих акаунтах — записах у блокчейні, які програма читає та модифікує. Anchor автоматизує найбільш помилковозбігну частину цього процесу: серіалізацію, перевірку прав власності та розрахунок простору. Цей матеріал дає повну ментальну модель того, як визначати акаунти, ініціалізувати їх та зв'язувати з instruction.
Версії для прикладів у «Anchor accounts: визначення та зв'язування даних» перевірено 2 серпня 2026 року. Стабільна гілка Anchor v1 має релізи 1.0.x і орієнтується на Solana 3.x; Anchor v2 у документації позначений як alpha. Приклади для Anchor 0.29–0.32 залишаються лише відтворюваними прикладами для зафіксованого legacy-середовища: їх не слід переносити в новий проєкт без міграції залежностей і повторного тестування.
Середовище: Anchor 0.29.0, Solana CLI 1.18.x, кластер Devnet.
Передумови: знайомство з базовою структурою Anchor-програми та написанням першої instruction (див. Як написати першу instruction на Anchor).
Що таке account struct
Account struct — це Rust-структура, яка описує, які саме дані зберігаються в конкретному акаунті. Вона не є акаунтом сама по собі: це лише схема (schema), за якою Anchor пакує та розпаковує байти.
#[account] макрос та його роль
Атрибут #[account] над структурою виконує кілька критичних функцій:
- Додає 8-байтовий дискримінатор на початку серіалізованих даних — унікальний хеш імені структури, за яким Anchor розрізняє типи акаунтів під час десеріалізації.
- Реалізує трейти
AccountSerialize,AccountDeserializeтаAnchorSerialize/AnchorDeserialize. - Реалізує трейт
Owner, який повертаєcrate::ID— ідентифікатор поточної програми. Це гарантує, що акаунт належить саме вашій програмі. - Реалізує трейт
Discriminatorдля доступу до константного дискримінатора.
Концептуальний приклад:
#[account]перетворює звичайну структуру даних на «схему акаунта» — об'єкт, який Anchor знає як серіалізувати, як перевіряти на власність і як відрізнити від інших типів акаунтів вашої програми.
Автоматична реалізація серіалізації та десеріалізації
Коли ви передаєте акаунт у instruction, Anchor читає його байти з блокчейну, перевіряє перші 8 байтів (дискримінатор) і, якщо він збігається, десеріалізує решту даних у ваші поля. Зворотній процес відбувається при записі. Вам не потрібно писати жодного коду для цього — #[account] робить усе автоматично.
Важливе обмеження: всі поля структури мають бути типами, які підтримують серіалізацію через Borsh. Це стандартні типи Rust (u8, u64, bool, String, Pubkey), масиви фіксованого розміру, вектори (Vec<T>) та інші структури з реалізованими відповідними трейтами.
Визначення стану акаунта
Поля struct: типи, атрибути
Кожне поле визначає одиницю даних, яка зберігається на ланцюгу. Вибір типу безпосередньо впливає на вартість створення акаунта, оскільки в Solana ви оплачуєте кожен зарезервований байт.
Приклад — акаунт для зберігання нотатки:
#[account]
pub struct Note {
pub author: Pubkey,
pub content: String,
pub created_at: i64,
}
Тут три поля з різною семантикою:
author: Pubkey— 32 байти, ідентифікатор творця нотатки.content: String— змінної довжини, у серіалізованому вигляді займає 4 байти (довжина) плюс фактичний вміст.created_at: i64— 8 байтів, Unix-таймстамп створення.
Якщо вам потрібно зберегти фіксований розмір рядка (що рекомендовано для точного розрахунку space), використовуйте масив: pub content: [u8; 256] — завжди рівно 256 байтів.
Визначення space для акаунта
У Solana акаунт має фіксований розмір, який виділяється при створенні і не може бути змінений пізніше. Тому ви зобов'язані розрахувати space завчасно.
Формула:
space = 8 (дискримінатор) + розмір кожного поля
Для структури Note з content як String з максимальною довжиною 256 символів:
| Компонент | Розмір у байтах |
|---|---|
Дискримінатор (#[account]) |
8 |
author: Pubkey |
32 |
content: String (4 байти довжина + 256 символів) |
260 |
created_at: i64 |
8 |
| Разом | 308 |
Якщо замінити String на [u8; 256], розрахунок спрощується: 8 + 32 + 256 + 8 = 304. Фіксовані масиви дають точніший контроль і менший ризик помилки.
Ініціалізація акаунта
init constraint — створення нового акаунта
Constraint init у контексті instruction виконує цілу послідовність дій:
- Обчислює PDA (Program Derived Address) для акаунта на основі вказаних сидів.
- Перевіряє, що акаунт за цією адресою ще не існує.
- Викликає
system_program.create_account, виділяючи вказанийspace. - Присвоює програмі власність над акаунтом (
owner = crate::ID). - Ініціалізує перші 8 байтів дискримінатором типу.
- Встановлює лічильник rent-exempt статусу (зарезервовані SOL повертаються при закритті акаунта).
Приклад контексту для створення нотатки:
#[derive(Accounts)]
pub struct CreateNote<'info> {
#[account(init, payer = author, space = 8 + 32 + 4 + 256 + 8)]
pub note: Account<'info, Note>,
#[account(mut)]
pub author: Signer<'info>,
pub system_program: Program<'info, System>,
}
payer, space, system_program
Кожен з цих елементів виконує конкретну роль:
payer— акаунт, який сплачує rent за створення. Повинен матиmut, оскільки його баланс зменшується. Зазвичай це підписувач (Signer).space— загальний розмір акаунта в байтах. Якщо вказати замало — дані не влізуть і транзакція впаде з помилкою. Якщо забагато — ви переплачуєте rent. Рекомендується розраховувати точно.system_program— обов'язковий параметр, оскільки під капотомinitвикликає системну програму для створення акаунта. Без цього поля Anchor не згенерує правильний CPI-виклик.
Очікуваний результат: після виклику instruction з таким контекстом на Devnet буде створено новий PDA-акаунт з виділеним простором 308 байтів, власником якого стане ваша програма, а баланс payer зменшиться на суму rent-exempt мінімуму для цього розміру.
Зв'язування акаунтів
Як instruction отримує доступ до даних
Зв'язування акаунта з instruction відбувається через тип Account<'info, T> у контекстній структурі. Коли клієнт передає адресу акаунта в транзакції, Anchor:
- Знаходить акаунт за вказаною адресою в списку акаунтів транзакції.
- Перевіряє, що
ownerакаунта збігається зT::owner()(ідентифікатором вашої програми). - Перевіряє, що перші 8 байтів збігаються з дискримінатором типу
T. - Десеріалізує решту байтів у структуру
T.
Якщо хоча б одна перевірка не проходить — транзакція відхиляється з описовою помилкою.
Приклад контексту для читання існуючої нотатки:
#[derive(Accounts)]
pub struct ReadNote<'info> {
#[account(
constraint = note.author == author.key()
)]
pub note: Account<'info, Note>,
pub author: Signer<'info>,
}
Тут додатковий constraint гарантує, що тільки автор може читати свою нотатку через цю instruction. Без нього будь-хто міг би передати адресу чужої нотатки.
Зміна стану через mutable reference
Щоб змінити дані акаунта, використовуйте Account<'info, T> з атрибутом mut:
#[derive(Accounts)]
pub struct UpdateNote<'info> {
#[account(
mut,
constraint = note.author == author.key()
)]
pub note: Account<'info, Note>,
pub author: Signer<'info>,
}
У тілі instruction ви працюєте з note як зі звичайною змінною Rust:
pub fn update_note(ctx: Context<UpdateNote>, new_content: String) -> Result<()> {
let note = &mut ctx.accounts.note;
note.content = new_content;
Ok(())
}
Anchor автоматично серіалізує зміни та включає оновлені дані в транзакцію. Вам не потрібно викликати жодних методів збереження — це відбувається неявно при успішному завершенні instruction.
Ключове правило: якщо ви додали mut до акаунта в контексті, але не змінили жодного поля — це не є помилкою. Однак якщо ви змінили поле без mut, компілятор Rust видасть помилку на етапі компіляції, ще до розгортання.
Типові помилки
AccountAlreadyInUse
Ця помилка виникає, коли init намагається створити акаунт за адресою, яка вже існує. Найчастіші причини:
- Ви викликали instruction двічі з однаковими сидами. Оскільки PDA детерміновано обчислюється з сидів, адреса буде тією ж самою.
- Інша програма або instruction вже створила акаунт за цією адресою раніше.
- Ви використовуєте
initзамістьinit_if_neededу сценарії, де акаунт може вже існувати.
Рішення: перевірте, чи унікальні сиди для кожного виклику. Для ідемпотентного створення використовуйте init_if_needed (але пам'ятайте: це вимагає додаткових перевірок безпеки у production-коді).
Невірний розмір space
Два варіанти помилки:
Занижений space. Коли ви намагаєтеся записати дані, що перевищують зарезервований розмір. Наприклад, виділили 8 + 32 + 4 + 32 + 8 = 84 байти (для рядка до 32 символів), але користувач передав рядок з 50 символів. Транзакція впаде з помилкою серіалізації.
Завищений space. Програма працюватиме, але ви переплачуєте rent. Для Devnet це не критично, але для mainnet — прямі втрати користувачів.
Практична рекомендація: розраховуйте space один раз, записуйте формулу в коментар поруч із init, і при зміні полів структури оновлюйте розрахунок. Приклад:
// space = 8 (discriminator) + 32 (Pubkey) + 4+256 (String) + 8 (i64) = 308
#[account(init, payer = author, space = 308)]
pub note: Account<'info, Note>,
Це дозволяє будь-кому, хто читає код, перевірити правильність розрахунку без повторного підрахунку.
Наступний крок у навчанні — Як тестувати просту програму локально, де ви перевірите створення та модифікацію акаунтів на локальному валідаторі.