> For the complete documentation index, see [llms.txt](https://docs.rome.builders/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.rome.builders/zh/kai-fa-zhe-zhi-nan/integrate-solana-program.md).

# 集成一个 Solana 程序

将 Solana 程序与 Rome EVM 集成——Solana 签名者是谁、价值如何传入你的程序并返回、单轨规则、原子式与迭代式，以及账户配置。

本指南面向希望让 Rome EVM 用户使用其程序的 Solana 程序团队——例如隐私、借贷或金库程序，且应为 EVM 账户持有价值。本指南建立在 [从 EVM 调用 Solana](/zh/kai-fa-zhe-zhi-nan/call-solana-from-evm.md) （预编译机制）之上，并回答当另一侧是真实程序时会出现的问题： *谁* 签名， *如何* 代币进出，以及 *哪些* 限制决定设计。

整个流程如下：一个 EVM 账户将 USDC 转入你的程序（“shield” 或 “deposit”），你的程序为该用户持有它，之后再把它释放回去（“unshield” 或 “withdraw”）。你可以用自己的动词替换这些说法。

## 1. 每个 EVM 地址都有一个 Solana 身份

Rome 为每个 EVM 地址——无论是用户还是合约——派生一个 Solana 账户，称为其 **外部权限**:

```
external_auth(addr) = find_program_address(["EXTERNAL_AUTHORITY", addr_h160], rome_evm_program_id)
```

它拥有该地址的关联代币账户（即 `wUSDC.balanceOf(addr)` 读取的数据来源），并且它是代表该地址签署 CPI 的密钥。你的程序看到的是一个 32 字节的 Solana 公钥；它永远不会看到 H160。

**谁来签名，由谁调用预编译来决定：**

| 预编译由以下主体调用：                 | Solana 签名者                                                                                                                                                                                                                                                                                                                                                                            |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 一个 **合约**，使用普通 `CALL`       | 上面的 **合约自身的** 外部权限（`external_auth(contract)`），以及它请求的该合约任何带盐的子 PDA                                                                                                                                                                                                                                                                                                                     |
| 一个 **用户**，在一笔直接发送到预编译地址的交易中 | 上面的 **用户自身的** 外部权限                                                                                                                                                                                                                                                                                                                                                                    |
| 通过以下方式的合约： `DELEGATECALL`   | 裸权限会被拒绝（`DelegatecallOwnerAuthority`). `invoke` 没有签名者，并且 `invoke_signed` 带盐的子 PDA 仍然可以运行——但这些子 PDA 以正在运行的合约地址命名空间区分（`["EXTERNAL_AUTHORITY", caller, running_contract, salt]`），因此 `pda_with_salt` 不会派生它们。对于 HelperProgram，只有创建和资金注入选择器可以通过（`create_ata*`, `create_pda*`, `swap_gas_to_lamports`, `init_spl_mint`, `create_and_init_mint`）；它们之所以被豁免，是因为它们的目标绑定到调用者或由操作者出资，而不是因为它们根本不签名。 |

其结果就是你已经熟悉的 ERC-20 规则： **合约不能自行移动用户的资产。** 它作为自身身份行事，用户先授予它权限。对于必须在 Solana 侧移动的代币，这种授权是用户代币账户上的 SPL 委托；包装器的 ERC-20 `approve` 只是 EVM 侧的额度，包装器自身的转账会遵守它。Rome 自己的代币包装器就是这样工作的，你的适配器也会这样工作（第 3 节）。

带盐的子 PDA： `invoke_signed` 接受一个 `bytes32` 盐值列表，且 Rome 使用以下方式签名： `["EXTERNAL_AUTHORITY", caller_h160, salt]` 对应每个（会附加 bump；在直接调用时，调用方就是该合约）。 `HelperProgram.pda_with_salt(addr, salt)` 在 Solidity 中返回该地址。当你的程序希望每个仓位都有一个独立的所有者账户时，为每个用户使用一个盐值（例如用户地址）。

资金注入：拥有 ATA 不一定需要先存在一个外部权限 PDA（`create_ata` 是幂等的，而且交易的 Solana 手续费支付者会承担租金）。只有当它为你的指令创建的账户提供资金时，它才必须存在并持有 lamports。 `HelperProgram.create_pda(addr)` 会将其作为普通 System `CreateAccount`创建，所以只调用一次；它不是幂等的。 `swap_gas_to_lamports(n)` 会为该地址的 PDA 记入 `n` 个 lamports，资金来自交易的 Solana 手续费支付者，调用方则以 Rome gas 支付这些费用：任何 legacy 轨道 CPI 对手续费支付者余额的影响，都会计入交易费用，并按 gas 价格向用户收费，创建账户时产生的租金也是同样如此。Solana lane 的准备步骤见 [从 Solana 调用 EVM](/zh/kai-fa-zhe-zhi-nan/call-evm-from-solana.md)；合约会在其构造函数或首次调用时准备自己的 PDA。

## 2. 持有用户仓位的三种方式

**A. 适配器合约（推荐用于可复用适配器）。** 一个 Solidity 合约作为你的程序的前端。用户先将其账户上的 SPL 委托授予适配器的权限；适配器把代币拉入自己拥有的代币账户，然后以自身身份调用你的程序，并通过按用户划分的子 PDA 或它在指令数据中传入的标识符，为你的程序内部的用户仓位建立索引。返程时，它会让你的程序把资金释放到用户自己的代币账户。对于你的程序来说，需要知道的唯一 Solana 身份就是适配器。这里的授权是 SPL 委托，而不是 ERC-20 `approve`；第 3 节会解释原因以及这对用户意味着什么。

**B. 用户作为 Solana 签名者。** 前端从用户的 EVM 账户发送一笔交易 **直接发送到 CpiProgram 预编译** (`to = 0xff…08`，calldata = ABI 编码的 `invoke` 或 `invoke_signed`）因此用户自身的外部权限会签名，你的程序据此为该仓位建立索引。没有适配器合约，也没有委托，但每个动作在钱包中都是一笔不透明的 calldata 交易，而流程所需的 CPI 之外的任何内容（ATA、先前的转账）都需要单独的交易。它适用于一次性授权（用户的 `approve_spl` 对包装器的授权）以及简单程序，不适合多步骤流程。 [Cardo](https://github.com/rome-protocol/cardo) 采用这种形态：它的前端会把 `invoke` 直接从用户钱包发送到预编译。

**C. 无托管路由器。** 一种从不持有用户代币的合约。用户将其自己代币账户上的 SPL 委托授予路由器的权限；路由器以该权限作为转移权限对你的程序发起 CPI，因此代币会从用户的代币账户移动到你程序的账户（或再返回用户账户），而路由器在两次调用之间不持有任何资产。路由器从 `msg.sender`派生每一个用户侧账户，因此一个用户的授权不能被指向另一个用户的账户；它调用的是一个固定程序和固定的指令形状，而不是作为通用 `invoke` 透传。 `RomeDexRouter` 从 [rome-dex](https://github.com/rome-protocol/rome-dex) 就是这样写的；rome-dex 的 EVM 侧也提供了 B 形态的原始直接 `invoke` 它需要与 A 相同的授权，承担相同的一次 delegate 成本（第 3 节），且不需要拉取步骤。

本指南其余部分以 A 为准；C 使用相同的授权和相同的签名规则。

## 3. 代币进，代币出

Rome 上的 USDC 从两个侧面看其实是同一个 SPL mint： `wUSDC` EVM 侧的 ERC-20 包装器，以及 Solana 侧外部权限的 ATA。对于用户自己的地址，这两者是同一个余额： `wUSDC.balanceOf(user)` 读取的是用户的 ATA。中间没有桥接或包装步骤。

**合约的包装器余额并不是合约可控的代币账户。** 工厂会为普通 mint 部署 `SPL_ERC20_cached` ，Hadrian 的 `wUSDC` 就是其中之一。该包装器无法代表合约签名，因此当 `wUSDC.transferFrom(user, adapter, amount)` 或 `transfer` 将代币发送到合约时，SPL 代币会进入 **包装器自身的托管代币账户** (`escrow_ata()`，而包装器会在其存储中按合约记录该金额。 `wUSDC.balanceOf(adapter)` 返回的是那条记录——一份托管账本，而不是由 `external_auth(adapter)`拥有的代币账户余额。你的适配器无法在 CPI 中把这些代币交给你的程序；它们只能再次通过包装器离开。因此，使用 `transferFrom` 为自身提供资金的适配器，在 Solana 侧没有可支配的资产。

可行路径是 SPL 委托：

* **授权。** 用户发送 `approve_spl(adapter, amount, mint)` 作为一笔交易 **直接发送到 HelperProgram** (`to = 0xff…09`，其中 `mint` 是包装器的 `mint_id()`。用户自身的权限签名，而它在用户代币账户上设置的 delegate 是适配器的外部权限。
* **转入。** 适配器的 `deposit(amount)` 通过 delegate 重载来拉取 `HelperProgram.transfer_spl(srcAta, dstAta, amount, mint)`，其中用户的 ATA 作为来源，适配器自己的 ATA（`HelperProgram.ata(address(this), mint)`，并已用其 `create_ata`创建一次）作为目标。适配器的权限以 delegate 身份签名。现在代币已经位于一个由 `external_auth(adapter)`拥有的代币账户中，这正是你的 CPI 所命名的账户；签名者和来源都是适配器自身的，因此不需要进一步授权。它们不会出现在 `wUSDC.balanceOf(adapter)`.
* **转出。** 你的程序的 withdraw 指令会把代币转到 **用户的** ATA， `HelperProgram.ata(user, mint)`。接收不需要用户签名，而且余额会立即显示为 `wUSDC.balanceOf(user)`。如果 ATA 可能尚不存在，请先用 `HelperProgram.create_ata(user, mint)` （幂等）创建它。

**一个代币账户只有一个委托。** 上面的授权会替换该账户上包装器自身的授权。在用户再次授予包装器之前，从其地址发出的任何 `wUSDC` `transfer` 或 `transferFrom` 转出都会回滚，并返回 SPL 错误 `0x4`。在使用此授权的任何流程之后，请调用 `wUSDC.isEnabled(user)`；当它返回 `false`时，请让用户再次发送 `approve_spl(wrapper, 2^64-1, mint)` 到 HelperProgram，这与包装器设计所依据的最大授权相同（见 [一次性：授予包装器](/zh/kai-fa-zhe-zhi-nan/dual-lane-app.md#one-time-grant-the-wrapper)）。受限额度的授权会在余额耗尽后再次破坏用户的普通 `wUSDC` 转账，且 `isEnabled` 会一直保持 `true` ，只要其中还剩余任何额度，应用就不会知道需要再次请求。

要检查某个 mint 对应哪个包装器，请读取工厂的 `wrapper_kind_by_mint(mint)` （工厂地址位于链的 `contracts.json` 在 [registry](https://github.com/rome-protocol/rome-registry)). `1` 是本节所描述的缓存包装器。 `2` 是工厂为带有已启用转账钩子的 Token-2022 mint 部署的直接 CPI 包装器；本指南不涵盖它。注册表的 `tokens.json` 仅列出包装器地址和 mint，但不说明其类型。

## 4. 每个合约、每笔交易只走一条轨道

Rome 有两类会改变 Solana 状态的预编译，而 **一笔交易只能使用其中一种** ——运行时会拒绝混合使用两类会改变状态的调用的交易，即使它们发生在不同合约之间。

| 轨道     | 预编译                                                                                                 | 影响生效                                  | 可在迭代执行中工作                                                 |
| ------ | --------------------------------------------------------------------------------------------------- | ------------------------------------- | --------------------------------------------------------- |
| **传统** | `CpiProgram` `0xff…08` (`invoke`, `invoke_signed`), `HelperProgram` `0xff…09`, `提取` `0x42…16`       | 立即生效，作为 EVM 步骤中的真实 CPI                | **否** ——迭代交易中的传统 CPI 会失败，并返回 `CpiProhibitedInIterativeTx` |
| **缓存** | `SplCached` `0xff…05`, `ASplCached` `0xff…06`, `SystemCached` `0xff…04`, `WithdrawCached` `0xff…0b` | 先在覆盖层中暂存，并在交易结束时作为 CPI 提交；EVM 回滚会丢弃它们 | 是                                                         |

这对程序集成意味着：

* **调用你自己程序的指令只能走传统轨道。** 缓存轨道没有通用 `invoke`；它涵盖 SPL 转账、ATA 创建、系统程序操作和 gas 提取。
* 因此，如果为适配器提供资金的那一步要与 CPI 处在同一笔交易中，它也必须走传统轨道。缓存包装器的 `transferFrom` 是缓存轨道调用，因此它不能与 CPI 共享同一笔交易（而且如第 3 节所示，它只会记入一份托管账本）。请改为使用 `HelperProgram.transfer_spl(userAta, adapterAta, amount, mint)` 在用户已让适配器成为 SPL delegate 之后再拉取（`approve_spl`，由用户直接发送到 HelperProgram）。拉取和 CPI 都属于传统轨道，因此它们可以在一笔交易中执行。
* **把包装器排除在那笔交易之外。** 不要在同一笔与传统 CPI 相同的交易中，从适配器调用包装器的会改变状态的函数（`wUSDC.transfer`, `transferFrom`）。请在前端检查 `isEnabled(user)` 和余额，使用 `eth_call`.
* **而且 CPI 交易必须是原子性的。** 这就是该设计所受的上限。

读取不受该规则约束： `account_info`, `account_data_at`, `account_u64_at`, `account_lamports`, `pdas_batch_derive` 和 `HelperProgram.user_balance` / `allowance_of` 这些都是对预加载账户的读取（无 CPI），在两条轨道和两种执行模式下都允许。把你的预检查放在这些读取上。有一条顺序规则：一旦某次缓存变更在交易中触发，剩余部分的传统轨道读取就会被阻止——先读，再改。

## 5. 原子性就是预算

一笔原子的 Rome 交易就是一笔 Solana 交易：大约 **140 万计算单元**，一个 **64 地址** 的账户列表，以及 **4 KB** 的链上字节大小（Solana v1 交易）。Rome 程序和你的 CPI 共享这部分预算。一个包装器转账在 Hadrian 上大约消耗 170K CU，而你的指令还会加上自身开销；请用 Solana 收据中的 `computeUnitsConsumed`，而不是 EVM `gasUsed`.

当模拟判断一笔交易放不下时，Rome 会 **迭代地** 将其拆分到多笔 Solana 交易中，而其中的传统 CPI 之后就会失败。请让 deposit 和 withdraw 各自都只包含一次代币移动以及一次进入你程序的 CPI，并把它们周围的 Solidity 保持简洁。任何更大的操作都拆成两笔用户交易。

CPI 深度：Solana 允许 4 层，Rome 使用其中 1 层，因此你的指令运行在第 2 层，下面还有两层（你的程序 → SPL Token 会剩下一层）。

## 6. 账户准备

运行时，你的程序不会从 Solidity 获得账户。代理会在发送之前模拟每笔交易，记录 CPI 触及的每一个 Solana 账户，并把它们放入 Solana 交易中。你的适配器在账户列表中命名这些账户；模拟会证明它们可以解析。由此得出三点： **模拟** 每笔交易，然后发送；它会记录 CPI 触及的所有 Solana 账户，并将它们放入 Solana 交易中。 `invoke_signed` 账户列表；模拟会证明它们能够解析。由此得出三点：

* 你的指令触及的每个账户都必须能从适配器已有的输入中推导出来（它自身的 PDA 和子 PDA、你的程序 ID、mint、用户地址）。 `SystemProgram.find_program_address` 和 `CpiProgram.pdas_batch_derive` 在 Solidity 中完成这些推导。
* 一个尚不存在的账户（新的仓位、一个新的 ATA）仍然必须在账户列表中命名；它的地址就是会进入 Solana 交易的地址。
* 交易触及的所有内容——包括 Rome 自己为用户、适配器和包装器持有的账户——都必须控制在 64 个地址以内。一次调用触及十个市场的设计将无法装下。

对于 Solana 钱包通道， `rome_emulateCallAccounts` 会向客户端暴露相同的发现能力；SDK 会使用它。

CPI 绝不能命名的两个账户：提交交易的 Solana 手续费支付者（普通 System 转账指令内部除外），以及 Rome 程序本身。向 Rome 程序本身发起 CPI 会被拒绝。

## 7. 用 Solidity 实现一次 deposit 和一次 withdraw

一个在传统轨道上的适配器最小示意。用户已经发送了 `approve_spl(adapter, amount, mint)` 到 HelperProgram（第 3 节）。 `PRIVACY_PROGRAM` 是你的程序 ID； `encodeDeposit` / `encodeWithdraw` 构造你的指令数据（Borsh，小端序）；账户元信息遵循你的指令布局。

```solidity
import {ICrossProgramInvocation, IHelperProgram}
    from "@rome-protocol/rome-solidity/contracts/interface.sol";

contract PrivacyAdapter {
    ICrossProgramInvocation constant Cpi    = ICrossProgramInvocation(0xFF00000000000000000000000000000000000008);
    IHelperProgram          constant Helper = IHelperProgram(0xFF00000000000000000000000000000000000009);
    bytes32 constant SPL_TOKEN = 0x06ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9; // TokenkegQ...

    bytes32 immutable PRIVACY_PROGRAM;
    bytes32 immutable USDC_MINT;

    constructor(bytes32 program, bytes32 mint) {
        PRIVACY_PROGRAM = program; USDC_MINT = mint;
        Helper.create_pda(address(this));                 // 适配器自身的 Solana 身份
        Helper.create_ata(address(this), USDC_MINT);      // 以及它拥有的一个 USDC 账户
    }

    function _salt(address user) internal pure returns (bytes32) { return bytes32(uint256(uint160(user))); }

    function deposit(uint64 amount) external {
        bytes32 self       = Helper.pda(address(this));                   // 签名者：适配器的权限
        bytes32 adapterAta = Helper.ata(address(this), USDC_MINT);
        bytes32 position   = Helper.pda_with_salt(address(this), _salt(msg.sender)); // 按用户划分的子 PDA，也会签名

        // 从用户的代币账户拉取。适配器的权限作为用户授予的 SPL delegate 签名；这是一笔传统轨道调用，就像下面的 CPI 一样。
        //
        Helper.transfer_spl(Helper.ata(msg.sender, USDC_MINT), adapterAta, amount, USDC_MINT);

        ICrossProgramInvocation.AccountMeta[] memory accs = new ICrossProgramInvocation.AccountMeta[](5);
        accs[0] = ICrossProgramInvocation.AccountMeta(self,       true,  true);
        accs[1] = ICrossProgramInvocation.AccountMeta(position,   true,  true);
        accs[2] = ICrossProgramInvocation.AccountMeta(adapterAta, false, true);                          // 来源：由适配器的权限拥有
        accs[3] = ICrossProgramInvocation.AccountMeta(_vaultFor(position), false, true);                 // 你程序的金库
        accs[4] = ICrossProgramInvocation.AccountMeta(SPL_TOKEN, false, false);

        bytes32[] memory salts = new bytes32[](1);
        salts[0] = _salt(msg.sender);
        Cpi.invoke_signed(PRIVACY_PROGRAM, accs, encodeDeposit(amount), salts);
    }

    function withdraw(uint64 amount) external {
        bytes32 position = Helper.pda_with_salt(address(this), _salt(msg.sender));
        bytes32 userAta  = Helper.ata(msg.sender, USDC_MINT);            // 目标：用户自己的账户
        // ... 形态相同：你的程序将 `amount` 从金库转到 userAta，并由 `position` 签名
        Cpi.invoke_signed(PRIVACY_PROGRAM, accs, encodeWithdraw(amount), salts);
    }
}
```

适配器从不调用 `wUSDC` 合约。拉取操作是一次 HelperProgram 调用，使用的是用户的委托，而代币存放在一个 `external_auth(adapter)` 由其拥有

的代币账户中，这就是 CPI 能够支出这些代币的原因。`Helper.pda(address(this))`），它无需出现在 `salts`中就能签名，以及 `salts`中的每一项对应一个带盐的子 PDA。你列出的每个盐值都必须作为签名者出现在账户列表中，否则调用会失败，并返回 `未找到由所提供 seed 计算出的 signer 账户`。任何其他被标记为签名者的账户，包括用户自身的权限，Rome 都不会为其提供 seed，Solana 运行时会拒绝该 CPI。用户对其代币的权利来自委托，而不是来自适配器代替用户签名。

无拉取的委托：如果你的程序应该直接从用户的代币账户支出，用户可以直接向 HelperProgram 发送 `HelperProgram.approve_spl_raw_delegate(userAta, yourProgramPda, amount, mint, decimals)` ，将你程序的 PDA 指定为 SPL delegate。Rome bridge 在 Wormhole 中就是这样使用它的。第 3 节中的单一 delegate 规则在这里同样适用：该授权会取代包装器的授权。

## 8. 测试

* 部署到 **Hadrian** （测试网，链 200010）上先进行；地址和 RPC 位于 [合约地址](/zh/can-kao/contract-addresses.md) 以及 [registry](https://github.com/rome-protocol/rome-registry).
* `eth_call` 和 `eth_estimateGas` 运行与代理用于发送时相同的模拟，因此在这里失败的 CPI 也会因同样原因在链上失败，并且回滚数据会携带底层的 Solana 错误（`非 EVM 调用错误：…` 在 Solana 程序日志中）。
* 使用以下方式读取已落地的 EVM 交易在 Solana 侧的内容： `rome_solanaTxForEvmTx(hash)`；Solana 收据中包含 CU 和你的程序日志。
* 在 `eth_call` 中可正常工作的 CPI，但在发送时被拒绝，通常是触发了模拟报告的某个限制：64 个地址上限、原子 CU 预算（该交易进入了迭代模式），或 track 规则。

## 9. 参考

* [预编译参考](/zh/can-kao/precompiles.md) — 每个预编译合约上的每个选择器，以及其分发类别和 delegatecall 规则。
* [约束](/zh/he-xin-gai-nian/constraints.md) — 将上述限制汇总在一张表中。
* [代币互操作](/zh/he-xin-gai-nian/token-interop.md) — 包装器、规范铸币、存款和取款。
* 公共仓库中的参考应用： [cardo](https://github.com/rome-protocol/cardo) （用户直接调用 CPI 预编译合约，形态 B）， [rome-dex](https://github.com/rome-protocol/rome-dex) （直接 `invoke` 加上一个无托管路由器，形态 C），以及 [aerarium](https://github.com/rome-protocol/aerarium) （一个基于共享市场的 Compound III 借贷应用；它不会从 Solidity 调用 Solana 程序（无 CPI），因此不是适配器的示例）。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.rome.builders/zh/kai-fa-zhe-zhi-nan/integrate-solana-program.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
