跳转到主要内容

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.

本页内容由 AI 自动翻译,所有内容以英文版本为准。查看英文版 →

CPI 何时是合适的工具

当交易需要与其他只有你的程序才能执行的链上状态变化原子性地发生时,自定义程序就很有意义。常见的案例包括:
  • 资金托管/限价单程序 — 用户向你的托管账户存入一个 mint,你的程序监视价格条件,当条件触发时,你的程序通过 Raydium 原子性地交换并将结果记入用户账户。
  • 聚合器代理 — 单个指令将交换路由到 Raydium 和一个或多个其他 DEX,所有跳跃都在你的程序拥有的单个滑点检查下。
  • 自动复合金库 — 将 LP 或农场质押存入你的金库,金库按计划收获奖励,重新提供流动性,发行份额代币。
  • 策略金库 — 通过交换 CLMM 进行再平衡的杠杆 LP 头寸;在一笔交易中平仓头寸并交换抵押品的清算人。
  • 带有自定义归属的代币启动平台 — 你的程序持有归属代币,并按计划释放到 Raydium 池中。
如果你只想从链外代码发送交换,CPI 大材小用 — 使用 SDK。CPI 只有在与你自己的状态原子性相结合时才值得复杂性。

组合模式

模式 1:瘦代理

你的程序公开一条指令,该指令验证某个策略(例如白名单 mint 对、验证用户的费用折扣),然后转发到 Raydium。
┌──────────────┐   user tx    ┌────────────────┐  CPI  ┌──────────┐
│ user         │─────────────▶│ your program   │──────▶│ Raydium  │
└──────────────┘              │  (validate)    │       │  (CPMM)  │
                              └────────────────┘       └──────────┘
状态存放在用户的 ATA 中。你的程序不拥有任何代币。信任足迹最小。

模式 2:资金托管

你的程序拥有一个持有用户输入 mint 的 PDA。触发时,PDA 签署向 Raydium 交换其自身余额的 CPI。
           deposit                   trigger
   user ───────────▶  PDA vault  ───────────────▶  Raydium swap
                     (your prog)                    (signed by PDA)


                                                    PDA vault (output mint)

                                                     withdraw ▼
                                                         user
关键细节:PDA 通过 CpiContext::new_with_signer 签署。参见签名者种子

模式 3:组合多跳

你的程序在一条指令中发出多个 CPI,在所有 CPI 中执行单个滑点界限。Raydium 交换指令各自有自己的 minimum_amount_out,但你将其设置为 0(或非常宽松的下限),在最后一跳后自己执行严格的最终最小值。
instruction:
  CPI swap: tokenA → tokenB   (raydium, loose min)
  CPI swap: tokenB → tokenC   (raydium / third-party, loose min)
  CPI swap: tokenC → tokenD   (raydium, loose min)
  require(user.tokenD_ata.amount - pre_balance >= user_min_out)
这为整条路由提供了单个回滚门。仅当你信任每一跳都是滑点安全的时才使用此模式;否则,让每一跳执行自己的最小值。

模式 4:金库/策略

你的程序在 PDA 中持有 LP 代币或农场质押。保管人(或用户)调用 compound(),该函数:
  1. 从农场收获奖励。
  2. 交换奖励以获取池代币(CPI 进入 CPMM 或 CLMM)。
  3. 将所得重新存入 LP(另一个 CPI)。
  4. 质押新的 LP(另一个 CPI)。
全部在一笔交易中进行,以便金库的 NAV 原子性地移动。计算预算通常为 600k–1M CU;地址查找表是强制的。

账户列表构造

调用程序的 Accounts 结构体镜像 Raydium 程序的账户顺序,但大多数 Raydium 侧的账户是 UncheckedAccount,因为 Raydium 本身验证它们。你只在你拥有的账户上添加约束:
use anchor_lang::prelude::*;
use anchor_spl::token::{Token, TokenAccount};

#[derive(Accounts)]
pub struct EscrowSwap<'info> {
    /// 托管 PDA;持有输入 mint 并签署 CPI。
    #[account(
        mut,
        seeds = [b"escrow", user.key().as_ref()],
        bump = escrow.bump,
    )]
    pub escrow: Account<'info, Escrow>,

    #[account(mut)]
    pub user: Signer<'info>,

    // ----- Raydium 侧账户,大多数未检查 -----

    /// CHECK: validated by CPMM
    #[account(mut)] pub pool_state: UncheckedAccount<'info>,
    /// CHECK: validated by CPMM
    pub amm_config: UncheckedAccount<'info>,
    /// CHECK: validated by CPMM
    pub pool_authority: UncheckedAccount<'info>,
    #[account(mut)] pub input_vault:  Account<'info, TokenAccount>,
    #[account(mut)] pub output_vault: Account<'info, TokenAccount>,
    /// CHECK: validated by CPMM
    #[account(mut)] pub observation_state: UncheckedAccount<'info>,

    /// 托管的输入 ATA — 由托管 PDA 拥有。
    #[account(
        mut,
        associated_token::mint = input_mint,
        associated_token::authority = escrow,
    )]
    pub escrow_input_ata: Account<'info, TokenAccount>,

    /// 托管的输出 ATA。
    #[account(
        mut,
        associated_token::mint = output_mint,
        associated_token::authority = escrow,
    )]
    pub escrow_output_ata: Account<'info, TokenAccount>,

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

    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>,
}
这种不对称性 — 在你的账户上严格验证,在 Raydium 的账户上使用 UncheckedAccount — 不是懒惰。接收方验证自己的账户;在调用方重复验证只会消耗 CU,并且当 Raydium 发货新的结构体布局字段时有失步风险。

CPI 调用本身

use raydium_cp_swap::cpi::{self, accounts::Swap as CpmmSwap};

pub fn escrow_swap(
    ctx: Context<EscrowSwap>,
    amount_in: u64,
    minimum_amount_out: u64,
) -> Result<()> {
    let user_key = ctx.accounts.user.key();
    let bump     = ctx.accounts.escrow.bump;
    let seeds: &[&[u8]] = &[b"escrow", user_key.as_ref(), &[bump]];
    let signer: &[&[&[u8]]] = &[seeds];

    let cpi_accounts = CpmmSwap {
        payer:                ctx.accounts.user.to_account_info(),
        authority:            ctx.accounts.escrow.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.escrow_input_ata.to_account_info(),
        output_token_account: ctx.accounts.escrow_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_with_signer(
        ctx.accounts.cpmm_program.to_account_info(),
        cpi_accounts,
        signer,
    );

    cpi::swap_base_input(cpi_ctx, amount_in, minimum_amount_out)?;
    Ok(())
}

PDA 签名者种子

仅当作为 authority 传递的 PDA 与调用方声称的派生相匹配时,CPI 才成功。两者必须就以下内容达成一致:
  1. 种子字节序列(这里 [b"escrow", user.key().as_ref()])。
  2. bump。
  3. 调用程序 ID(你的程序,而非 Raydium 的)。
Raydium 不关心 authority 是谁 — 它只关心 authority 签名覆盖交易,且输入 ATA 由该 authority 拥有。验证发生在 anchor_spl::token::transfer 中:ATA 的 authority 字段必须等于签名者。 常见错误:将 user 作为 authority 传递(并从由托管 PDA 拥有的 escrow_input_ata 转账)。SPL Token 程序以 owner mismatch 拒绝。始终让 authority 字段与 ATA 所有者匹配。

剩余账户

几个 Raydium 指令在固定账户之后采用可变长度的账户列表 — 剩余账户
  • CLMM SwapV2:1–8 个 TickArrayState 账户,用于交换可能穿过的 tick 数组,按交换方向。
  • Farm v6 Deposit / Harvest / Withdraw(reward_vault, user_reward_ata) 对,每个活跃奖励槽一对。
  • Token-2022 transfer-hook mints:transfer-hook 程序加上 hook 需要的任何账户。
Anchor CPI 助手不对剩余账户进行类型检查。直接传递它们:
let cpi_ctx = CpiContext::new_with_signer(program, accounts, signer)
    .with_remaining_accounts(ctx.remaining_accounts.to_vec());
顺序很重要。 对于 CLMM:
remaining = [
    tick_array_in_direction_0,    // first one crossed
    tick_array_in_direction_1,
    ...,
]
对于 farm v6 harvest:
remaining = [
    reward_vault_0, user_reward_ata_0,
    reward_vault_1, user_reward_ata_1,
    // omit any slot whose reward_state is Uninitialized
]
你的调用程序必须从客户端原样接收和传递剩余账户。不要尝试过滤或重新排序它们。

组合调用的计算预算

一个 CPI 的调用框架本身耗费约 1,500 CU;被调用者自己的 CU 使用堆积在顶部。每个 Raydium CPI 的粗略预算:
调用CU (SPL Token)CU (Token-2022)
CPMM swap_base_input~150,000~200,000
CLMM swap_v2 (单个 tick 数组)~180,000~230,000
CLMM swap_v2 (穿过 2 个 ticks)~220,000~270,000
Farm v6 deposit~120,000~150,000
Farm v6 harvest (每个奖励槽)+30,000+40,000
AMM v4 swap_base_in~140,000n/a
为每个 CPI 框架添加 ~1,500,为你自己程序的开销添加 ~20,000。执行 harvest → swap A → swap B → deposit LP → stake LP 的自动复合器轻易达到 700k CU。 总是设置显式的 ComputeBudgetProgram::set_compute_unit_limit
import { ComputeBudgetProgram } from "@solana/web3.js";

const tx = new Transaction().add(
  ComputeBudgetProgram.setComputeUnitLimit({ units: 900_000 }),
  ComputeBudgetProgram.setComputeUnitPrice({ microLamports: priorityFeeMicroLamports }),
  yourInstruction,
);
默认 200k CU 上限将在组合调用完成之前很久就悄然耗尽。

错误传播

Raydium 程序返回带有稳定错误代码的 Anchor 错误。你的调用程序将其视为 Err(ProgramError::Custom(code))。默认情况下冒泡:
cpi::swap_base_input(cpi_ctx, amount_in, minimum_amount_out)?;
或拦截特定代码:
use raydium_cp_swap::error::ErrorCode as CpmmErr;

match cpi::swap_base_input(cpi_ctx, amount_in, minimum_amount_out) {
    Ok(_) => {},
    Err(err) if is_err(err, CpmmErr::ExceededSlippage) => {
        // 你的程序可能想以更大的滑点重试,或撤销状态。
        return err!(YourErr::PoolTooVolatile);
    }
    Err(err) => return Err(err),
}
错误代码到含义的映射按 IDL 策略(sdk-api/anchor-idl)是稳定的;新代码在末尾附加,现有代码从不改变含义。

完整工作示例:限价单托管

流程:
  1. open_order — 用户向托管 PDA 存入 amount_ininput_mint;记录目标 min_amount_out 和有效期。
  2. execute_order — 任何人(保管人)以当前池账户调用。程序检查当前报价 ≥ min_amount_out,然后 CPI Raydium 交换并在托管中保留输出。
  3. claim — 用户从托管中提取输出 mint。
#[account]
pub struct LimitOrder {
    pub user:          Pubkey,
    pub input_mint:    Pubkey,
    pub output_mint:   Pubkey,
    pub amount_in:     u64,
    pub min_out:       u64,
    pub expiry_unix:   i64,
    pub state:         u8,    // 0 open, 1 filled, 2 cancelled, 3 expired
    pub bump:          u8,
}

#[program]
pub mod limit_orders {
    use super::*;

    pub fn execute_order(
        ctx: Context<ExecuteOrder>,
    ) -> Result<()> {
        let order = &ctx.accounts.order;
        require!(order.state == 0, OrderErr::NotOpen);
        require!(Clock::get()?.unix_timestamp < order.expiry_unix, OrderErr::Expired);

        let user_key = order.user;
        let seeds: &[&[u8]] = &[b"order", user_key.as_ref(), &[order.bump]];
        let signer: &[&[&[u8]]] = &[seeds];

        let pre_out_balance = ctx.accounts.escrow_output_ata.amount;

        let cpi_accounts = CpmmSwap {
            payer:                ctx.accounts.keeper.to_account_info(),
            authority:            ctx.accounts.order.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.escrow_input_ata.to_account_info(),
            output_token_account: ctx.accounts.escrow_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_with_signer(
            ctx.accounts.cpmm_program.to_account_info(),
            cpi_accounts,
            signer,
        );

        // 让托管执行最小值 — 我们信任 Raydium 的滑点,但我们也重新检查我们自己的交换后
        // 增量以防未来的变化曾经放松它。
        cpi::swap_base_input(cpi_ctx, order.amount_in, order.min_out)?;

        ctx.accounts.escrow_output_ata.reload()?;
        let delta = ctx.accounts.escrow_output_ata.amount
            .checked_sub(pre_out_balance)
            .ok_or(error!(OrderErr::AccountingError))?;
        require!(delta >= order.min_out, OrderErr::InsufficientOutput);

        let order = &mut ctx.accounts.order;
        order.state = 1;
        Ok(())
    }
}
保管人支付交易费(他们在其他地方获得保管人费用 — 未显示)。托管 PDA 签署 CPI。Raydium 侧滑点检查托管自己的增量检查都执行下限 — 双重保险。

测试

将 Raydium 程序拉入本地验证器以进行集成测试(来自 Anchor.toml):
[test.validator]
clone = [
  { address = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK" }, # CPMM
  { address = "CLMM...." },                                     # CLMM
  { address = "675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8" }, # AMM v4
  { address = "FarmqiPv5eAj3j1GMdMCMUGXqPUvmquZtMy86QH6rzhG" }, # Farm v6
]
克隆池状态账户,以便你的测试能够真正执行交换;anchor test 在启动时从主网获取它们。参见 sdk-api/rust-cpi

组合特有的陷阱

重入

Solana 没有真正的重入 — 在同一调用中,CPI 不能回调到原始程序。但你仍然可以将自己陷入逻辑重入:CPI 读取你的状态,然后你的代码再次读取它,假设 CPI 没有改变它。对于 Raydium,CPI 不接触你的状态,所以这比例如闪贷上下文中的风险要小。但如果你将 Raydium 与借贷协议组合,请注意。

账户可变性漂移

如果你的程序将账户作为 mut 传递,但 Raydium 期望它是只读的(或反之),运行时以 InvalidAccountData 拒绝调用。总是检查 IDL 中 Raydium 指令的预期可变性;anchor_cp_swap::cpi::accounts::Swap 通过其字段类型执行它。

Token-2022 程序字段

输入和输出 mints 可能在不同的代币程序下 — 一个 SPL Token,一个 Token-2022。CPI 有单独的 input_token_programoutput_token_program 字段是有原因的。始终检查每个 mint 的 owner 字段,并将正确的程序路由到每个槽中。

版本化交易

执行 2+ Raydium CPI 加上 ATA 创建的组合 tx 很少适合遗留(v0 不带 LUT)交易。使用 V0 和地址查找表;通过 raydium.getRaydiumLutAddresses() 拉取 Raydium 的公开 LUT。

指针

来源: