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

# Bangun aplikasi dua jalur

Panduan langkah demi langkah sebuah aplikasi dua jalur — satu kontrak Solidity yang digunakan oleh pengguna MetaMask dan pengguna dompet Solana, dengan penjelasan tepat tentang apa yang terjadi di setiap langkah.

Sebuah **aplikasi dua jalur** adalah satu kontrak Solidity yang digunakan oleh baik seorang **MetaMask (EVM)** pengguna dan seorang **wallet Solana** pengguna gunakan langsung — kontrak yang sama, state yang sama, masing-masing dengan wallet yang sudah mereka miliki. Halaman ini memandu melalui *tepat* apa yang terjadi pada setiap langkah, di masing-masing sisi.

Contohnya adalah sebuah kecil **vault**: Anda `deposit` USDC dan kemudian `withdraw` itu. "Stake / unstake", "supply / redeem", "tip / claim" memiliki bentuk yang sama.

## Yang Anda tulis — vault ERC-20 standar

Di Rome, USDC milik pengguna Solana muncul di sisi EVM sebagai sebuah biasa **token ERC-20** — pembungkus SPL untuk mint tersebut (mis. `wUSDC`). Jadi kontrak Anda adalah vault token biasa: kontrak ini menarik token dengan `transferFrom` dan mengembalikannya dengan `transfer`. Tidak ada yang spesifik 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;                 // pembungkus 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));
    }
}
```

> **Mengapa ERC-20, bukan `payable` / `msg.value`?** Saldo yang dapat dibelanjakan milik pengguna Solana adalah **akun token SPL wallet mereka**, ditampilkan 1:1 sebagai pembungkus ERC-20 ini — *bukan* saldo native EVM. Jadi pengguna Solana selalu bisa mendanai sebuah `transferFrom`, tetapi sebuah `deposit() payable` akan membutuhkan nilai native yang tidak mereka simpan saat tidak aktif. Bangun di sekitar token dan kedua jalur akan bekerja dengan cara yang sama.

Deploy dengan Foundry atau Hardhat, arahkan konstruktor ke alamat pembungkus untuk token Anda (dari [registry](https://github.com/rome-protocol/rome-registry)). Di Rome, token gas adalah USDC, jadi Anda perlu sedikit saldo gas USDC untuk deploy.

## Dua jalur

| Jalur      | Wallet                                               | Bagaimana aplikasi memanggilnya                                                  |
| ---------- | ---------------------------------------------------- | -------------------------------------------------------------------------------- |
| **EVM**    | MetaMask (kunci EVM)                                 | `submitRomeTx` — penulisan Rome standar                                          |
| **Solana** | sebuah wallet Solana seperti Solflare (kunci Solana) | `submitRomeTxV1` — satu transaksi Solana v1 per panggilan, tidak perlu kunci EVM |

Jalur EVM itu biasa. Bagian lain dari halaman ini adalah **jalur Solana** — bagian yang menarik.

## Gagasan utamanya: synthetic adalah jalur penerusan

Identitas EVM pengguna Solana adalah **alamat sintetis** — `keccak256(solana_pubkey)[12:]`. Itu adalah `msg.sender` di kontrak, tetapi **itu tidak menyimpan apa-apa saat tidak aktif.** Uang pengguna berada di **wallet Solana** (sebagai SPL USDC), dan di sisi EVM saldo yang sama itulah yang `wUSDC.balanceOf(synthetic)` dibaca. Nilai mengalir *melalui* melalui synthetic:

* **Masuk** (deposit): akun token wallet → akun token synthetic → kontrak (melalui `transferFrom`).
* **Keluar** (withdraw): kontrak → akun token synthetic → akun token wallet.

Synthetic kembali menjadi nol setelah setiap perjalanan bolak-balik.

## Satu kali: Aktifkan (provision synthetic)

Akun on-chain synthetic yang baru tidak ada sampai Anda membuatnya. Saat pertama kali pengguna Solana bertindak, synthetic mereka **diprovisionkan** dengan sebuah `create_pda` panggilan — setelah itu, panggilan yang memindahkan nilai (ERC-20 `transferFrom`, sweep) dapat ditandatangani olehnya.

`submitRomeTxV1` tidak melakukannya untuk Anda, jadi provision secara eksplisit, sekali, sebelum panggilan pemindah nilai pertama. Tempat yang baik adalah langkah "Activate", atau tepat sebelum approve pertama:

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

const synthetic = syntheticAddress(wallet.publicKey);
const deps = { connection, proxyUrl, chainId, programId, signer }; // signer: Keypair dalam skrip, atau LaneSigner di aplikasi wallet
if (!(await isSyntheticProvisioned(connection, programId, synthetic))) {
  await submitRomeTxV1(deps, buildCreatePdaCall(synthetic)); // satu create_pda
}
```

## Sekali saja: berikan hak pada wrapper

Pembungkus wUSDC memindahkan token pengguna sebagai **delegate SPL** dari akun token mereka, jadi setiap pengguna harus memberikan hak itu sekali per pembungkus, sebelum deposit pertama. Ini berlaku sama untuk pengguna MetaMask dan alamat synthetic pengguna Solana. Tanpa izin itu, setiap `transferFrom` keluar dari wallet gagal dan deposit dibatalkan dengan error SPL `0x4` (ketidaksesuaian owner).

Periksa dengan view pembungkus `isEnabled(user)`. Jika mengembalikan `false`, kirim `approve_spl(wrapper, 2^64-1, mint)` langsung ke precompile Helper di `0xff00000000000000000000000000000000000009`; mint-nya adalah `mint_id()` pada pembungkus. Pada jalur EVM itu adalah sebuah `submitRomeTx` panggilan, pada jalur Solana sebuah `submitRomeTxV1` panggilan:

```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) }); // klien viem pada RPC chain

// who: alamat MetaMask, atau syntheticAddress(wallet.publicKey) untuk pengguna Solana
let grant = null; // tetap null ketika isEnabled bernilai true
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] }) };
}
// Jalur EVM:    jika (grant) await submitRomeTx(provider, { from: who, ...grant });
// Jalur Solana: jika (grant) await submitRomeTxV1(deps, grant);
```

## Apa yang dibutuhkan masing-masing sisi

|                             | Membutuhkan                                                                    | Mengapa                                                                                                                             |
| --------------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| **Pengguna Solana**         | **SOL** (sedikit)                                                              | membayar biaya transaksi Solana pada setiap tx jalur                                                                                |
|                             | **USDC sebagai token SPL** di wallet mereka                                    | nilai yang mereka depositkan (terlihat di sisi EVM sebagai `wUSDC`)                                                                 |
| **Pengguna EVM (MetaMask)** | **USDC** sebagai saldo gas Rome mereka                                         | gas, dan nilai yang mereka depositkan setelah dibungkus; mereka menambahnya dengan **menjembatani USDC** ke Rome (tidak ada faucet) |
|                             | sebuah **bungkus** dari sebagian gas itu menjadi **wUSDC** (`withdraw_to_ata`) | vault mengambil wUSDC, pembungkus ERC-20, bukan gas native. Saldo gas USDC di MetaMask belum menjadi wUSDC sampai dibungkus         |
|                             | satu kali **izin pembungkus** (`approve_spl`)                                  | membuat pembungkus wUSDC dapat memindahkan token mereka; lihat [Sekali saja: berikan hak pada wrapper](#one-time-grant-the-wrapper) |
| **Anda (pembuat)**          | sebuah wallet dengan **USDC** gas di Rome                                      | untuk men-deploy kontrak                                                                                                            |

## Nilai MASUK — deposit (langkah demi langkah)

Pengguna Solana memiliki SOL + USDC di wallet Solana mereka. Setiap langkah adalah transaksi Solana v1, **ditandatangani oleh wallet itu**, dan aplikasi mengirimnya ke Solana RPC. Bagian pendanaan dibangun di aplikasi dan wallet menandatanganinya dengan satu prompt; setiap panggilan kontrak disusun oleh proxy, dan wallet menandatanganinya setelah pesan otorisasi yang mudah dibaca:

1. **Bagian pendanaan** — `buildFundLeg(...)` → `submitSolanaInstructionsV1(...)`. Membuat akun token USDC milik synthetic (jika diperlukan) dan menjalankan **`ActivateAta`**, memindahkan `jumlah` USDC dari akun token wallet **ke**. Sekarang `wUSDC.balanceOf(synthetic)` menampilkan saldo itu.
2. **Berikan izin pada pembungkus** (ketika `tetap` adalah false) — `submitRomeTxV1(deps, { to: HELPER, data: approve_spl(wUSDC, 2^64-1, mint) })`. Lihat [Sekali saja: berikan hak pada wrapper](#one-time-grant-the-wrapper). *(Provision synthetic sebelum ini jika ini adalah panggilan jalur pertama pengguna.)*
3. **Setujui** — `submitRomeTxV1(deps, { to: wUSDC, data: approve(vault, amount) })`. Memungkinkan vault menarik token.
4. **Setoran** — `submitRomeTxV1(deps, { to: vault, data: deposit(amount) })`. Vault menjalankan `transferFrom(synthetic, vault, amount)` — USDC berpindah dari synthetic ke vault, dan dikreditkan ke alamat synthetic.

**Efek bersih:** USDC berpindah **wallet 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) atau Keypair

// 1) bagian pendanaan — USDC wallet → akun token synthetic (wallet menandatangani)
await submitSolanaInstructionsV1(
  buildFundLeg({ programId, chainId, mint: usdcMint, amount: depositAmount, wallet: wallet.publicKey, synthetic }),
  { connection, signer },
);

// 2) berikan izin pada pembungkus, sekali (synthetic sudah diprovisioning); `grant` adalah panggilan yang dibuat di bawah "Satu kali: Berikan izin pada pembungkus", atau null ketika isEnabled bernilai true
if (grant) await submitRomeTxV1(deps, grant);

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

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

## Nilai KELUAR — withdraw (langkah demi langkah)

Sekarang pengguna menarik dana. Juga ditandatangani wallet, dua prompt per panggilan:

1. **Withdraw** — `submitRomeTxV1(deps, { to: vault, data: withdraw(amount) })`. `vault.withdraw` menjalankan `transfer(synthetic, amount)` — USDC berpindah dari vault kembali ke **milik synthetic** akun token.
2. **Bagian sweep** — `buildSweepLeg(...)` memberi Anda `HelperProgram.transfer_spl` panggilan + akun; jalankan (buat akun token wallet jika diperlukan dengan `submitSolanaInstructionsV1`, lalu kirim panggilan Helper melalui `submitRomeTxV1`; proxy menemukan akun token) untuk memindahkan USDC dari synthetic **kembali ke wallet Solana milik pengguna**Synthetic menjadi nol.

**Efek bersih:** USDC berpindah **vault → (synthetic) → wallet Solana milik pengguna.** Tidak ada yang tertinggal.

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

// 1) withdraw — vault mengembalikan USDC ke akun token synthetic
await submitRomeTxV1(deps, { to: vault, data: encodeFunctionData({ abi, functionName: "withdraw", args: [amount] }) });

// 2) sweep leg — akun token synthetic → akun token wallet milik pengguna
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 });
```

## Diukur pada Hadrian

Setiap langkah jalur Solana di bawah ini dijalankan sebagai satu transaksi versi-1 di Hadrian:

| Langkah                           | Unit komputasi | Byte  | Akun |
| --------------------------------- | -------------- | ----- | ---- |
| Provision (`create_pda`)          | 39,008         | 703   | 9    |
| Pemberian wrapper (`approve_spl`) | 50,687         | 899   | 13   |
| Setujui                           | 79,335         | 769   | 10   |
| Deposit vault                     | 242,880        | 1,070 | 20   |
| Penarikan vault                   | 280,903        | 1,037 | 19   |
| Sweep (`transfer_spl`)            | 46,810         | 899   | 13   |

## Hal-hal yang perlu diwaspadai

Inilah hal-hal yang sering salah pada transaksi jalur Solana yang dibuat manual. SDK dan composer milik proxy menangani sebagian besar di antaranya:

* **Provisioning.** Akun synthetic yang baru harus dibuat (`create_pda`) sebelum panggilan pemindah nilai apa pun, atau ia tidak dapat menandatangani transfer. `submitRomeTxV1` tidak melakukannya untuk Anda: periksa `isSyntheticProvisioned` dan kirim `buildCreatePdaCall(synthetic)` sekali.
* **Berikan izin pada pembungkus sekali.** Pembungkus memindahkan token sebagai delegasi SPL pengguna. Periksa `tetap` dan kirim `approve_spl` ke precompile Helper sebelum deposit pertama, atau deposit dibatalkan dengan error SPL `0x4`.
* **Gunakan pembungkus, bukan `msg.value`.** Saldo pengguna Solana adalah akun token SPL mereka, ditampilkan sebagai pembungkus ERC-20 — pindahkan dengan `transfer` / `transferFrom`, jangan pernah nilai native.
* **Disusun untuk Anda.** ComputeBudget, treasury wallet, dan penemuan akun dilakukan oleh composer proxy, jadi semuanya tidak muncul di kode Anda. Sebelum wallet menandatangani, SDK memeriksa transaksi yang disusun dan menolak apa pun selain panggilan yang diotorisasi pengguna.
* **Ke mana dikirim.** Wallet menandatangani transaksi v1 dan **aplikasi mengirimnya ke Solana RPC** — bukan ke proxy, yang menyusunnya (panggilan yang direlay adalah satu pengecualian di bawah; lihat [Panggilan yang direlai dan panggilan panjang](/id/panduan-pengembang/call-evm-from-solana.md#relayed-calls-and-long-calls)). On-chain, program menurunkan `msg.sender` dari penanda tangan Solana.
* **Dua prompt per panggilan.** Wallet menandatangani pesan otorisasi yang mudah dibaca (`otorisasi Rome` dan hash transaksi EVM), lalu transaksi, keduanya dengan kunci yang sama.
* **Periksa apakah wallet Anda menandatangani v1.** `signV1Wire` harus mengembalikan tanda tangan 64 byte atas pesan v1; melalui Wallet Standard itu adalah `solana:signTransaction` pada wire mentah dan `signedTransaction.slice(-64)`. Tidak semua wallet dapat mendekode transaksi v1, jadi uji wallet Anda sebelum membangunnya. Di devnet saat ini, Solflare menandatanganinya; Phantom belum.
* **Panggilan besar direlay.** Panggilan yang tidak muat dalam satu transaksi direlay oleh proxy dengan tanda tangan otorisasi yang sama, dan hasilnya memiliki `route: "relay"`. Panggilan yang direlay dibayar dengan gas native chain, jadi top up synthetic dengan `fundSyntheticGas(deps, amountWei)`. Lihat [Panggilan yang direlai dan panggilan panjang](/id/panduan-pengembang/call-evm-from-solana.md#relayed-calls-and-long-calls).
* **Gas adalah USDC.** Tidak ada faucet — jembatani USDC masuk (lihat [Mendapatkan dana](/id/sumber-daya/faucets.md)).

Yang sebelumnya `submitRomeTxSolanaLane` masih ada di SDK: ini membangun transaksi Solana legacy di klien (satu prompt wallet, dengan ComputeBudget dan treasury wallet ditangani oleh SDK).

## Aplikasi yang sama dari MetaMask

Pengguna EVM memanggil kontrak yang sama persis dengan `submitRomeTx` — tooling EVM standar, gas dalam USDC. Satu perbedaan dari pengguna Solana: pengguna MetaMask memegang **gas native USDC**, bukan wUSDC. Vault mengambil pembungkus wUSDC, jadi pengguna membungkus sebagian gas mereka terlebih dahulu, lalu memberi izin pada pembungkus sekali, lalu melakukan langkah ERC-20 seperti biasa. Tidak ada bagian pendanaan atau sweep, karena token yang sudah dibungkus memang sudah berada di alamat EVM mereka.

1. **Bungkus gas menjadi wUSDC** — kirim `withdraw_to_ata(wei)` ke precompile Withdraw di `0x4200000000000000000000000000000000000016`. Ini membakar sebanyak itu gas native dan mengkreditkan jumlah yang setara ke akun token pengguna dalam token gas, yang `wUSDC.balanceOf(user)` kemudian ditampilkan. `wei` berada dalam unit gas 18 desimal dan harus kelipatan 10^12 (satu unit wUSDC 6 desimal), atau panggilan dibatalkan. Lewati langkah ini jika pengguna sudah memegang wUSDC.
2. **Berikan izin pada pembungkus** (ketika `tetap` adalah false) — panggilan yang sama `approve_spl` seperti di atas, dikirim dengan `submitRomeTx`. Lihat [Sekali saja: berikan hak pada wrapper](#one-time-grant-the-wrapper).
3. **Setujui** — `approve(vault, amount)` pada wUSDC.
4. **Setoran** — `deposit(amount)` pada vault. Withdraw adalah `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) bungkus sebagian gas USDC menjadi wUSDC (weiAmount berada dalam unit gas 18 desimal)
await submitRomeTx(provider, { from: who, to: WITHDRAW, data: encodeFunctionData({ abi: withdrawAbi, functionName: "withdraw_to_ata", args: [weiAmount] }) });

// 2) berikan izin pada pembungkus, sekali: `grant` adalah panggilan yang dibuat di bawah "Satu kali: Berikan izin pada pembungkus", atau null
if (grant) await submitRomeTx(provider, { from: who, ...grant });

// 3) setujui dan 4) deposit, seperti pada ERC-20 mana pun
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] }) });
```

Kedua pengguna berbagi `balanceOf` state yang sama di vault.

## Berikutnya

* [Call EVM from Solana](/id/panduan-pengembang/call-evm-from-solana.md) — mekanisme jalur Solana secara detail
* [Call Solana from EVM](/id/panduan-pengembang/call-solana-from-evm.md) — arah sebaliknya (CPI)
* [Mendapatkan dana](/id/sumber-daya/faucets.md) — USDC sebagai token gas; jembatani masuk


---

# 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/id/panduan-pengembang/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.
