Skip to main content
本页内容由 AI 自动翻译,所有内容以英文版本为准。查看英文版 →
CPI(“跨程序调用”)是一个 Solana 程序调用另一个程序的机制。Raydium 的大多数程序都提供 Anchor CPI 包装 crate,使调用点看起来像一个类型化的函数调用,其中账户结构具有验证的字段名和 cpi::<ix>() 辅助函数。本页面介绍通用模式一次,然后说明各程序的差异。有关可运行的 TypeScript 代码,请参阅每个产品章节的 code-demos 页面。

哪个模式适用于哪个程序

如果你正在集成 CPMM、CLMM 或 LaunchLab,请先阅读通用模式,然后跳转到你的程序部分了解账户列表和任何差异。Farm v6 和 AMM v4 的差异足够大,值得单独阅读它们的部分。

Cargo 依赖

依赖键必须与目标仓库的 [package] name 完全匹配,包括连字符。 在解析 git 依赖时,Cargo 不会将 raydium_cp_swap 视为等同于 raydium-cp-swap
branch = "master" 跟踪最新发布的源代码;如果需要可重现的构建,请固定到特定的 rev = "<commit>"。一旦你完成原型设计,这是推荐的做法,因为上游 master 上的账户布局更改会在没有警告的情况下破坏你的构建。 cpi 功能标志使 crate 仅编译为 CPI 表面(账户结构 + 调用器),而不是完整程序,因此你的二进制文件保持较小。 anchor-lang / anchor-spl 必须与目标 crate 固定的版本匹配,截至 2026-09,两个公开的 Raydium crate 不一致
你现在不能从一个程序同时依赖两个 crate。 每个都用 = 固定 Anchor,所以 Cargo 必须将两个不兼容的 Anchor 特性副本链接到一个二进制文件中,构建失败。如果你的程序同时 CPI 到 CPMM 和 CLMM,你必须要么将其拆分为两个程序,要么为其中一个放弃类型化 CPI crate 并手动编码该指令(AMM v4 显示的模式适用于任何程序)。在开始之前重新检查两个 Cargo.toml 文件 — 当 CLMM 迁移到 Anchor 1.x 时,这应该会解决。
Anchor 1.0 改变了每个 CPI 调用点接触的两件事。 如果你正在将工作集成从 0.3x 迁移:
  • CpiContext::new 接受 Pubkey,而不是 AccountInfo CpiContext::new(ctx.accounts.cpmm_program.to_account_info(), accts) 变为 CpiContext::new(*ctx.accounts.cpmm_program.key, accts)new_with_signer 也是如此。结构字段现在是 program_id: Pubkey
  • Context 有一个生命周期,而不是四个。 Context<'_, '_, 'info, 'info, MyProxySwap<'info>> 变为 Context<'info, MyProxySwap<'info>>
在客户端,anchor-clientRequestBuilder::instructions() 现在返回 Vec<Instruction> 而不是 Result<Vec<Instruction>>(删除 ?),CommitmentConfig 已从 solana-sdk 移出 — 改为从 anchor_client 获取。spl-associated-token-account 8.0 也移动了其辅助函数:get_associated_token_address 现在在 ::address 下,程序 ID 是 ::program::ID
有关端到端连接账户结构的可运行 CPI 示例,请参阅 raydium-io/raydium-cpi-example(涵盖 AMM v4、CPMM 和 CLMM)。

通用 Anchor CPI 模式

本部分以 CPMM 为完整示例逐步介绍:Accounts 结构、CpiContextcpi::<ix>()。CLMM 遵循相同的形状,但账户列表不同,并且需要剩余账户。LaunchLab 遵循相同的机制,但其账户列表包含 CPMM/CLMM 没有的几个账户(global_configplatform_configevent_authorityprogram),因此将其视为相同的模式,而不是相同的形状。参见每个程序自己的部分,而不是假设本演练的账户列表直接转移。

账户列表构造

每个 Raydium CPI 都需要在调用程序中有一个 Accounts 结构。其字段是你的指令需要的任何账户,具有字段级验证器;它们的声明顺序不必与 Raydium 自己的指令账户顺序匹配,因为你自己的 IDL 生成的客户端按名称而不是位置寻址它们:
大多数 Raydium 端的账户是 UncheckedAccount,因为被调用方(Raydium)拥有验证。你的调用程序只严格验证你拥有的账户,例如用户 ATA 和你自己的 PDA。/// CHECK: 文档注释抑制 Anchor 关于缺少检查的警告。一个 Raydium 端的例外是 cpmm_program 本身:它是被调用的程序而不是 Raydium 内部验证的数据账户,所以它被类型化为 Program<T> 并获得 Anchor 的自动地址检查,而不是手动 /// CHECK:。这种大多数 UncheckedAccount 的形状,其中 Raydium 验证自己的账户,对 CLMM 和 LaunchLab 也是相同的。此示例假设两个 mint 都是经典 SPL Token;如果任一方可以是 Token-2022 mint,添加 token_program_2022: Program<'info, anchor_spl::token_2022::Token2022> 字段,并在下面的 CPI 调用中将其作为该方的 input_token_program/output_token_program 传递,而不是 token_program

构建 CPI 调用

Anchor 为每个指令生成一个辅助函数,以及一个 CPI 账户结构(cpi::accounts::Swap,下面别名为 CpmmSwap)。与上面你自己的 MyProxySwap 结构不同,这个的字段名和顺序由 raydium-cp-swap 自己的 IDL 固定,必须完全匹配:
cpi::swap_base_input 由 IDL 生成;其参数列表镜像 Anchor 指令的参数列表。每个已确认的基于 Anchor 的 Raydium 程序(CPMM、CLMM、LaunchLab)以相同的方式生成其 cpi::<ix>() 辅助函数,函数名与指令名匹配(蛇形命名法)。这是否扩展到 Farm v6 未确认;参见其部分。

签名者种子(PDA 签名的 CPI)

当你的程序代表 PDA 签署 CPI 时(对于保险库、托管等很常见),使用 CpiContext::new_with_signer
签名者种子必须与 PDA 的推导相匹配。对于作为 authority(或类似签名者角色)传递的任何账户,Solana 运行时检查 PDA 通过这些种子签署。

剩余账户

某些 Raydium 指令接受剩余账户,这是在固定账户之后附加的可变长度列表。Anchor 的 CPI 辅助函数不对剩余账户进行类型检查;通过 .with_remaining_accounts(...) 传递它们:
顺序总是重要的,因为接收程序按你传递它们的顺序迭代剩余账户。两个已确认的顺序:
  • CLMM SwapV2tick 数组,按方向排序。
  • Farm v6(reward_vault, user_reward_ata) 对,但仅从第二个奖励流开始;参见 Farm v6 了解解码真实交易显示的内容。

应用模式:CLMM

SwapV2 遵循通用模式,但账户列表不同,并且需要 tick 数组的剩余账户。crate 的 #[program] 模块名为 raydium_clmm,这也是其 Rust use 路径。
CPI 账户结构名为 SwapSingleV2,而不是 SwapV2 SwapV2 是链上指令名称。
以与 SDK 相同的方式计算 tick 数组列表,通过对当前池状态的报价,而不是猜测固定计数;超出你传递的数组的交换会以 TickArrayNotFound 回退(参见 products/clmm/instructions 了解完整账户表和错误列表)。按价格遍历方向传递它们:交换方向的第一个数组首先。

应用模式:LaunchLab

LaunchLab 基于 Anchor 并发布了 IDL:公开 raydium-idl 仓库中的 raydium_launchpad/raydium_launchpad.json。该 IDL 的内部元数据标识符是 raydium_launchpad,这是底层程序的技术名称,而不是产品的替代名称。与 CPMM 和 CLMM 不同,程序自己的源代码不公开(参见 reference/program-addresses)。没有 git = "..." 依赖可指向 Cargo,也没有源代码来确认真实 crate 的 Rust use 路径会是什么。 使用 Anchor 的 declare_program! 宏从已发布的 IDL 生成绑定。将 IDL JSON 保存为 crate 中的 idls/raydium_launchpad.json(Cargo 在 CARGO_MANIFEST_DIR 相对位置查找 idls/ 目录),然后 declare_program!(raydium_launchpad); 直接从 IDL 生成 raydium_launchpad::cpi::accounts::<Ix> 结构和 cpi::<ix>() 函数,无需程序源。生成的账户结构名称始终是指令名称的 PascalCase(buy_exact_inBuyExactIn),字段名与 IDL 的账户名完全匹配,与下面 MyProxyBuy 中已使用的相同账户列表。 CPI 形状遵循通用模式。下面的账户列表和参数来自链上 IDL 的 buy_exact_in 指令,而不是 products/launchlab/instructions.mdx
毕业后,目标程序是 CPMM 或 AMM v4,取决于 pool_state.migrate_typeproducts/launchlab/accounts.mdx 说这是在 Initialize 时设置的。你的 CPI 账户列表必须为任一方做好准备,或者你需要先从 PoolState 读取 migrate_type 并分支。

错误传播

每个基于 Anchor 的 Raydium 程序都返回自己的错误枚举;Anchor 包装它们,所以你的调用程序将它们视为 Err(ProgramError::Custom(code))。要处理特定错误:
为你调用的程序交换相关的错误类型(CLMM 为 raydium_clmm::error::ErrorCode,等等)。错误代码数字根据 IDL 策略是稳定的(sdk-api/anchor-idl),所以你可以通过与数值进行比较来测试特定代码。完整错误表:CPMMCLMMAMM v4、Farm v6 和 LaunchLab

组合 CPI 中的计算预算

每个 CPI 帧都有开销,被调用方自己的 CU 消耗堆叠在你的之上,所以从你的程序内部调用 Raydium 的交易需要显式计算预算,而不是依赖 200k CU 默认值。
单个测量数据点,而不是基准。 CPMM 交换 CPI 的常见经验法则估计(大约 1,500 CU CPI 开销 + 150,000 CU 用于交换本身 + 10,000 CU 用于观察更新,总计约 161,500 CU)大幅高估了实际使用。真实交换 CPI(my_proxy_swap 调用 swap_base_input、SPL token mint、新创建的两 token 池)消耗了约 47,700 CU 总计,从 connection.getTransaction(...).meta.computeUnitsConsumed 读取,大约是该估计的三分之一。将此视为来自一个池形状和一个 mint 配置的一个数据点,而不是规范。测量你自己的交易,而不是根据任一数字进行预算。
CLMM 和 LaunchLab CPI 成本更高(CLMM 特别是通过 remaining_accounts 遍历额外的 tick 数组,每个数组添加 CU),但只有上面的 CPMM 数字是测量值。始终设置从你自己的测量大小的显式 ComputeBudgetProgram::set_compute_unit_limit(...) 指令,而不是从文档复制的数字,因为默认 200k CU 限制会静默耗尽,每指令成本随着程序升级而变化。

AMM v4:手动指令构造

AMM v4 早于 Anchor,没有 CPI crate,使其成为本文档中不遵循上述通用模式的唯一程序。手动构建 Instruction
参见 products/amm-v4/code-demos 了解完整账户列表。

Farm v6

如果这是你的集成的一个选项,请使用 TS SDK。 raydium.farm.deposit(...) (参见 products/farm-staking/code-demos)由真实演示执行,不依赖是否存在此程序的 Rust Anchor crate。
Farm v6 的 Anchor CPI 状态未确认,可用证据与之矛盾。 Farm v6 不存在公开 IDL 或源代码,与 products/farm-staking/code-demos.mdx 声称存在 Anchor CPI crate(raydium_farm_v6)和 Deposit 账户结构相矛盾。
如果你仍然需要 Rust CPI,例如从另一个链上程序组合,手动构建 Instruction,与 AMM v4 相同的方式:独立推导真实账户列表和指令判别器,例如通过解码 SDK 的 TypeScript 布局(raydium-sdk-V2 的 farm 模块)、直接解码真实交易(参见下面)或转储和反汇编已部署的程序。 对于与收获或声明调用一致的零参数指令形状,真实账户顺序是固定前缀(token_program、farm 的状态账户、保险库授权 PDA、该 PDA 的第一个奖励保险库、第二个 PDA、调用者和调用者对该第一个奖励 mint 的 ATA),然后是 (reward_vault_i, user_reward_ata_i) 对在 remaining_accounts 中,用于第一个之后的每个奖励流。配对约定是真实的,但它仅从第二个奖励流开始:第一个流的保险库和 ATA 是固定账户,彼此不相邻,根本不是 remaining_accounts 的一部分。

测试 CPI 流

本地开发需要 Raydium 程序在你的测试验证器中可用。三个选项:
  1. anchor test 与程序克隆。 将已部署的主网字节码拉入你的本地验证器;参见下面的将程序克隆到本地验证器了解 Anchor.toml 配置和两个特别困扰池创建测试的事项。
  2. Devnet。 Raydium 将大多数程序部署到 devnet,但每个程序的程序 ID 与主网不同(CPMM、CLMM、AMM v4、Stable AMM 和 LaunchLab 各有不同的 devnet 地址;参见 reference/program-addresses 中的 Devnet 表)。Farm v3/v5/v6 在 devnet 上不可靠发布;实时 API(https://api-v3-devnet.raydium.io/main/info)有当前情况。如果你使用 raydium_clmm 的捆绑 DEVNET_PROGRAM_ID 常量(或其他 crate 的等效项),不要假设主网 ID 也适用于 devnet。运行 anchor test --provider.cluster devnet 以在有正确地址后命中实时代码。
  3. 本地部署。 克隆 Raydium 仓库(CPMM、CLMM;LaunchLab 的源对此选项不可用)并 anchor deploy 到本地验证器。增加测试周期开销,但让你修改被调用方以进行调试。
运行 anchor test,或如果你在迭代测试文件而不更改程序,先 anchor build 然后 anchor test --skip-build

将程序克隆到本地验证器

这通过程序 ID 工作,无论程序的源是否公开,所以 LaunchLab 以与 CPMM 和 CLMM 相同的方式克隆,即使其源不可用。reference/program-addresses 是这里每个地址的真实来源。
如果你的测试也创建一个池(而不是针对已存在的池进行交换),仅克隆程序是不够的。 CPMM 的 initialize 指令根据真实链上数据验证其 amm_configcreate_pool_fee 账户,所以你也需要克隆那些,否则 initialize 直接失败。对于 CPMM 特别是:克隆你想要的费用等级 AmmConfig(从 GET https://api-v3.raydium.io/main/cpmm-config 获取其地址,索引 0 是 0.25% 等级)和费用接收者 token 账户,由精确地址验证,而不是动态创建,所以它必须已经存在。
你的测试刚创建的池在同一时刻不可交换。 CPMM 的 initialize 静默覆盖不严格在未来的请求 open_timeif open_time <= block_timestamp { open_time = block_timestamp + 1 }),所以即使 startTime: 0(“立即开放”,根据 SDK)也会在池接受交换之前留下真实的 ≥1 秒间隙。创建池并立即针对它交换的测试将命中 NotApproved。池创建和第一次交换之间的短 await(1–2s)就足够了。这特定于测试;人类运行两个单独的手动命令通常不会注意到,因为输入和进程启动已经消耗超过一秒。

指针

来源: