Версії для прикладів у «Що таке CPI у Solana» перевірено 2 серпня 2026 року. Стабільна гілка Anchor v1 має релізи 1.0.x і орієнтується на Solana 3.x; Anchor v2 у документації позначений як alpha. Приклади для Anchor 0.29–0.32 залишаються лише відтворюваними прикладами для зафіксованого legacy-середовища: їх не слід переносити в новий проєкт без міграції залежностей і повторного тестування.
CPI (Cross-Program Invocation) — це механізм, який дозволяє одній програмі на Solana викликати іншу програму під час виконання транзакції. Без CPI кожна програма була б ізольованою: вона могла б працювати лише з власними даними. CPI знімає цю ізоляцію й утворює основу для будь-якої нетривіальної логіки в екосистемі — від переказу токенів до взаємодії з децентралізованими біржами.
Означення CPI
Cross-Program Invocation — виклик однією програмою іншою
CPI — це процес, під час якого виконувана програма передає керування іншій програмі, передаючи їй набір акаунтів і інструкцію для обробки. Після завершення виклику керування повертається до початкової програми. Уся дія відбувається в межах однієї транзакції: немає окремих мережевих запитів, немає додаткових підписів користувача.
Ключова властивість CPI: права доступу (signer, writable) передаються від батьківської програми до дочірньої, але з важливими винятками, про які нижче.
Аналогія з викликом функції між модулями
У традиційній розробці на Rust ви імпортуєте модуль і викликаєте його функцію. У Solana програми не можуть імпортувати одна одну на рівні байт-коду — кожна програма є окремим розгорнутим контрактом. CPI відіграє роль такого «виклику функції», але через інтерфейс виконувача (runtime) Solana: ви вказуєте адресу програми, список акаунтів і серіалізовані дані інструкції.
Різниця у тому, що виклик між модулями відбувається в межах одного процесу, а CPI — це формалізований перехід керування між різними програмами з перевіркою прав на рівні runtime.
Як працює CPI
invoke та invoke_signed
У Solana існують дві базові функції для CPI, які доступні через crate solana-program:
invoke — використовується, коли всі необхідні підписи вже присутні в транзакції (наприклад, користувач підписав транзакцію, і програма передає цей підпис далі).
invoke_signed — використовується, коли програма має надати підпис від імені PDA (Program Derived Address). PDA не має приватного ключа, тому підпис генерується програмно через набір seed-ів.
Нижче наведено концептуальний приклад виклику System Program для створення акаунта. Це не production-рішення, а ілюстрація того, як виглядає структура виклику.
Середовище: Anchor 0.30.1, Solana CLI 1.18.26, кластер Devnet.
solana_program::program::invoke(
&instruction, // інструкція для цільової програми
&account_infos, // масив акаунтів, які передаються
)?;
У фреймворку Anchor ці виклики абстрагуються через моделі акаунтів та контексти (CPI context), але під капотом відбувається саме виклик invoke або invoke_signed.
Передача акаунтів та даних між програмами
Під час CPI програма формує два об'єкти:
- Інструкція — містить ідентифікатор програми (pubkey), масив метаданих акаунтів (які саме акаунти передаються і які права на них є) та серіалізовані дані інструкції (байти, які цільова програма розпізнає як конкретну команду).
- Масив акаунтів — посилання на об'єкти
AccountInfo, які вже пройшли перевірку в батьківській програмі.
Runtime Solana перевіряє, що кожен акаунт у масиві відповідає метаданим у інструкції: якщо акаунт позначений як writable у інструкції, він має бути writable і в транзакції. Ця перевірка запобігає підвищенню привілеїв під час CPI.
Обмеження CPI
Глибина рекурсії (4 рівні)
Solana обмежує глибину вкладених викликів CPI чотирма рівнями. Лічильник починається з нуля для першої програми в транзакції, тобто максимально допустимий ланцюжок виглядає так:
- Програма A (рівень 0) викликає Програму B
- Програма B (рівень 1) викликає Програму C
- Програма C (рівень 2) викликає Програму D
- Програма D (рівень 3) викликає Програму E
Якщо Програма E спробує зробити CPI, runtime поверне помилку InstructionError::CallDepth. Це обмеження існує для запобігання нескінченній рекурсії та перевантаженню валідаторів.
Compute units: кожен CPI додає витрати
Кожен виклик CPI споживає compute units на два компоненти:
- Накладні витрати самого виклику — серіалізація інструкції, перевірка прав акаунтів, передача керування.
- Виконання цільової програми — логіка програми, до якої ви звертаєтесь.
Стандартний ліміт на транзакцію — 200 000 compute units (можна збільшити до 1 400 000 з приоритетною комісією). Глибокі ланцюжки CPI з кількома програмами можуть швидко вичерпати цей ліміт. Під час розробки на Devnet завжди перевіряйте спожиті compute units через логи транзакції (solana confirm -v), щоб уникнути несподіваних помилок на Mainnet.
Типові сценарії CPI
Виклик System Program (створення акаунта)
Найчастіший сценарій — програма створює новий акаунт для зберігання даних. Для цього вона викликає System Program через CPI з інструкцією create_account, передаючи PDA як signera (через invoke_signed).
Це концептуальна структура того, що відбувається:
- Програма обчислює PDA для нового акаунта.
- Формує інструкцію
create_accountдля System Program (вказує розмір, ламports, власника). - Викликає
invoke_signed, передаючи PDA-підпис. - Після успішного CPI програма може записувати дані в створений акаунт.
Детальний приклад коду для цього сценарію розглядається в розділі Як створити PDA у програмі.
Виклик Token Program (transfer, mint)
Щоб програма могла переказати токени (SPL) або здійснити mint, вона викликає Token Program через CPI. Типові інструкції:
transfer— переказ токенів між акаунтами. Програма передає акаунти відправника та отримувача, а також токен-акаунти.mint_to— емісія нових токенів на вказаний токен-акаунт. Викликається з PDA-підписом mint-авторитету.burn— знищення токенів.
У всіх цих випадках програма не зберігає токени сама по собі — вона делегує роботу з токен-акаунтами Token Program через CPI.
Типові помилки
CPI caller is not signer
Помилка InstructionError::InvalidAccountData або NotEnoughSigners виникає, коли цільова програма очікує підпис акаунта, але батьківська програма передала його без прав signer.
Причина: під час звичайного invoke права signer не створюються з нуля — вони лише передаються з оригінальної транзакції. Якщо користувач не підписав цей акаунт у транзакції, програма не може «надати» йому статус signer через invoke.
Рішення: якщо акаунт є PDA і програма має право його підписувати, використовуйте invoke_signed з правильними seed-ами. Якщо акаунт належить користувачу — він має підписати транзакцію.
Невірний порядок акаунтів у CPI
Кожна програма на Solana очікує акаунти у суворому порядку, визначеному її інтерфейсом. Якщо ви передасте акаунти в іншому порядку під час CPI, цільова програма інтерпретує їх неправильно: наприклад, токен-акаунт відправника буде прочитаний як токен-акаунт отримувача.
Це особливо небезпечно, тому що runtime не перевіряє семантичний зміст акаунтів — лише їхні права (writable, signer) та відповідність метаданим інструкції. Транзакція може пройти валідацію, але логіка програми спрацює некоректно.
Рішення: завжди перевіряйте порядок акаунтів у документації цільової програми (або в її IDL, якщо це Anchor-програма) і строго дотримуйтесь його при формуванні інструкції для CPI. У Anchor контексти (CpiContext) частково захищають від цієї помилки, оскільки порядок задається на рівні типів, але ручне формування інструкцій вимагає особливої уваги.
Практичні приклади формування CPI-викликів з коректним порядком акаунтів та обробкою помилок розглядаються в наступному розділі: Як викликати іншу програму через CPI.