> 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/dual-lane-app.md).

# 构建双通道应用

A **双通道应用** 是一个 Solidity 合约，同时供一个 **MetaMask（EVM）** 用户和一个 **Phantom（Solana）** 用户可直接使用——同一个合约、同一份状态，每个人都用自己已有的钱包。本页将逐步讲解 *准确地* 在每一步、在双方各自一侧会发生什么。

示例是一个很小的 **金库**：你 `存入` USDC，之后再 `提取` 它。“质押 / 取消质押”、“供应 / 赎回”、“打赏 / 领取”都是同一类模式。

## 你要编写的是——一个标准的 ERC-20 金库

在 Rome 上，Solana 用户的 USDC 在 EVM 侧会表现为普通的 **ERC-20 代币** ——该铸币的 SPL 包装代币（例如 `wUSDC`）。因此你的合约就是一个普通的代币金库：它通过 `transferFrom` 拉取代币，并通过 `transfer`返还代币。没有任何 Rome 特有的内容：

```solidity
interface IERC20 {
    function transfer(address to, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

contract Vault {
    IERC20 public immutable token;                 // wUSDC 包装代币
    mapping(address => uint256) public balanceOf;
    constructor(IERC20 _token) { token = _token; }

    function deposit(uint256 amount) external {
        require(token.transferFrom(msg.sender, address(this), amount));
        balanceOf[msg.sender] += amount;
    }
    function withdraw(uint256 amount) external {
        balanceOf[msg.sender] -= amount;
        require(token.transfer(msg.sender, amount));
    }
}
```

> **为什么用 ERC-20，而不是 `payable` / `msg.value`?** Solana 用户可支配的余额是他们的 **钱包中的 SPL 代币账户**，它会一对一地体现为这个 ERC-20 包装代币—— *不是* EVM 原生余额。因此，Solana 用户总是可以为一个 `transferFrom`注资，但一个 `deposit() payable` 则需要他们并未持有的原生资产。围绕代币来设计，两条通道就能以相同方式工作。

使用 Foundry 或 Hardhat 部署它，并将构造函数指向你的代币对应的包装地址（来自 [注册表](https://github.com/rome-protocol/rome-registry)）。在 Rome 上，gas 代币是 USDC，因此你需要少量 USDC gas 余额才能部署。

## 两条通道

| 通道         | 钱包                    | 应用如何调用它                               |
| ---------- | --------------------- | ------------------------------------- |
| **EVM**    | MetaMask（一个 EVM 密钥）   | `submitRomeTx` ——标准的 Rome 写入          |
| **Solana** | Phantom（一个 Solana 密钥） | `submitRomeTxSolanaLane` ——不需要 EVM 密钥 |

EVM 通道很普通。本页其余部分讲的是 **Solana 通道** ——有意思的另一半。

## 核心思想：synthetic 只是一个中转通道

Solana 用户的 EVM 身份就是他们的 **synthetic 地址** —— `keccak256(solana_pubkey)[12:]`。它是他们的 `msg.sender` 在合约中的身份，但 **它静态时不持有任何资产。** 用户的资金保存在他们的 **Solana 钱包** （以 SPL USDC 形式），而在 EVM 侧，同一余额就是 `wUSDC.balanceOf(synthetic)` 读取到的值。价值流动 *经过* synthetic：

* **入** （存款）：钱包代币账户 → synthetic 代币账户 → 合约（通过 `transferFrom`).
* **出** （提取）：合约 → synthetic 代币账户 → 钱包代币账户。

每次往返后，synthetic 都会净额归零。

## 一次性操作：激活（为 synthetic 提供账户）

全新的 synthetic 链上账户在你创建之前并不存在。Solana 用户第一次操作时，他们的 synthetic 会被 **预置** 为 `create_pda` 调用——之后，任何移动价值的调用（ERC-20 `transferFrom`，以及 sweep）都可以由它签名。

`submitRomeTxSolanaLane` 会自动执行此操作 **在首次使用时自动** (`autoProvision` 默认开启）。如果你更希望展示一个明确的“激活”界面（一次性的账户设置），可以自行实现：

```javascript
import { provisionSynthetic, isSyntheticProvisioned } from "@rome-protocol/sdk";

const deps = { connection, proxyUrl, programId, chainId, payer: wallet.publicKey, signTransaction: wallet.signTransaction };
if (!(await isSyntheticProvisioned(connection, programId, synthetic))) {
  await provisionSynthetic(deps); // 一次 create_pda；然后以 autoProvision: false 提交写入
}
```

## 双方各自需要什么

|                        | 需要                           | 原因                                              |
| ---------------------- | ---------------------------- | ----------------------------------------------- |
| **Solana 用户（Phantom）** | **SOL** （少量）                 | 为每笔通道交易支付 Solana 交易费用                           |
|                        | **作为 SPL 代币的 USDC** 在他们的钱包中  | 他们存入的价值（在 EVM 侧显示为 `wUSDC`)                     |
| **EVM 用户（MetaMask）**   | **USDC** 作为他们的 Rome gas 余额   | gas + 价值；他们通过以下方式补充： **桥接 USDC** 进入 Rome（没有水龙头） |
| **你（构建者）**             | 一个带有 **USDC** Rome 上 gas 的钱包 | 用于部署合约                                          |

## 价值流入——存款（逐步说明）

Solana 用户的 Phantom 钱包中有 SOL + USDC。每笔通道交易都是 **由 Phantom 签名** ——钱包对其签名并发送到 Solana RPC（代理仅用于发现账户）：

1. **资金注入步骤** —— `buildFundLeg(...)` → `submitSolanaInstructions(...)`。创建 synthetic 的 USDC 代币账户（如有需要），并执行 **`ActivateAta`**，将 `amount` 的 USDC 从钱包的代币账户 **转入 synthetic 的**。现在 `wUSDC.balanceOf(synthetic)` 会显示该余额。
2. **批准** —— `submitRomeTxSolanaLane({ to: wUSDC, data: approve(vault, amount) })`。允许金库拉取代币。 *（这通常是第一次通道调用，因此 synthetic 会在此自动预置。）*
3. **存款** —— `submitRomeTxSolanaLane({ to: vault, data: deposit(amount) })`。金库执行 `transferFrom(synthetic, vault, amount)` ——USDC 从 synthetic 转入金库，并记入 synthetic 的地址。

**净效应：** USDC 经由 **Phantom 钱包 →（synthetic）→ 金库。**

```javascript
import { syntheticAddress, buildFundLeg, submitSolanaInstructions, submitRomeTxSolanaLane } from "@rome-protocol/sdk";
import { encodeFunctionData, erc20Abi } from "viem";

const synthetic = syntheticAddress(wallet.publicKey);
const deps = { connection, proxyUrl, programId, chainId, payer: wallet.publicKey, signTransaction: wallet.signTransaction };

// 1) 资金注入步骤 — 钱包 USDC → synthetic 代币账户（由 Phantom 签名）
await submitSolanaInstructions(
  buildFundLeg({ programId, chainId, mint: usdcMint, amount: depositAmount, wallet: wallet.publicKey, synthetic }),
  { connection, feePayer: wallet.publicKey, signTransaction: wallet.signTransaction },
);

// 2) 批准金库（第一次通道调用 → synthetic 自动预置）
await submitRomeTxSolanaLane(deps, { to: wUSDC, data: encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [vault, depositAmount] }) });

// 3) 存款 — 金库通过 transferFrom 拉取
await submitRomeTxSolanaLane(deps, { to: vault, data: encodeFunctionData({ abi, functionName: "deposit", args: [depositAmount] }) });
```

## 价值流出——提取（逐步说明）

现在用户提取。仍由 Phantom 签名：

1. **提取** —— `submitRomeTxSolanaLane({ to: vault, data: withdraw(amount) })`. `vault.withdraw` 执行 `transfer(synthetic, amount)` ——USDC 从金库重新转入 **synthetic 的** 代币账户。
2. **清扫步骤** —— `buildSweepLeg(...)` 会为你提供 `HelperProgram.transfer_spl` 调用及相关账户；执行它（如有需要先创建钱包的代币账户，然后通过 `DoTxUnsigned` 向 Helper 预编译合约发起）将 USDC 从 synthetic **转回用户自己的 Solana 钱包**。synthetic 再次净额归零。

**净效应：** USDC 经由 **金库 →（synthetic）→ 用户的 Phantom 钱包。** 没有任何资产被滞留。

```javascript
import { buildSweepLeg } from "@rome-protocol/sdk";

// 1) 提取 — 金库将 USDC 返还到 synthetic 的代币账户
await submitRomeTxSolanaLane(deps, { to: vault, data: encodeFunctionData({ abi, functionName: "withdraw", args: [amount] }) });

// 2) 清扫步骤 — synthetic 代币账户 → 用户自己的钱包代币账户
const sweep = buildSweepLeg({ programId, mint: usdcMint, amount, wallet: wallet.publicKey, synthetic });
await submitSolanaInstructions([sweep.ensureWalletAtaIx], { connection, feePayer: wallet.publicKey, signTransaction: wallet.signTransaction });
await submitRomeTxSolanaLane(deps, { to: sweep.helperTo, data: sweep.calldata, extraAccounts: sweep.extraAccounts });
```

## 注意事项——全部由 SDK 处理

这些是手工构建的 Solana 通道交易容易出错的地方； `submitRomeTxSolanaLane` SDK 会替你处理：

* **预置。** 全新的 synthetic 账户必须先创建（`create_pda`）才能进行任何价值移动调用，否则它无法对转账签名。首次使用时自动执行；如需关闭，请使用 `autoProvision: false` + `provisionSynthetic`.
* **花费包装代币，而不是 `msg.value`.** Solana 用户的余额就是他们的 SPL 代币账户，并以 ERC-20 包装代币的形式呈现——请使用 `transfer` / `transferFrom`进行转移，切勿使用原生资产。
* **ComputeBudget。** Rome 的 EVM 需要更高的 CU 上限（约 135 万）和更大的堆帧（约 250 KB）。Solana 默认的 20 万 CU / 32 KB 会失败。
* **Treasury 钱包。** 执行会向每条链的 treasury 账户支付少量费用；账户发现不会包含它，因此 SDK 会自动附加。
* **发送到哪里。** 钱包 **对 Solana 交易签名并将其发送到 Solana RPC** ——不是发送给代理。代理仅用于账户发现。在链上，程序会从 `msg.sender` Solana 签名者派生。
* **Gas 以 USDC 计。** 没有水龙头——请桥接 USDC 进来（参见 [获取资金](/zh/zi-yuan/faucets.md)).

## 来自 MetaMask 的同一应用

EVM 用户使用以下方式调用相同的合约： `submitRomeTx` ——标准 EVM 工具，gas 以 USDC 计。他们仍然需要 `批准` 然后 `存入` （与普通 ERC-20 相同），不需要资金注入/清扫步骤（他们的代币本来就位于其 EVM 地址）。两类用户共享同一份 `balanceOf` 状态。

## 下一步

* [从 Solana 调用 EVM](/zh/kai-fa-zhe-zhi-nan/call-evm-from-solana.md) ——Solana 通道机制详解
* [从 EVM 调用 Solana](/zh/kai-fa-zhe-zhi-nan/call-solana-from-evm.md) ——反向调用（CPI）
* [获取资金](/zh/zi-yuan/faucets.md) ——以 USDC 作为 gas 代币；请桥接进来


---

# 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 dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.rome.builders/zh/kai-fa-zhe-zhi-nan/dual-lane-app.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
