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

# デュアルレーンアプリを構築する

デュアルレーンアプリのステップバイステップ解説 — MetaMaskユーザーとSolanaウォレットユーザーの両方が使う1つのSolidityコントラクトで、各ステップで何が起こるかを正確に示します。

ひとつの **デュアルレーンのアプリ** とは、1つのSolidityコントラクトで、 **MetaMask（EVM）** ユーザーと **Solanaウォレット** ユーザーがそのまま直接使います — 同じコントラクト、同じ状態で、それぞれ既存のウォレットを使います。このページでは *まさに* 各ステップで何が起こるかを、両側から説明します。

例は小さな **vault**です。あなたは `deposit` USDCを預け、その後で `withdraw` それを引き出します。「stake / unstake」「supply / redeem」「tip / claim」は同じ形です。

## 書くもの — 標準的なERC-20 vault

Romeでは、SolanaユーザーのUSDCはEVM側では通常の **ERC-20トークン** — そのミントのSPLラッパー（例： `wUSDC`）。したがって、あなたのコントラクトは通常のトークンvaultです。トークンを `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ラッパーとして1対1で表面化します — *ではなく* EVMネイティブ残高です。したがって、Solanaユーザーは常に `transferFrom`に資金を入れられますが、 `payable な deposit()` は、休止時には持っていないネイティブ価値を必要とします。トークンを中心に設計すれば、両方のレーンは同じように動きます。

FoundryまたはHardhatでデプロイし、コンストラクタにはあなたのトークンのラッパーアドレス（ [registry](https://github.com/rome-protocol/rome-registry)）を指定します。RomeではガストークンはUSDCなので、デプロイには少しUSDCのガス残高が必要です。

## 2つのレーン

| レーン        | ウォレット                             | アプリがどう呼び出すか                                               |
| ---------- | --------------------------------- | --------------------------------------------------------- |
| **EVM**    | MetaMask（EVMキー）                   | `submitRomeTx` — Romeの標準的な書き込み                            |
| **Solana** | SolflareのようなSolanaウォレット（Solanaキー） | `submitRomeTxV1` — 1回の呼び出しにつきSolana v1トランザクション1件、EVMキーは不要 |

EVMレーンは普通です。このページの残りは **Solana レーン** — 面白いほうの半分です。

## 要点：syntheticはパススルーです

SolanaユーザーのEVM上の識別子は、その **シンセティックアドレス** — `keccak256(solana_pubkey)[12:]`。それはその `msg.sender` としてコントラクト内にありますが、 **休止時には何も保持しません。** ユーザーのお金はその **Solanaウォレット** （SPL USDCとして）にあり、EVM側ではその同じ残高が `wUSDC.balanceOf(synthetic)` で読み取られます。価値は *を通って* syntheticを経由して流れます：

* **流入時** （deposit）：ウォレットのトークン口座 → syntheticのトークン口座 → コントラクト（ `transferFrom`).
* **流出時** （withdraw）：コントラクト → syntheticのトークン口座 → ウォレットのトークン口座。

syntheticは、往復ごとに差し引きゼロになります。

## 1回だけ：Activate（syntheticをプロビジョニングする）

新しいsyntheticのオンチェーンアカウントは、作成するまで存在しません。Solanaユーザーが最初に操作するとき、そのsyntheticは **プロビジョニングされ** 、 `create_pda` での呼び出しにより作成されます — その後は、価値を動かす呼び出し（ERC-20 `transferFrom`、sweep）はそれで署名できます。

`submitRomeTxV1` はこれを自動では行わないため、最初の価値移動呼び出しの前に、明示的に1回プロビジョニングしてください。適した場所は「Activate」ステップか、最初の approve の直前です：

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

const synthetic = syntheticAddress(wallet.publicKey);
const deps = { connection, proxyUrl, chainId, programId, signer }; // signer: スクリプトではKeypair、ウォレットアプリではLaneSigner
if (!(await isSyntheticProvisioned(connection, programId, synthetic))) {
  await submitRomeTxV1(deps, buildCreatePdaCall(synthetic)); // one create_pda
}
```

## 一回限り：ラッパーに付与

wUSDCラッパーは、ユーザーのトークンを **SPL デリゲート** トークン口座のデリゲートとして動かします。そのため、各ユーザーは、最初のdepositの前に、ラッパーごとに1回、その権限を付与する必要があります。これはMetaMaskユーザーにも、Solanaユーザーのsyntheticアドレスにも同様に適用されます。権限付与がないと、毎回 `transferFrom` ウォレットから出そうとすると失敗し、depositはSPLエラーでrevertします `0x4` (owner mismatch) で revert します。

ラッパーのviewで確認してください `isEnabled(user)`。返り値が `false`であれば、 `approve_spl(wrapper, 2^64-1, mint)` を `0xff00000000000000000000000000000000000009`のHelper precompileに直接送ります。mintは `mint_id()` ラッパー上にあります。EVMレーンではこれは `submitRomeTx` 呼び出しであり、Solanaレーンでは `submitRomeTxV1` 呼び出しです：

```javascript
import { submitRomeTx, submitRomeTxV1, syntheticAddress } from "@rome-protocol/sdk";
import { createPublicClient, encodeFunctionData, http, parseAbi } from "viem";

const HELPER = "0xff00000000000000000000000000000000000009";
const wrapperAbi = parseAbi(["function isEnabled(address user) view returns (bool)", "function mint_id() view returns (bytes32)"]);
const grantAbi = parseAbi(["function approve_spl(address spender, uint64 amount, bytes32 mint)"]);

const publicClient = createPublicClient({ transport: http(rpcUrl) }); // チェーンのRPC上のviemクライアント

// who: MetaMaskアドレス、またはSolanaユーザーなら syntheticAddress(wallet.publicKey)
let grant = null; // isEnabled が true のときは null のまま
if (!(await publicClient.readContract({ address: wUSDC, abi: wrapperAbi, functionName: "isEnabled", args: [who] }))) {
  const mint = await publicClient.readContract({ address: wUSDC, abi: wrapperAbi, functionName: "mint_id" });
  grant = { to: HELPER, data: encodeFunctionData({ abi: grantAbi, functionName: "approve_spl", args: [wUSDC, 2n ** 64n - 1n, mint] }) };
}
// EVM lane:    if (grant) await submitRomeTx(provider, { from: who, ...grant });
// Solana lane: if (grant) await submitRomeTxV1(deps, grant);
```

## 各側で必要なもの

|                       | 必要なもの                                                  | 理由                                                                                   |
| --------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------ |
| **Solanaユーザー**        | **SOL** （少し）                                           | 各レーンのtxごとのSolanaトランザクション手数料を支払う                                                      |
|                       | **SPLトークンとしてのUSDC** ウォレット内に                            | 預ける価値（EVM側では `wUSDC`)                                                                |
| **EVMユーザー（MetaMask）** | **USDC** Romeのガス残高として                                  | ガスと、ラップしたあとの預け入れ価値です；不足分は **USDCをブリッジして補充します** Romeへ（faucetはありません）                   |
|                       | ある **wrap** をそのガスの一部について **wUSDC** (`withdraw_to_ata`) | vaultはネイティブガスではなく、ERC-20ラッパーであるwUSDCを受け取ります。MetaMaskのガスUSDC残高は、ラップされるまではwUSDCではありません |
|                       | 1回限りの **ラッパーへの権限付与** (`approve_spl`)                   | により、wUSDCラッパーがそのトークンを移動できるようになります；参照 [一回限り：ラッパーに付与](#one-time-grant-the-wrapper)     |
| **あなた（ビルダー）**         | を持つウォレット **USDC** Rome上のガス                             | コントラクトをデプロイするための                                                                     |

## Value IN — deposit（ステップごと）

SolanaユーザーはSolanaウォレットにSOLとUSDCを持っています。各ステップはSolana v1トランザクションで、 **そのウォレットで署名され**、アプリがそれをSolana RPCに送ります。fund legはアプリ内で組み立てられ、ウォレットは1回のプロンプトで署名します。各コントラクト呼び出しはプロキシによって組み立てられ、ウォレットは読みやすい承認メッセージの後に署名します：

1. **資金レッグ** — `buildFundLeg(...)` → `submitSolanaInstructionsV1(...)`。syntheticのUSDCトークン口座（必要なら）を作成し、 **`ActivateAta`**&#x3092;移動します。 `量` ウォレットのトークン口座からUSDCの **をsyntheticの**によって構築されます。これで `wUSDC.balanceOf(synthetic)` に移し、その残高を表示します。
2. **ラッパーに権限を付与** （ `isEnabled` が false のとき）— `submitRomeTxV1(deps, { to: HELPER, data: approve_spl(wUSDC, 2^64-1, mint) })`。参照 [一回限り：ラッパーに付与](#one-time-grant-the-wrapper). *（ユーザーの最初のレーン呼び出しなら、この前にsyntheticをプロビジョニングしてください。）*
3. **Approve** — `submitRomeTxV1(deps, { to: wUSDC, data: approve(vault, amount) })`。vaultがトークンを引き出せるようにします。
4. **預け入れ** — `submitRomeTxV1(deps, { to: vault, data: deposit(amount) })`。vaultは `transferFrom(synthetic, vault, amount)` — USDCはsyntheticからvaultへ移動し、syntheticのアドレスに計上されます。

**差し引き効果：** USDCは **Solanaウォレット →（synthetic）→ vault。**

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

const synthetic = syntheticAddress(wallet.publicKey);
const deps = { connection, proxyUrl, chainId, programId, signer }; // signer: LaneSigner（payer, signAuthHash, signV1Wire）またはKeypair

// 1) fund leg — wallet USDC → synthetic token account (the wallet signs)
await submitSolanaInstructionsV1(
  buildFundLeg({ programId, chainId, mint: usdcMint, amount: depositAmount, wallet: wallet.publicKey, synthetic }),
  { connection, signer },
);

// 2) ラッパーへの権限付与を1回だけ行う：`grant` は「1回だけ：ラッパーへの権限付与」で組み立てた呼び出し、または null
if (grant) await submitRomeTxV1(deps, grant);

// 3) vault を approve する
await submitRomeTxV1(deps, { to: wUSDC, data: encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [vault, depositAmount] }) });

// 4) deposit — the vault pulls via transferFrom
await submitRomeTxV1(deps, { to: vault, data: encodeFunctionData({ abi, functionName: "deposit", args: [depositAmount] }) });
```

## Value OUT — withdraw（ステップごと）

ここでユーザーが引き出します。こちらもウォレット署名で、1回の呼び出しにつき2回のプロンプトです：

1. **Withdraw** — `submitRomeTxV1(deps, { to: vault, data: withdraw(amount) })`. `vault.withdraw` が実行され `transfer(synthetic, amount)` — USDCはvaultから次の場所へ戻ります **syntheticの** トークン口座。
2. **回収レッグ** — `buildSweepLeg(...)` で得られるのは `HelperProgram.transfer_spl` 呼び出し＋アカウントです；必要なら `submitSolanaInstructionsV1`でウォレットのトークン口座を作成し、その後 Helper 呼び出しを `submitRomeTxV1`；プロキシがトークン口座を見つけます）を使って、USDCをsyntheticから移動します **ユーザー自身のSolanaウォレットへ戻します**。syntheticは差し引きゼロになります。

**差し引き効果：** USDCは **vault →（synthetic）→ ユーザーのSolanaウォレット。** 何も取り残されません。

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

// 1) withdraw — vault returns USDC to the synthetic's token account
await submitRomeTxV1(deps, { to: vault, data: encodeFunctionData({ abi, functionName: "withdraw", args: [amount] }) });

// 2) sweep leg — synthetic token account → the user's own wallet token account
const sweep = buildSweepLeg({ programId, mint: usdcMint, amount, wallet: wallet.publicKey, synthetic });
await submitSolanaInstructionsV1([sweep.ensureWalletAtaIx], { connection, signer });
await submitRomeTxV1(deps, { to: sweep.helperTo, data: sweep.calldata });
```

## Hadrianで計測

以下の各Solanaレーンのステップは、Hadrian上でversion-1トランザクション1件として実行されました：

| ステップ                   | 計算ユニット  | バイト   | アカウント |
| ---------------------- | ------- | ----- | ----- |
| プロビジョニング（`create_pda`) | 39,008  | 703   | 9     |
| ラッパー付与（`approve_spl`)  | 50,687  | 899   | 13    |
| Approve                | 79,335  | 769   | 10    |
| Vaultへの入金              | 242,880 | 1,070 | 20    |
| Vaultからの出金             | 280,903 | 1,037 | 19    |
| 回収（`transfer_spl`)     | 46,810  | 899   | 13    |

## 注意点

手作りのSolanaレーンのトランザクションで間違えやすい点です。SDKとプロキシのcomposerがその大半を処理します：

* **プロビジョニング。** 新しいsyntheticのアカウントは作成しなければなりません（`create_pda`） `submitRomeTxV1` は自動ではやってくれません：確認するのは `isSyntheticProvisioned` を確認し、 `buildCreatePdaCall(synthetic)` を1回です。
* **ラッパーへの権限付与は1回だけ行います。** ラッパーはユーザーのSPLデリゲートとしてトークンを動かします。確認して `isEnabled` 送信し `approve_spl` 最初のdepositの前にHelper precompileへ送ります。そうしないとdepositはSPLエラーでrevertします `0x4`.
* **使うのはラッパーであって、 `msg.value`.** Solanaユーザーの残高はSPLトークン口座であり、ERC-20ラッパーとして表面化しています — それを移動するには `transfer` / `transferFrom`を使い、ネイティブ価値は使いません。
* **あなたのために組み立てられます。** ComputeBudget、treasure wallet、アカウント探索はすべてプロキシのcomposerが行うため、コードには一切現れません。ウォレットが署名する前に、SDKが組み立て済みトランザクションを検査し、ユーザーが許可した呼び出し以外は拒否します。
* **送信先。** ウォレットはv1トランザクションと **アプリがそれをSolana RPCへ送ります** — 組み立てるプロキシではありません（ただし、下記のリレイ呼び出しだけは例外です；参照 [リレイされた呼び出しと長時間呼び出し](/ja/gaido/call-evm-from-solana.md#relayed-calls-and-long-calls)）。オンチェーンでは、プログラムが `msg.sender` をSolana署名者から導出します。
* **1回の呼び出しにつき2回のプロンプト。** ウォレットは読みやすい承認メッセージ（`Rome authorization` とEVMトランザクションハッシュ）に署名し、その後トランザクション本体にも同じ鍵で署名します。
* **あなたのウォレットがv1に署名できるか確認してください。** `signV1Wire` はv1メッセージに対する64バイトの署名を返さなければなりません；Wallet Standard経由ではそれは `solana:signTransaction` 生のwire上では `signedTransaction.slice(-64)`。まだすべてのウォレットがv1トランザクションを解釈できるわけではないので、採用する前にご自身のものをテストしてください。現在のdevnetでは、Solflareは署名できますが、Phantomはまだ対応していません。
* **大きな呼び出しはリレーされます。** 1件のトランザクションに収まらない呼び出しは、同じ承認署名でプロキシによりリレーされ、その結果には `route: "relay"`。リレイ呼び出しの支払いはチェーンのネイティブガスで行われるため、syntheticには `fundSyntheticGas(deps, amountWei)`。参照 [リレイされた呼び出しと長時間呼び出し](/ja/gaido/call-evm-from-solana.md#relayed-calls-and-long-calls).
* **ガスはUSDCです。** faucetはありません — USDCをブリッジしてください（参照 [資金を受け取る](/ja/rissu/faucets.md)).

先ほどの `submitRomeTxSolanaLane` は今もSDKにあります：クライアント側で従来のSolanaトランザクションを組み立てます（ウォレットへのプロンプトは1回で、ComputeBudgetとtreasure walletの処理はSDKが行います）。

## MetaMaskから同じアプリを使う場合

EVMユーザーは同じコントラクトを `submitRomeTx` — 標準的なEVMツールを使い、ガスはUSDCです。Solanaユーザーとの違いは1つだけ：MetaMaskユーザーが持つのは **ネイティブガスのUSDC**であって、wUSDCではありません。vaultはwUSDCラッパーを受け取るため、ユーザーはまずガスの一部をwrapし、その後ラッパーへ1回だけ権限を付与し、それから通常どおりERC-20の手順を行います。ラップ済みトークンはすでにEVMアドレス上にあるので、fundやsweepのレッグはありません。

1. **ガスをwUSDCにwrapする** — 送信 `withdraw_to_ata(wei)` Withdraw precompileの `0x4200000000000000000000000000000000000016`へ送ります。それだけのネイティブガスをburnし、同量をユーザーのガストークン口座に加算します。 `wUSDC.balanceOf(user)` その後、それが表示されます。 `wei` は18桁のガス単位で、10^12の倍数（6桁のwUSDC 1単位）でなければならず、そうでないと呼び出しはrevertします。ユーザーがすでにwUSDCを持っている場合は、このステップは省略してください。
2. **ラッパーに権限を付与** （ `isEnabled` が false のとき）— 同じ `approve_spl` 呼び出しを上と同様に `submitRomeTx`。参照 [一回限り：ラッパーに付与](#one-time-grant-the-wrapper).
3. **Approve** — `approve(vault, amount)` をwUSDCに対して送ります。
4. **預け入れ** — `deposit(amount)` をvaultに対して送ります。Withdrawは `withdraw(amount)`.

```javascript
import { submitRomeTx } from "@rome-protocol/sdk";
import { encodeFunctionData, erc20Abi, parseAbi } from "viem";

const WITHDRAW = "0x4200000000000000000000000000000000000016";
const withdrawAbi = parseAbi(["function withdraw_to_ata(uint256 wei_)"]);

// 1) ガスUSDCの一部をwUSDCにwrapする（weiAmountは18桁のガス単位）
await submitRomeTx(provider, { from: who, to: WITHDRAW, data: encodeFunctionData({ abi: withdrawAbi, functionName: "withdraw_to_ata", args: [weiAmount] }) });

// 2) ラッパーへの権限付与を1回だけ行う：`grant` は「1回だけ：ラッパーへの権限付与」で組み立てた呼び出し、または null
if (grant) await submitRomeTx(provider, { from: who, ...grant });

// 3) approve、4) deposit は通常のERC-20と同じ
await submitRomeTx(provider, { from: who, to: wUSDC, data: encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [vault, depositAmount] }) });
await submitRomeTx(provider, { from: who, to: vault, data: encodeFunctionData({ abi, functionName: "deposit", args: [depositAmount] }) });
```

両ユーザーは同じ `balanceOf` の状態をvault内で共有します。

## 次に何をするか

* [SolanaからEVMを呼ぶ](/ja/gaido/call-evm-from-solana.md) — Solanaレーンの仕組みを詳しく
* [EVMからSolanaを呼ぶ](/ja/gaido/call-solana-from-evm.md) — 逆方向（CPI）
* [資金を受け取る](/ja/rissu/faucets.md) — ガストークンとしてのUSDC；ブリッジして入れる


---

# 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/ja/gaido/dual-lane-app.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.
