Версії для прикладів у «Як викликати іншу програму через CPI» перевірено 2 серпня 2026 року. Стабільна гілка Anchor v1 має релізи 1.0.x і орієнтується на Solana 3.x; Anchor v2 у документації позначений як alpha. Приклади для Anchor 0.29–0.32 залишаються лише відтворюваними прикладами для зафіксованого legacy-середовища: їх не слід переносити в новий проєкт без міграції залежностей і повторного тестування.

CPI (Cross-Program Invocation) у Solana — це механізм, який дозволяє одній програмі делегувати виконання іншій програмі в межах єдиної транзакції. Цей матеріал показує, як саме сформувати виклик, передати акаунти, підписати транзакцію від імені PDA та обробити результат. Всі приклади розраховані на середовище Devnet, Anchor 0.30.x та Solana CLI 1.18.x.

Підготовка акаунтів для CPI

Які акаунти потрібні цільовій програмі

Перш ніж формувати виклик, потрібно точно знати, які акаунти очікує цільова програма. Ця інформація міститься в їїIDL або документації. Кожен акаунт у виклику має бути переданий у правильному порядку, з правильними прапорцями writable та signer.

Типовий набір акаунтів для CPI включає:

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

Практичне правило: відкрийтеIDL цільової програми, знайдіть потрібну інструкцію та скопіюйте порядок акаунтів звідти. Будь-яка відмінність у порядку чи прапорцях призведе до помилки виконання.

Додавання CpiContext у Anchor

У Anchor обгортка CpiContext звʼязує акаунти з цільовою програмою. Вона автоматично виставляє прапорці writable і signer на основі типів акаунтів у вашій структурі контексту.

Базовий шаблон створення CpiContext:

CpiContext::new(cpi_program, cpi_accounts)

де cpi_program — це AccountInfo цільової програми, а cpi_accounts — структура, що імітує акаунти цільової інструкції. Якщо PDA має виступити підписувачем, замість new використовується CpiContext::new_with_signer — про це детально в розділі про invoke_signed.

CPI через Anchor

Виклик системної програми (System Program)

Найпростіший приклад CPI — переказ lamports через системну програму. Це корисно, коли ваша програма стягує комісію або повертає залишок.

Структура контексту для вашої інструкції:

#[derive(Accounts)]
pub struct TransferLamports<'info> {
  #[account(mut)]
  pub from: Signer<'info>,
  #[account(mut)]
  pub to: SystemAccount<'info>,
  pub system_program: Program<'info, System>,
}

Сам виклик:

use anchor_lang::system_program::{transfer, Transfer};

pub fn send_lamports(ctx: Context<TransferLamports>, amount: u64) -> Result<()> {
  let cpi_accounts = Transfer {
    from: ctx.accounts.from.to_account_info(),
    to: ctx.accounts.to.to_account_info(),
  };
  let cpi_program = ctx.accounts.system_program.to_account_info();
  transfer(CpiContext::new(cpi_program, cpi_accounts), amount)
}

Очікуваний результат: після виклику баланс from зменшиться на amount, а баланс to відповідно збільшиться. Перевірити можна через solana account на Devnet після виконання транзакції.

Виклик Token Program

Для роботи зі SPL-токенами ваша програма викликає Token Program через CPI. Нижче наведено концептуальний приклад переказу токенів від авторизованого користувача.

Структура контексту:

use anchor_spl::token::{Token, TokenAccount, Transfer as SplTransfer};

#[derive(Accounts)]
pub struct TransferTokens<'info> {
  pub authority: Signer<'info>,
  #[account(mut)]
  pub from: Account<'info, TokenAccount>,
  #[account(mut)]
  pub to: Account<'info, TokenAccount>,
  pub token_program: Program<'info, Token>,
}

Виклик:

pub fn send_tokens(ctx: Context<TransferTokens>, amount: u64) -> Result<()> {
  let cpi_accounts = SplTransfer {
    from: ctx.accounts.from.to_account_info(),
    to: ctx.accounts.to.to_account_info(),
    authority: ctx.accounts.authority.to_account_info(),
  };
  let cpi_program = ctx.accounts.token_program.to_account_info();
  token::transfer(CpiContext::new(cpi_program, cpi_accounts), amount)
}

У цьому прикладі authority — це звичайний підписувач (externally owned account). Ситуація змінюється, коли авторизованою стороною є PDA — це розглядається в наступному розділі.

invoke_signed

Коли потрібен invoke_signed (PDA як signer)

Функція invoke_signed з модуля solana_program::program потрібна тоді, коли вашу програму має підписати PDA. PDA не має приватного ключа, тому підпис формується криптографічно на основі насінин (seeds), з яких цей PDA було поховано. Runtime Solana перевіряє, що насінини відповідають адресі підписувача.

Типові сценарії, де це необхідно:

  • Ваша програма володіє токен-акаунтом і має переказати токени з нього.
  • Ваша програма закриває PDA-акаунт і повертає rent.
  • Ваша програма викликає іншу програму, яка вимагає підпису вашого PDA.

У Anchor для цього є зручна обгортка CpiContext::new_with_signer, яка під капотом викликає саме invoke_signed.

Формування signer_seeds

Насінини мають точно збігатися з тими, що використовувалися при створенні PDA. Будь-яка відмінність — і підпис не буде підтверджено.

Приклад у Anchor з PDA як авторизованою стороною токен-акаунта:

#[derive(Accounts)]
pub struct TransferFromPda<'info> {
  #[account(
    seeds = [b"vault", authority.key().as_ref()],
    bump
  )]
  pub vault: Account<'info, TokenAccount>,
  pub authority: Signer<'info>,
  #[account(mut)]
  pub to: Account<'info, TokenAccount>,
  pub token_program: Program<'info, Token>,
}

pub fn transfer_from_vault(ctx: Context<TransferFromPda>, amount: u64) -> Result<()> {
  let cpi_accounts = SplTransfer {
    from: ctx.accounts.vault.to_account_info(),
    to: ctx.accounts.to.to_account_info(),
    authority: ctx.accounts.vault.to_account_info(),
  };
  let cpi_program = ctx.accounts.token_program.to_account_info();
  let seeds = &[
    b"vault".as_ref(),
    ctx.accounts.authority.key().as_ref(),
    &[ctx.bumps.vault],
  ];
  let signer_seeds = &[&seeds[..]];
  token::transfer(
    CpiContext::new_with_signer(cpi_program, cpi_accounts, signer_seeds),
    amount,
  )
}

Ключовий момент: ctx.bumps.vault містить bump, який Anchor обчислив під час валідації акаунтів. Він має бути останнім елементом насінин. Якщо ви працюєте без Anchor і викликаєте invoke_signed безпосередньо, bump потрібно обчислити самостійно через Pubkey::find_program_address.

Приклад без Anchor:

use solana_program::program::invoke_signed;
use solana_program::instruction::{AccountMeta, Instruction};

let (vault_pda, bump) = Pubkey::find_program_address(
  &[b"vault", authority.key().as_ref()],
  program_id,
);

let signer_seeds = &[
  b"vault".as_ref(),
  authority.key().as_ref(),
  &[bump],
];

invoke_signed(
  &instruction,
  &account_infos_slice,
  &[signer_seeds],
)?;

Обробка результату CPI

Перевірка помилок цільової програми

У Solana транзакція є атомарною: якщо будь-яка інструкція всередині нею завершується помилкою, вся транзакція відкочується. Це означає, що помилка CPI автоматично скасовує всі зміни, зроблені вашою програмою раніше в цій транзакції.

В Anchor помилка CPI повертається як ErrorCode з простору імен цільової програми. Наприклад, якщо Token Program отримає недостатньо токенів для переказу, ви побачите помилку типу InsufficientFunds. У логах транзакції (через solana confirm -v на Devnet) буде вказано як саму помилку, так і програму, яка її згенерувала.

Якщо вам потрібно перехопити помилку CPI і обробити її специфічним чином (наприклад, повернути власне повідомлення), у Anchor це робиться через оператор ? з подальшим map_err або через явний match. Проте варто розуміти: ви не можете «продовжити виконання» після невдалого CPI в межах тієї ж інструкції — ви можете лише змінити текст помилки, що повертається клієнту.

Retry-логіка при невдачі

Важливо розуміти: retry-логіка не працює всередині виконання програми на Solana. Програма не може «повторити спробу» CPI, бо кожна інструкція виконується детерміновано і без можливості зворотного звʼязку з мережею під час виконання.

Retry-логіка реалізується виключно на стороні клієнта:

  1. Клієнт відправляє транзакцію.
  2. Отримує помилку (наприклад, TransactionError::InstructionError).
  3. Аналізує, чи є це помилкою, яку можна вирішити повторною спробою (наприклад, тимчасова нестача обчислювальних одиниць або блокування акаунта).
  4. За необхідності формує нову транзакцію і відправляє знову.

У production-рішеннях клієнтська retry-логіка зазвичай включає експоненційну затримку та обмеження кількості спроб. Проте це належить до теми клієнтської архітектури, а не до логіки самої програми.

Типові помилки

Missing account для CPI

Помилка InstructionError::NotEnoughAccountKeys виникає, коли у списку акаунтів, переданих вашій інструкції, бракує акаунта, який потрібен цільовій програмі. Це одна з найчастіших помилок при роботі з CPI.

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

Як діагностувати: порівняйте список акаунтів у вашій структурі Accounts з акаунтами, які вимагає цільова інструкція за її IDL. Перевірте, що клієнт передає всі акаунти, включно з програмами (system_program, token_program тощо).

Невірний program_id у CPI

Якщо у CpiContext::new ви передали неправильний program_id, транзакція завершиться з помилкою InstructionError::IncorrectProgramId. Runtime перевіряє, що акаунт, вказаний як програма, дійсно є скомпільованою програмою (executable) і що його адреса збігається з тією, що вказана в інструкції.

Типові причини:

  • Використано адресу програми з Mainnet під час роботи на Devnet (або навпаки).
  • У структурі контексту тип акаунта вказано як Program<'info, System>, але клієнт передав інший акаунт замість системної програми.
  • При прямому виклику invoke або invoke_signed передано хибний program_id у структурі Instruction.

Як уникнути: завжди використовуйте типізовані посилання на програми в Anchor (Program<'info, System>, Program<'info, Token>) замість ручного передавання program_id. Anchor автоматично звіряє адресу. Якщо працюєте без Anchor — жорстко задавайте program_id як константу і перевіряйте її відповідність кластеру.

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

Джерела