InstructionError у Solana — це не одна помилка, а контейнер, який runtime повертає, коли конкретна інструкція у транзакції не виконалася. Щоб її усунути, потрібно витягнути числовий код помилки з логів RPC-відповіді, зіставити його з переліком у вихідному коді Solana runtime та перевірити стан відповідних акаунтів і даних. Нижче — покрокова схема діагностики без здогадок.

Коди InstructionError та їх значення

Solana runtime визначає InstructionError як enum з числовими варіантами. Повний перелік міститься у файлі solana-sdk/src/instruction.rs у репозиторії Solana. Ось ті, що трапляються найчастіше в production:

Код Назва Суть
0 GenericError Загальна помилка інструкції без уточнення. Зазвичай означає, що програма повернула Err(ProgramError::Custom(0)) або неспецифікований збій.
1 InvalidArgument Аргумент інструкції не пройшов валідацію: некоректне число, порожній масив, недопустиме значення enum.
2 InvalidInstructionData Серіалізовані дані інструкції не відповідають очікуваній структурі: невідповідна довжина, невідомий discriminator, пошкоджені байти.
3 InvalidAccountData Дані на акаунті, переданому в інструкцію, не відповідають очікуваній структурі або стану.
4 AccountDataTooSmall Розмір даних акаунта менший, ніж вимагає програма для десеріалізації або запису.
5 InsufficientFunds На акаунті-джерелі недостатньо SOL для виконання операції (комісія транзакції, створення акаунта, transfer).
6 IncorrectProgramOwner Акаунт, який мав бути власним для певної програми, насправді належить іншій програмі.
7 MissingRequiredSignature Акаунт, що позначений як signer, не підписав транзакцію.
8 AccountAlreadyInUse Один і той самий акаунт передано двічі в межах однієї транзакції з конфліктуючими правами доступу (writable у одному місці та readonly у іншому).
9 InstructionFallbackNotFound Програма не має обробника fallback-інструкції, але клієнт її викликав.
10 AccountNotProgramOwned Операція вимагає, щоб акаунт належав програмі, але він цього не робить.

Повний актуальний перелік варто перевіряти безпосередньо у вихідному коді версії SDK, з якою працюєте, оскільки перелік може доповнюватися.

Найпоширеніші причини: insufficient funds, invalid account

InsufficientFunds (код 5)

Ця помилка виникає, коли баланс акаунта-відправника менший за суму, яку потрібно списати. Важливо розуміти, що до суми transfer додається комісія транзакції (base fee + compute unit price × compute unit limit), яку стягує runtime до виконання будь-якої інструкції.

Типові сценарії:

  • Клієнт розраховує баланс без урахування комісії транзакції.
  • Паралельна транзакція змінила баланс між моментом побудови транзакції та моментом її подачі в кластер.
  • Програма намагається створити акаунт через create_account, але відправник не має достатньо SOL для rent-exempt мінімуму плюс комісія.

Щоб перевірити гіпотезу безпечно, викличте getBalance для акаунта-джерела через той самий RPC-endpoint, який використовує ваш сервіс, і порівняйте з сумою, яку намагається списати програма. Зверніть увагу: різні RPC-провайдери можуть мати затримку синхронізації слота.

InvalidAccountData (код 3) та AccountDataTooSmall (код 4)

Ці помилки часто супроводжують одна одну. Програма очікує на акаунті певну структуру (наприклад, 8 байтів discriminator + поля), але знаходить щось інше.

Типові сценарії:

  • Акаунт ініціалізовано іншою версією програми, де структура даних відрізнялася.
  • Акаунт не ініціалізовано взагалі (дані порожні), але програма намагається його десеріалізувати.
  • Після оновлення програми розмір структури збільшився, але старі акаунти не мігровані.
  • Передано неправильний PDA — обчислення збігається з іншим сидом, і акаунт містить дані іншого типу.

Для діагностики викличте getAccountInfo з параметром encoding: base64 і перевірте фактичний розмір даних (data.length) та перші байти. Якщо використовуєте Anchor, порівняйте discriminator (перші 8 байтів) з очікуваним хешем.

Послідовна діагностика за кодом

Нижче — універсальний алгоритм, який можна адаптувати до вашого моніторингу або локальної відладки.

Крок 1. Витягніть повний лог транзакції.

Викличте getTransaction з параметром maxSupportedTransactionVersion: 0 (або актуальною версією) та проаналізуйте поле meta.err. Там буде структура виду:

InstructionError [index, Custom(42)] або InstructionError [index, InsufficientFunds]

Значення index вказує на порядковий номер інструкції в транзакції (починаючи з 0). Це критично, коли транзакція містить кілька інструкцій або CPI-викликів.

Крок 2. Визначте, чи це Custom-помилка.

Якщо всередині стоїть Custom(N), це означає, що ваша програма повернула Err(ProgramError::Custom(N)). У такому разі коди з таблиці вище не допоможуть — шукайте значення N у вихідному коді вашої програми. У Anchor це зазвичай коди з error-enumу вашого модуля.

Крок 3. Зіставте код з переліком InstructionError.

Якщо код не Custom, знайдіть його у таблиці вище або у файлі instruction.rs відповідної версії SDK.

Крок 4. Зберіть контекст акаунтів для цієї інструкції.

Для інструкції з індексом index витягніть список акаунтів з transaction.message.accountKeys. Для кожного акаунта викличте getAccountInfo і зафіксуйте: власника, чи writable, чи signer, розмір даних, лампорт-баланс.

Крок 5. Перевірте стан на момент транзакції, а не на момент діагностики.

Якщо помилка сталася в минулому, стан акаунтів міг змінитися. Використовуйте getTransaction з повними логами та, за можливості, перевірте пре-баланси через архівні RPC-endpointи або експлорери, які зберігають історичний стан.

Крок 6. Відтворіть локально.

Підставте ті самі дані акаунтів (баланси, data, owner) у локальний тест з банківським тестом (bankrun). Це дозволить ізольовано перевірити саме цю інструкцію без впливу мережевого стану.

Типові помилки інтерпретації InstructionError

Припущення, що код InstructionError — це код вашої програми.

Це найпоширеніша помилка. Розробник бачить число і шукає його у своєму error-enumі, тоді як насправді це код із Solana runtime. Різниця очевидна в логах: InstructionError [0, InsufficientFunds] — це runtime, а InstructionError [0, Custom(42)] — це ваша програма.

Ігнорування індексу інструкції.

У транзакції з трьома інструкціями помилка InstructionError [2, ...] означає, що перші дві виконалися успішно. Якщо ви аналізуєте першу інструкцію — ви дивитесь не туди. У складних транзакціях з глибокими CPI-викликами індекс може вказувати на внутрішній виклик, і тоді потрібно аналізувати стек CPI за логами.

Плутанина між InstructionError та TransactionError.

InstructionError вкладений у TransactionError. Якщо meta.err містить TransactionError::InstructionError(index, err), то внутрішній err — це саме InstructionError. Не плутайте їх: TransactionError також має власні варіанти (BlockhashNotFound, InsufficientFundsForFee тощо), які не мають відношення до конкретної інструкції.

Діагностика за поточним станом замість стану на момент транзакції.

Якщо акаунт після невдалої транзакції змінився (інша транзакція успішно його оновила), ви побачите інші дані. Це особливо актуально для high-frequency сценаріїв та MEV-ботів. Завжди спираєтесь на дані з getTransaction або архівні слоти.

Приписування InstructionError проблемам мережі.

InstructionError майже завжди вказує на логічну помилку в даних або правах доступу, а не на тимчасові проблеми кластера. Якщо RPC повертає помилку з'єднання або таймаут — це не InstructionError. Не змішуйте ці категорії в моніторингу.

Після того як ви ідентифікували точний код і перевірили стан акаунтів, наступний логічний крок — зіставити отримані дані з логікою вашої програми. Якщо програма написана на Anchor, детальніше про роботу з її логами та структурованими помилками можна прочитати в окремому матеріалі про Anchor logs.

Джерела