Перейти к основному содержанию

Documentation Index

Fetch the complete documentation index at: https://docs.raydium.io/llms.txt

Use this file to discover all available pages before exploring further.

Эта страница переведена с помощью ИИ. За эталон принимается английская версия.Открыть английскую версию →
CPI (cross-program invocation) — это механизм, благодаря которому одна программа Solana вызывает другую. Программы Raydium на Anchor поставляются с CPI wrapper крейтами, которые превращают место вызова в типизированный вызов функции — структуры аккаунтов с проверенными именами полей и cpi::<ix>() вспомогательные функции. На этой странице описан общий паттерн; специфичные для продукта примеры кода смотрите на странице code-demos каждого раздела продукта.

Зависимости Cargo

[dependencies]
anchor-lang            = "0.29"
anchor-spl             = "0.29"
raydium_cp_swap        = { git = "https://github.com/raydium-io/raydium-cp-swap", features = ["cpi"] }
raydium_amm_v3         = { git = "https://github.com/raydium-io/raydium-clmm",    features = ["cpi"] }
# AMM v4 и farm v6: нет опубликованного Anchor CPI крейта. Смотрите раздел "AMM v4 / farm v6" ниже.
Флаг функции cpi заставляет крейты компилироваться только в поверхность CPI (структуры аккаунтов + invoker’ы) вместо полной программы, так что ваш бинарный файл остаётся небольшим. Для рабочих примеров CPI, которые подключают структуры аккаунтов от начала до конца, смотрите raydium-io/raydium-cpi-example (охватывает AMM v4, CPMM и CLMM).

Построение списка аккаунтов

Каждый CPI Raydium требует структуры Accounts в вызывающей программе. Поля совпадают с порядком аккаунтов инструкции программы один в один, с валидаторами на уровне поля:
use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount, Mint};

#[derive(Accounts)]
pub struct MyProxySwap<'info> {
    #[account(mut)]
    pub user: Signer<'info>,

    /// CHECK: validated by CPMM
    #[account(mut)]
    pub pool_state: UncheckedAccount<'info>,

    /// CHECK: ditto
    pub amm_config: UncheckedAccount<'info>,

    /// CHECK: ditto
    pub pool_authority: UncheckedAccount<'info>,

    #[account(mut)]
    pub input_vault: Account<'info, TokenAccount>,
    #[account(mut)]
    pub output_vault: Account<'info, TokenAccount>,

    pub input_mint: Account<'info, Mint>,
    pub output_mint: Account<'info, Mint>,

    #[account(mut)]
    pub user_input_ata:  Account<'info, TokenAccount>,
    #[account(mut)]
    pub user_output_ata: Account<'info, TokenAccount>,

    pub cpmm_program: Program<'info, raydium_cp_swap::program::RaydiumCpSwap>,
    pub token_program: Program<'info, Token>,
    pub token_program_2022: Program<'info, anchor_spl::token_2022::Token2022>,
    /// CHECK: observation PDA
    #[account(mut)]
    pub observation_state: UncheckedAccount<'info>,
}
Большинство аккаунтов со стороны Raydium — это UncheckedAccount, потому что вызываемая программа (Raydium) отвечает за их проверку. Ваша вызывающая программа строго проверяет только аккаунты, которыми вы владеете — пользовательские ATA, собственные PDA. Комментарий /// CHECK: подавляет предупреждение Anchor о недостающих проверках.

Построение вызова CPI

Anchor генерирует по одному вспомогательному методу для каждой инструкции:
use raydium_cp_swap::cpi::{self, accounts::Swap as CpmmSwap};

pub fn my_proxy_swap(
    ctx: Context<MyProxySwap>,
    amount_in: u64,
    minimum_amount_out: u64,
) -> Result<()> {
    let cpi_accounts = CpmmSwap {
        payer:                ctx.accounts.user.to_account_info(),
        authority:            ctx.accounts.user.to_account_info(),
        amm_config:           ctx.accounts.amm_config.to_account_info(),
        pool_state:           ctx.accounts.pool_state.to_account_info(),
        input_token_account:  ctx.accounts.user_input_ata.to_account_info(),
        output_token_account: ctx.accounts.user_output_ata.to_account_info(),
        input_vault:          ctx.accounts.input_vault.to_account_info(),
        output_vault:         ctx.accounts.output_vault.to_account_info(),
        input_token_program:  ctx.accounts.token_program.to_account_info(),
        output_token_program: ctx.accounts.token_program.to_account_info(),
        input_token_mint:     ctx.accounts.input_mint.to_account_info(),
        output_token_mint:    ctx.accounts.output_mint.to_account_info(),
        observation_state:    ctx.accounts.observation_state.to_account_info(),
    };
    let cpi_ctx = CpiContext::new(
        ctx.accounts.cpmm_program.to_account_info(),
        cpi_accounts,
    );

    cpi::swap_base_input(cpi_ctx, amount_in, minimum_amount_out)?;
    Ok(())
}
cpi::swap_base_input генерируется из IDL; его список аргументов зеркально отражает список аргументов инструкции Anchor.

Сids подписантов (CPI подписанный PDA)

Когда ваша программа подписывает CPI от имени PDA (обычно для хранилищ, условных сделок и т.д.), используйте CpiContext::new_with_signer:
let bump         = ctx.accounts.my_authority_bump;
let signer_seeds: &[&[&[u8]]] = &[&[b"my_authority", &[bump]]];

let cpi_ctx = CpiContext::new_with_signer(
    ctx.accounts.cpmm_program.to_account_info(),
    cpi_accounts,
    signer_seeds,
);

cpi::swap_base_input(cpi_ctx, amount_in, minimum_amount_out)?;
Сids подписантов должны совпадать с производной PDA. Для любого аккаунта, переданного как authority (или другая роль подписанта), рантайм Solana проверяет, что PDA подписывает через эти seeds.

Оставшиеся аккаунты

Некоторые инструкции Raydium принимают оставшиеся аккаунты — список переменной длины, добавленный после фиксированных аккаунтов. Канонические примеры:
  • CLMM SwapV2: добавляет 1–8 аккаунтов TickArrayState, соответствующих tick массивам, которые может пересечь swap.
  • Farm v6 Deposit: добавляет пары (reward_vault, user_reward_ata) для каждого активного потока вознаграждений.
CPI helpers Anchor не проверяют типы оставшихся аккаунтов. Передавайте их через .with_remaining_accounts(...):
let cpi_ctx = CpiContext::new(program, accounts)
    .with_remaining_accounts(ctx.remaining_accounts.to_vec());
Порядок имеет значение: получающая программа перебирает оставшиеся аккаунты в порядке, в котором вы их передаёте. Для CLMM tick массивы должны быть упорядочены по направлению (первый массив в направлении swap идёт первым). Для farm v6 слоты вознаграждений идут в порядке индекса слота.

Распространение ошибок

Программы Raydium возвращают собственные перечисления ошибок. Anchor их оборачивает; ваша вызывающая программа видит их как Err(ProgramError::Custom(code)). Для обработки конкретных ошибок:
use raydium_cp_swap::error::ErrorCode as CpmmErr;

match cpi::swap_base_input(cpi_ctx, amount_in, minimum_amount_out) {
    Ok(_) => Ok(()),
    Err(e) => {
        msg!("CPMM swap failed: {:?}", e);
        // Пробросить дальше или конвертировать в собственный тип ошибки.
        Err(e)
    }
}
Номер кода ошибки стабилен в соответствии с политикой IDL (sdk-api/anchor-idl). Вы можете тестировать конкретные коды, сравнивая с числовым значением.

Бюджет вычислений в составных CPI

Каждый фрейм CPI имеет накладные расходы (~1,500 CU для самого вызова), и собственное потребление CU вызываемого программой складывается с вашим. Транзакция, которая вызывает CPMM swap изнутри вашей программы, расходует:
your_program_cu
+ ~1_500       (CPI overhead)
+ ~150_000     (CPMM swap, SPL-token variant)
+ ~200_000     (если Token-2022 с комиссией за перевод)
+ ~10_000      (observation update)
Для маршрутизации с несколькими уровнями (ваша программа → агрегатор → CPMM + CLMM + farm harvest), бюджет ≥500k CU. Всегда устанавливайте явную инструкцию ComputeBudgetProgram::set_compute_unit_limit(...) в транзакции — лимит по умолчанию 200k CU молча исчерпается.

AMM v4 — ручное построение Instruction

AMM v4 не имеет крейта Anchor. Постройте Instruction вручную:
use anchor_lang::solana_program::program::invoke_signed;
use anchor_lang::solana_program::instruction::{Instruction, AccountMeta};

const AMM_V4_PROGRAM_ID: Pubkey = pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8");

// Дискриминатор SwapBaseIn — 9.
let mut data = vec![9u8];
data.extend_from_slice(&amount_in.to_le_bytes());
data.extend_from_slice(&minimum_amount_out.to_le_bytes());

let ix = Instruction {
    program_id: AMM_V4_PROGRAM_ID,
    accounts: vec![
        AccountMeta::new_readonly(token_program_id, false),
        AccountMeta::new(amm_id, false),
        AccountMeta::new_readonly(amm_authority, false),
        // ... оставшиеся аккаунты согласно products/amm-v4/instructions ...
    ],
    data,
};
invoke_signed(&ix, &account_infos, signer_seeds)?;
Полный список аккаунтов смотрите в products/amm-v4/code-demos.

Farm v6 — оставшиеся аккаунты пар вознаграждений

Farm v6’s Deposit / Withdraw / Harvest использует паттерн пары (reward_vault_i, user_reward_ata_i) в оставшихся аккаунтах. Точная последовательность:
remaining_accounts = [
    reward_vault_0, user_reward_ata_0,
    reward_vault_1, user_reward_ata_1,
    ...
]
Одна пара на активный (работающий или закончившийся, но неполученный) слот вознаграждений. Пропускайте неиспользуемые слоты; программа диспетчеризует по farm_state.reward_infos[i].reward_state.

Тестирование потока CPI

Локальная разработка требует наличия программ Raydium в вашем тестовом валидаторе. Варианты:
  1. anchor test с клонированием программы — в Anchor.toml:
    [test.validator]
    clone = [
      { address = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK" },  # CPMM
      { address = "CLMM...program-id..." },                          # CLMM
      { address = "farm-v6-program-id..." },                         # farm v6
    ]
    
    Это подтягивает развёрнутый байткод из мейннета в ваш локальный валидатор.
  2. Devnet — Raydium развёртывает все программы на devnet с теми же program ID, что и мейннет. Запустите anchor test --provider.cluster devnet для обращения к живому коду.
  3. Локальное развёртывание — клонируйте репозитории Raydium и выполните anchor deploy на локальный валидатор. Добавляет накладные расходы цикла тестирования, но позволяет изменить вызываемую программу для отладки.

Ссылки

Источники: