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 固定的版本匹配:
从同一 Anchor 版本线获取两个 crate。 两个 master 分支都固定 =1.0.2,所以一个程序可以从单个 crate 中 CPI 到 CPMM 和 CLMM。混合版本线会破坏构建:例如,raydium-cp-swap 在 master 上,而 raydium-clmm 固定到其 Anchor 1.0 升级前的 rev。Cargo 必须将两个不兼容的 Anchor 特性副本链接到一个二进制文件中。如果你被困在混合对中,将程序分成两个,或为其中一侧放弃类型化 CPI crate 并手动编码该指令(为 AMM v4 显示的模式适用于任何程序)。
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-client 的 RequestBuilder::instructions() 现在返回 Vec<Instruction> 而不是 Result<Vec<Instruction>>(删除 ?),CommitmentConfig 已从 solana-sdk 移出——改为从 anchor_client 获取。spl-associated-token-account 8.0 从新的 spl-associated-token-account-interface crate 重新导出其辅助函数。get_associated_token_address 和 ID 仍可在 crate 根(spl_associated_token_account::{get_associated_token_address, ID})访问,但地址辅助函数在那里已弃用——更倾向于直接依赖 spl-associated-token-account-interface 并导入 spl_associated_token_account_interface::address::get_associated_token_address 和 spl_associated_token_account_interface::program::ID。注意 ::address 和 ::program 是 interface crate 的模块;spl_associated_token_account::address::… 无法解析。
有关端到端连接账户结构的工作 CPI 示例,请参见 raydium-io/raydium-cpi-example(涵盖 AMM v4、CPMM 和 CLMM)。其最新分支是 anchor-0.31.0——还没有 Anchor 1.x 分支,所以将该仓库视为账户结构连接的参考,而不是本页规定的版本固定。

通用 Anchor CPI 模式

本节以 CPMM 为例详细讲解端到端的流程:Accounts 结构体、CpiContext、cpi::<ix>()。CLMM 遵循完全相同的模式,只是账户列表和剩余账户的要求不同。LaunchLab 采用相同的机制,但其账户列表包含几个 CPMM/CLMM 中不存在的账户(global_config、platform_config、event_authority、program),因此应将其视为相同的模式而非相同的形状。请参考各程序自己的章节,而不要假设本演练中的账户列表可以直接转移。

账户列表构建

每个 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 时(对于 vault、escrow 等很常见),使用 CpiContext::new_with_signer:
签名者种子必须与 PDA 的推导相匹配。对于作为 authority(或类似签名者角色)传递的任何账户,Solana 运行时会检查 PDA 是否通过这些种子签署。

剩余账户

某些 Raydium 指令接受剩余账户,这是在固定账户之后附加的可变长度列表。Anchor 的 CPI 辅助函数不对剩余账户进行类型检查;通过 .with_remaining_accounts(...) 传递它们:
顺序总是重要的,因为接收程序按你传递它们的顺序迭代剩余账户。两个已确认的顺序:
  • CLMM SwapV2:tick 数组,按方向排序。
  • 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_launchpad/raydium_launchpad.json,位于公开的 raydium-idl 仓库中。该 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_in → BuyExactIn),字段名称与 IDL 的账户名称完全匹配,与下面 MyProxyBuy 中已使用的账户列表相同。 CPI 的形状遵循通用模式。下面的账户列表和参数来自链上 IDL 的 buy_exact_in 指令,而非 products/launchlab/instructions.mdx:
毕业后,目标程序是 CPMM 或 AMM v4,取决于 pool_state.migrate_type,products/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),所以你可以通过与数值进行比较来测试特定代码。完整的错误表:CPMM、CLMM、AMM v4、Farm v6 和 LaunchLab。

组合 CPI 中的计算预算

每个 CPI 帧都有开销,被调用方自身的 CU 消耗会叠加在你的基础上,所以从你的程序内部调用 Raydium 的交易需要显式的计算预算,而不是依赖 200k CU 的默认值。
实测,非估算。 主网上的 CPMM swap_base_input 在 CPMM 程序本身消耗约 23,000 CU——2026-09-09 在高交易量池上采样了八次实时交易(22,721–23,052),从 Program CPMMoo8… consumed N of M compute units 日志行读取。作为对比:AMM v4 交换约 26,000;CLMM swap 约 41,000;CLMM swap_v2 约 48,000(43,838–52,887),随着每次 tick 穿越而增加。本页的早期版本报告了代理交换 CPI 约 47,700 CU。该数字是整个交易(computeUnitsConsumed),包括调用方自身的程序、CPI 帧和任何 ATA 设置——而非被调用方的成本。两者都有用,但它们不是同一个数字,所以要进行类似的比较。根据你自己的测量来衡量你的交易,而不是根据文档中复制的数字进行预算,因为默认的 200k CU 限制会无声地耗尽,而且随着程序升级,每条指令的成本会变化。
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 路径。 crates.io 上没有 raydium_farm_v6 crate,没有公开的源代码仓库,也没有链上 IDL——该程序既没有遗留的 anchor:idl 账户,也没有在程序元数据程序中的条目(见 sdk-api/anchor-idl)。将其视为非 Anchor 程序,手动构建其指令,如下所示。
如果你无论如何都需要 Rust CPI,例如从另一个链上程序进行组合,手动构建 Instruction,方式与 AMM v4 相同:独立推导真实账户列表和指令判别器,例如通过解码 SDK 的 TypeScript 布局(raydium-sdk-V2 的 farm 模块)、直接解码真实交易(见下文),或转储和反汇编已部署的程序。 对于与收获或领取调用一致的零参数指令形状,真实账户顺序是一个固定前缀(token_program、farm 的状态账户、vault 权限 PDA、该 PDA 的第一个奖励 vault、第二个 PDA、调用方和调用方的该第一个奖励 mint 的 ATA),然后是 remaining_accounts 中的 (reward_vault_i, user_reward_ata_i) 对,用于第一个之后的每个奖励流。配对约定是真实的,但它仅从第二个奖励流开始:第一个流的 vault 和 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_config 和 create_pool_fee 账户,所以你也需要克隆这些账户,否则 initialize 会直接失败。对于 CPMM 具体来说:克隆你想要的费用等级 AmmConfig(从 GET https://api-v3.raydium.io/main/cpmm-config 获取其地址,索引 0 是 0.25% 等级)和费用接收代币账户,它由精确地址验证而非动态创建,所以必须已经存在。
你的测试刚创建的池在同一时刻不可交换。 CPMM 的 initialize 会静默覆盖不严格在未来的 open_time(if open_time <= block_timestamp { open_time = block_timestamp + 1 }),所以即使 startTime: 0(根据 SDK 表示”立即开放”)也会留下真实的 ≥1 秒间隙才能接受交换。创建池后立即交换且无延迟的测试会触发 NotApproved。在池创建和首次交换之间稍作等待(1–2 秒)就足够了。这是测试特有的;人工运行两个独立命令通常不会注意到,因为输入和进程启动已经消耗超过一秒。

参考资源

来源: