Усі приклади нижче перевірені в такому середовищі: Node.js 18+, @solana/web3.js 1.95.3, @coral-xyz/anchor 0.30.1, кластер — Devnet. Припускається, що ви вже маєте обʼєкт Connection з указаним RPC-ендпоінтом Devnet.
Отримання інформації про акаунт
getAccountInfo — запит до RPC
Метод getAccountInfo — це базовий інструмент для читання стану будь-якого акаунта в Solana. Він повертає метадані акаунта (лампорти, власник, прапорець executable) та необроблені байти даних.
import { Connection, PublicKey } from '@solana/web3.js';
const connection = new Connection('https://api.devnet.solana.com', 'confirmed');
const address = new PublicKey('7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU');
const accountInfo = await connection.getAccountInfo(address);
if (accountInfo === null) {
console.log('Акаунт не існує');
} else {
console.log('Лампорти:', accountInfo.lamports);
console.log('Власник:', accountInfo.owner.toBase58());
console.log('Executable:', accountInfo.executable);
console.log('Розмір даних (байти):', accountInfo.data.length);
console.log('Rent epoch:', accountInfo.rentEpoch);
}
Очікуваний результат: обʼєкт AccountInfo<Buffer> або null, якщо акаунт не знайдено. За замовчуванням дані повертаються як Buffer (внутрішньо використовується кодування base64).
Корисний параметр — dataSlice, який дозволяє завантажити лише частину даних, економлячи трафік:
const partialInfo = await connection.getAccountInfo(address, {
encoding: 'base64',
dataSlice: { offset: 0, length: 8 }, // лише перші 8 байтів
});
Це особливо корисно, коли вам потрібне лише одне поле з великого акаунта, і ви не хочете завантажувати кілобайти даних через мережу.
Десеріалізація отриманих даних
Raw-байти з accountInfo.data самі по собі не мають сенсу — їх треба десеріалізувати згідно зі структурою, яку визначає програма-власник. Для програм, написаних без Anchor, зазвичай використовують Borsh — компактний бінарний формат серіалізації.
Приклад: програма зберігає лічильник як u64 (8 байтів).
import * as borsh from 'borsh';
class CounterAccount {
count = 0;
constructor(fields?: { count: number }) {
if (fields) this.count = fields.count;
}
}
const schema = new Map([
[CounterAccount, { kind: 'struct', fields: [['count', 'u64']] }],
]);
// accountInfo.data — Buffer з попереднього кроку
const deserialized = borsh.deserialize(
schema,
CounterAccount,
accountInfo.data
);
// u64 десеріалізується як BN-екземпляр
console.log('Лічильник:', deserialized.count.toString());
Ключовий момент: порядок полів у схемі має точно збігатися з порядком, у якому програма на Rust записує дані. Навіть одна переставлена позиція призведе до некоректного результату без помилки на рівні десеріалізації.
Робота з програмними акаунтами
Читання стану Anchor-програми через IDL
Anchor-програми генерують IDL (Interface Definition Language) — JSON-опис усіх акаунтів, інструкцій та типів. Завдяки цьому вам не потрібно вручну писати схему Borsh: Anchor-клієнт десеріалізує дані автоматично.
import { Program, AnchorProvider } from '@coral-xyz/anchor';
import idl from '../target/idl/my_program.json';
const provider = new AnchorProvider(connection, wallet, {});
const programId = new PublicKey('11111111111111111111111111111111');
const program = new Program(idl, programId, provider);
const pda = PublicKey.findProgramAddressSync(
[Buffer.from('counter')],
programId
)[0];
const account = await program.account.counter.fetch(pda);
console.log('Лічильник:', account.count.toString());
Очікуваний результат: типізований обʼєкт, де кожне поле відповідає структурі в IDL. Метод fetch під капотом викликає getAccountInfo і десеріалізує відповідь.
Використання згенерованого клієнта
Після виконання anchor build у папці target/types/ зʼявляються згенеровані TypeScript-типи. Їхнє підключення дає перевірку типів на етапі компіляції, а не лише під час виконання.
import { MyProgram } from '../target/types/my_program';
const program = new Program<MyProgram>(idl, programId, provider);
// Тепер account.count має коректний тип (BN)
// Помилка типу виникне, якщо ви звернетеся до неіснуючого поля
const account = await program.account.counter.fetch(pda);
console.log('Лічильник:', account.count.toString());
Різниця між підходами: IDL без згенерованих типів дає десеріалізацію під час виконання, але без автодоповнення в редакторі. Згенерований клієнт додає статичну типізацію — це концептуальний приклад для розробки, у production-проєктах завжди використовуйте згенеровані типи.
Отримання балансу
getBalance — запит балансу SOL
Для читання балансу SOL не потрібен getAccountInfo — є спеціалізований метод, який повертає лише кількість лампортів.
const lamports = await connection.getBalance(address);
const sol = lamports / 1_000_000_000;
console.log('Баланс:', sol, 'SOL');
Очікуваний результат: число (lamports як number). Для акаунтів із дуже великим балансом врахуйте обмеження Number.MAX_SAFE_INTEGER — у таких випадках використовуйте getBalance з параметром commitment і обробляйте значення обережно.
getTokenAccountBalance — запит балансу токена
Для SPL-токенів баланс зберігається в спеціальному акаунті — token account (власник якого — Token Program). Адреса такого акаунта відрізняється від адреси гаманця користувача.
const tokenAccountAddress = new PublicKey('TokenAccountAddressHere');
const tokenBalance = await connection.getTokenAccountBalance(tokenAccountAddress);
console.log('Сирий баланс:', tokenBalance.amount); // string
console.log('Десяткові знаки:', tokenBalance.decimals); // number
console.log('Форматований:', tokenBalance.uiAmountString); // string | null
Очікуваний результат: обʼєкт TokenBalance з полями amount (рядок), decimals (число), uiAmount (число або null), uiAmountString (рядок або null). Поле amount завжди рядок, оскільки значення може перевищувати Number.MAX_SAFE_INTEGER.
Важливо: якщо передати адресу звичайного акаунта (не token account), RPC поверне помилку. Переконайтеся, що акаунт належить Token Program (TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA).
Підписка на зміни стану
onAccountChange — відстеження оновлень
Solana RPC підтримує WebSocket-підписки, які дозволяють отримувати оновлення акаунта в реальному часі без повторних опитувань.
const subscriptionId = connection.onAccountChange(
address,
(updatedAccountInfo, context) => {
console.log('Слот оновлення:', context.slot);
console.log('Нові дані:', updatedAccountInfo.data.toString('base64'));
console.log('Лампорти:', updatedAccountInfo.lamports);
},
'confirmed' // рівень commitment для підписки
);
// Для скасування підписки:
// connection.removeAccountChangeListener(subscriptionId);
Очікуваний результат: callback викликається щоразу, коли стан акаунта змінюється. Параметр context.slot містить номер слота, в якому відбулася зміна.
Обовʼязково зберігайте subscriptionId і викликайте removeAccountChangeListener при завершенні роботи. Нескасовані підписки призводять до витоку ресурсів як на клієнті, так і на стороні RPC-провайдера.
Коли використовувати підписки
- Використовуйте підписки: фронтенд-застосунки, де потрібно миттєво оновлювати UI після зміни стану (наприклад, баланс гаманця, стан лічильника в грі, статус ордера).
- Використовуйте polling (періодичні виклики
getAccountInfo): серверні процеси, пакетна обробка, скрипти міграції, ситуації, коли WebSocket-зʼєднання нестабільне. - Не використовуйте підписки: якщо вам потрібне одноразове читання, якщо ви обробляєте історичні дані, якщо RPC-провайдер обмежує кількість одночасних WebSocket-підключень.
На Devnet публічні RPC-ендпоінти можуть обмежувати кількість активних підписок. У production-середовищі використовуйте власний RPC-вузол або платного провайдера з гарантованою підтримкою WebSocket.
Типові помилки
Акаунт не знайдено (null response)
getAccountInfo повертає null, коли акаунт з такою адресою ніколи не створювався або був закритий (лампорти виведено, дані очищено). Це не помилка RPC — це коректна відповідь.
const info = await connection.getAccountInfo(address);
// Неправильно — спроба звернутися до .data викличе TypeError
// console.log(info.data.length);
// Правильно — обовʼязкова перевірка на null
if (!info) {
console.log('Акаунт не знайдено. Можливо, його ще не створено або вже закрито.');
// Тут: ініціалізація акаунта або альтернативна логіка
}
Поширена причина в Devnet: акаунт створювався в одній сесії, але його адреса була втрачена, або airdrop-лампорти не були використані для створення акаунта через SystemProgram.createAccount.
Невірна десеріалізація даних
Ця помилка не викликає винятку під час десеріалізації — ви просто отримуєте некоректні значення, які виглядають правдоподібно.
Типові причини:
- Невідповідність схеми: порядок полів у вашій схемі Borsh не збігається з порядком у Rust-структурі програми. Рішення: звірити з вихідним кодом програми.
- Невірний розмір буфера: ви очікуєте 8 байтів, а акаунт містить 16 (наприклад, програма була оновлена і додала нове поле). Рішення: перевірити
accountInfo.data.lengthперед десеріалізацією. - Зміна ендіанності: Borsh використовує little-endian, і це враховано автоматично. Але якщо ви десеріалізуєте вручну через
DataView, легко помилитися з порядком байтів. - Застарілий IDL: при використанні Anchor ви завантажили стару версію IDL, яка не відповідає розгорнутій програмі. Рішення: перезібрати програму і скопіювати свіжий IDL.
// Безпечний патерн: перевірка розміру перед десеріалізацією
const EXPECTED_SIZE = 8; // u64 = 8 байтів
if (accountInfo.data.length !== EXPECTED_SIZE) {
throw new Error(
`Несподіваний розмір даних: очікувалось ${EXPECTED_SIZE}, ` +
`отримано ${accountInfo.data.length}. Можливо, програму оновлено.`
);
}
const deserialized = borsh.deserialize(schema, CounterAccount, accountInfo.data);
Цей патерн дозволяє виявити невідповідність на ранньому етапі замість мовчазної роботи з неправильними даними.