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

# Eine Dual-Lane-App bauen

Eine Schritt-für-Schritt-Anleitung für eine Dual-Lane-App — ein Solidity-Vertrag, der sowohl von einem MetaMask-Nutzer als auch von einem Solana-Wallet-Nutzer verwendet wird, mit genau dem, was bei jedem Schritt passiert.

Eine **Dual-Lane-App** ist ein Solidity-Contract, den sowohl ein **MetaMask (EVM)** Benutzer als auch ein **Solana-Wallet** direkt verwenden — derselbe Contract, derselbe Zustand, jeweils mit der Wallet, die sie bereits haben. Diese Seite führt durch *genau* was in jedem Schritt auf jeder Seite passiert.

Das Beispiel ist ein kleiner **Tresor**: Du `deposit` USDC einzahlst und später `withdraw` ihn abhebst. „Stake / unstake“, „supply / redeem“, „tip / claim“ sind dieselbe Form.

## Was du schreibst — ein Standard-ERC-20-Tresor

Auf Rome erscheint das USDC eines Solana-Nutzers auf der EVM-Seite als gewöhnlicher **ERC-20-Token** — der SPL-Wrapper für dieses Mint (z. B. `wUSDC`). Dein Contract ist also ein normaler Token-Tresor: Er zieht Tokens mit `transferFrom` und gibt sie mit `transfer`zurück. Nichts Rome-spezifisches:

```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;                 // der wUSDC-Wrapper
    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));
    }
}
```

> **Warum ERC-20, nicht `zahlbar` / `msg.value`?** Das ausgebbare Guthaben eines Solana-Nutzers ist ihr **SPL-Token-Konto der Wallet**— 1:1 als dieser ERC-20-Wrapper abgebildet — *nicht* das native EVM-Guthaben. Ein Solana-Nutzer kann also immer einen `transferFrom`finanzieren, aber ein `deposit() payable` würde native Werte benötigen, die sie im Ruhezustand nicht halten. Bilde alles um den Token herum, dann funktionieren beide Lanes gleich.

Deploye es mit Foundry oder Hardhat und zeige im Konstruktor auf die Wrapper-Adresse für deinen Token (aus dem [Registry](https://github.com/rome-protocol/rome-registry)). Auf Rome ist der Gas-Token USDC, daher brauchst du etwas USDC-Gasguthaben zum Deployen.

## Die beiden Lanes

| Lane       | Wallet                                                 | Wie die App es aufruft                                                                    |
| ---------- | ------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
| **EVM**    | MetaMask (ein EVM-Schlüssel)                           | `submitRomeTx` — der Standard-Rome-Schreibzugriff                                         |
| **Solana** | eine Solana-Wallet wie Solflare (ein Solana-Schlüssel) | `submitRomeTxV1` — eine Solana-v1-Transaktion pro Aufruf, kein EVM-Schlüssel erforderlich |

Die EVM-Lane ist gewöhnlich. Der Rest dieser Seite ist die **Solana-Pfad** — die interessante Hälfte.

## Die Kernidee: Das Synthetic ist ein Durchlauf

Die EVM-Identität eines Solana-Nutzers ist ihre **synthetische Adresse** — `keccak256(solana_pubkey)[12:]`. Es ist ihre `msg.sender` im Contract, aber **es hält im Ruhezustand nichts.** Das Geld des Nutzers liegt in ihrem **Solana-Wallet** (als SPL-USDC), und auf der EVM-Seite ist genau dieses Guthaben das, was `wUSDC.balanceOf(synthetic)` ausliest. Wert fließt *durch* durch das Synthetic:

* **Eingang** (Einzahlung): Wallet-Token-Konto → Synthetic-Token-Konto → Contract (über `transferFrom`).
* **Ausgang** (Abhebung): Contract → Synthetic-Token-Konto → Wallet-Token-Konto.

Das Synthetic gleicht sich nach jedem Hin- und Rückweg wieder auf null aus.

## Einmalig: Aktivieren (das Synthetic bereitstellen)

Das On-Chain-Konto eines brandneuen Synthetic existiert nicht, bis du es erstellst. Wenn ein Solana-Nutzer zum ersten Mal handelt, ist sein Synthetic **bereitgestellt** mit einem `create_pda` Aufruf — danach können wertbewegende Aufrufe (der ERC-20 `transferFrom`der Sweep) von ihm signiert werden.

`submitRomeTxV1` macht das nicht für dich, also stelle es ausdrücklich und genau einmal vor dem ersten wertbewegenden Aufruf bereit. Ein guter Ort ist ein „Aktivieren“-Schritt oder direkt vor dem ersten Approve:

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

const synthetic = syntheticAddress(wallet.publicKey);
const deps = { connection, proxyUrl, chainId, programId, signer }; // signer: ein Keypair in Skripten, ein LaneSigner in einer Wallet-App
if (!(await isSyntheticProvisioned(connection, programId, synthetic))) {
  await submitRomeTxV1(deps, buildCreatePdaCall(synthetic)); // ein create_pda
}
```

## Einmalig: dem Wrapper die Erlaubnis geben

Der wUSDC-Wrapper bewegt die Tokens eines Nutzers als der **SPL-Delegate** ihres Token-Kontos, daher muss jeder Nutzer ihm dieses Recht einmal pro Wrapper vor der ersten Einzahlung gewähren. Das gilt gleichermaßen für einen MetaMask-Nutzer und für die Synthetic-Adresse eines Solana-Nutzers. Ohne die Freigabe schlägt jeder `transferFrom` Transfer aus der Wallet fehl und die Einzahlung schlägt mit SPL-Fehler `0x4` (Besitzer stimmt nicht überein).

Prüfe mit der View-Funktion des Wrappers `isEnabled(user)`. Wenn sie `false`zurückgibt, sende `approve_spl(wrapper, 2^64-1, mint)` direkt an das Helper-Precompile bei `0xff00000000000000000000000000000000000009`; das Mint ist `mint_id()` auf dem Wrapper. Auf der EVM-Lane ist das ein `submitRomeTx` Aufruf, auf der Solana-Lane ein `submitRomeTxV1` Aufruf:

```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) }); // ein viem-Client auf dem RPC der Chain

// wer: die MetaMask-Adresse oder syntheticAddress(wallet.publicKey) für einen Solana-Nutzer
let grant = null; // bleibt null, wenn isEnabled true ist
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:    wenn (grant) await submitRomeTx(provider, { from: who, ...grant });
// Solana-Lane: wenn (grant) await submitRomeTxV1(deps, grant);
```

## Was jede Seite braucht

|                           | Benötigt                                                                  | Warum                                                                                                                                         |
| ------------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| **Solana-Nutzer**         | **SOL** (ein wenig)                                                       | bezahlt die Solana-Transaktionsgebühr für jede Lane-Transaktion                                                                               |
|                           | **USDC als SPL-Token** in ihrer Wallet                                    | der Wert, den sie einzahlen (auf der EVM-Seite sichtbar als `wUSDC`)                                                                          |
| **EVM-Nutzer (MetaMask)** | **USDC** als ihr Rome-Gasguthaben                                         | Gas, und den Wert, den sie einzahlen, sobald er gewrappt ist; sie füllen ihn auf durch **Bridgen von USDC** nach Rome (es gibt keinen Faucet) |
|                           | ein **Wrappen** eines Teils dieses Gases in **wUSDC** (`withdraw_to_ata`) | Der Tresor nimmt wUSDC, den ERC-20-Wrapper, nicht natives Gas. Ein MetaMask-Guthaben an Gas-USDC ist erst dann wUSDC, wenn es gewrappt wurde  |
|                           | die einmalige **Wrapper-Freigabe** (`approve_spl`)                        | erlaubt dem wUSDC-Wrapper, ihre Tokens zu bewegen; siehe [Einmalig: dem Wrapper die Erlaubnis geben](#one-time-grant-the-wrapper)             |
| **Du (der Builder)**      | eine Wallet mit **USDC** Gas auf Rome                                     | um den Contract zu deployen                                                                                                                   |

## Wert IN — Einzahlung (Schritt für Schritt)

Der Solana-Nutzer hat SOL + USDC in seiner Solana-Wallet. Jeder Schritt ist eine Solana-v1-Transaktion, **von dieser Wallet signiert**und die App sendet sie an die Solana-RPC. Der Finanzierungs-Leg wird in der App gebaut und die Wallet signiert ihn mit einer einzigen Aufforderung; jeder Contract-Aufruf wird vom Proxy zusammengesetzt, und die Wallet signiert ihn nach einer lesbaren Autorisierungsnachricht:

1. **Finanzierungs-Leg** — `buildFundLeg(...)` → `submitSolanaInstructionsV1(...)`. Erstellt das USDC-Token-Konto des Synthetic (falls nötig) und führt **`ActivateAta`**&#x61;us und verschiebt `Betrag` von USDC vom Token-Konto der Wallet **in das**. Jetzt `wUSDC.balanceOf(synthetic)` zeigt dieses Guthaben.
2. **Dem Wrapper die Freigabe erteilen** (wenn `isEnabled` falsch ist) — `submitRomeTxV1(deps, { to: HELPER, data: approve_spl(wUSDC, 2^64-1, mint) })`. Siehe [Einmalig: dem Wrapper die Erlaubnis geben](#one-time-grant-the-wrapper). *(Stelle das Synthetic vorher bereit, wenn dies der erste Lane-Aufruf des Nutzers ist.)*
3. **Genehmigen** — `submitRomeTxV1(deps, { to: wUSDC, data: approve(vault, amount) })`. Erlaubt dem Tresor, die Tokens abzurufen.
4. **Einzahlung** — `submitRomeTxV1(deps, { to: vault, data: deposit(amount) })`. Der Tresor führt `transferFrom(synthetic, vault, amount)` aus — das USDC bewegt sich vom Synthetic in den Tresor und wird der Adresse des Synthetic gutgeschrieben.

**Nettowirkung:** USDC ging **Solana-Wallet → (Synthetic) → der Tresor.**

```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: ein LaneSigner (payer, signAuthHash, signV1Wire) oder ein Keypair

// 1) Finanzierungs-Leg — Wallet-USDC → Synthetic-Token-Konto (die Wallet signiert)
await submitSolanaInstructionsV1(
  buildFundLeg({ programId, chainId, mint: usdcMint, amount: depositAmount, wallet: wallet.publicKey, synthetic }),
  { connection, signer },
);

// 2) dem Wrapper einmal die Freigabe erteilen (das Synthetic ist bereits bereitgestellt); `grant` ist der Aufruf aus „Einmalig: Dem Wrapper die Freigabe erteilen“ oder null, wenn isEnabled true ist
if (grant) await submitRomeTxV1(deps, grant);

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

// 4) einzahlen — der Tresor zieht per transferFrom
await submitRomeTxV1(deps, { to: vault, data: encodeFunctionData({ abi, functionName: "deposit", args: [depositAmount] }) });
```

## Wert AUS — Abhebung (Schritt für Schritt)

Nun hebt der Nutzer ab. Ebenfalls wallet-signiert, zwei Prompts pro Aufruf:

1. **Abheben** — `submitRomeTxV1(deps, { to: vault, data: withdraw(amount) })`. `vault.withdraw` führt `transfer(synthetic, amount)` aus — USDC bewegt sich vom Tresor zurück in das **Synthetic** Token-Konto.
2. **Sweep-Leg** — `buildSweepLeg(...)` gibt dir den `HelperProgram.transfer_spl` Aufruf + Konten; führe ihn aus (erstelle bei Bedarf das Token-Konto der Wallet mit `submitSolanaInstructionsV1`und sende dann den Helper-Aufruf durch `submitRomeTxV1`; der Proxy findet die Token-Konten) um das USDC vom Synthetic **zurück in die eigene Solana-Wallet des Nutzers**. Das Synthetic gleicht sich auf null aus.

**Nettowirkung:** USDC ging **der Tresor → (Synthetic) → die Solana-Wallet des Nutzers.** Nichts bleibt liegen.

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

// 1) abheben — der Tresor gibt USDC an das Token-Konto des Synthetic zurück
await submitRomeTxV1(deps, { to: vault, data: encodeFunctionData({ abi, functionName: "withdraw", args: [amount] }) });

// 2) Sweep-Leg — Token-Konto des Synthetic → Token-Konto der eigenen Wallet des Nutzers
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 });
```

## Gemessen auf Hadrian

Jeder der folgenden Solana-Lane-Schritte lief auf Hadrian als eine Version-1-Transaktion:

| Schritt                             | Compute-Einheiten | Bytes | Konten |
| ----------------------------------- | ----------------- | ----- | ------ |
| Bereitstellung (`create_pda`)       | 39,008            | 703   | 9      |
| Wrapper-Genehmigung (`approve_spl`) | 50,687            | 899   | 13     |
| Genehmigen                          | 79,335            | 769   | 10     |
| Tresor-Einzahlung                   | 242,880           | 1,070 | 20     |
| Tresor-Abhebung                     | 280,903           | 1,037 | 19     |
| Sweep (`transfer_spl`)              | 46,810            | 899   | 13     |

## Die Fallstricke

Das sind die Dinge, die eine von Hand gebaute Solana-Lane-Transaktion falsch macht. Das SDK und der Composer des Proxys übernehmen die meisten davon:

* **Bereitstellung.** Das Konto eines frischen Synthetic muss erstellt werden (`create_pda`) bevor irgendein wertbewegender Aufruf erfolgt, sonst kann es die Übertragung nicht signieren. `submitRomeTxV1` macht das nicht für dich: prüfe `isSyntheticProvisioned` und sende `buildCreatePdaCall(synthetic)` einmal.
* **Erteile dem Wrapper einmal die Freigabe.** Der Wrapper bewegt Tokens als SPL-Delegierter des Nutzers. Prüfe `isEnabled` und sende `approve_spl` an das Helper-Precompile vor der ersten Einzahlung, sonst wird die Einzahlung mit SPL-Fehler zurückgesetzt `0x4`.
* **Verwende den Wrapper, nicht `msg.value`.** Das Guthaben eines Solana-Nutzers ist ihr SPL-Token-Konto, dargestellt als ERC-20-Wrapper — bewege es mit `transfer` / `transferFrom`, niemals mit nativen Werten.
* **Für dich komponiert.** ComputeBudget, die Treasure-Wallet und die Account-Erkennung werden vom Composer des Proxys erledigt, daher tauchen sie in deinem Code nicht auf. Bevor die Wallet signiert, prüft das SDK die komponierte Transaktion und verweigert alles außer dem Aufruf, den der Nutzer autorisiert hat.
* **Wohin es gesendet wird.** Die Wallet signiert die v1-Transaktion und die **App sendet sie an die Solana-RPC** — nicht an den Proxy, der sie komponiert (ein relayed Aufruf ist die eine Ausnahme unten; siehe [Weitergeleitete Aufrufe und lange Aufrufe](/de/entwicklerleitfaden/call-evm-from-solana.md#relayed-calls-and-long-calls)). On-Chain leitet das Programm `msg.sender` aus dem Solana-Signer ab.
* **Zwei Prompts pro Aufruf.** Die Wallet signiert eine lesbare Autorisierungsnachricht (`Rome authorization` und den EVM-Transaktions-Hash), dann die Transaktion, beides mit demselben Schlüssel.
* **Prüfe, ob deine Wallet v1 signiert.** `signV1Wire` muss die 64-Byte-Signatur über die v1-Nachricht zurückgeben; über Wallet Standard ist das `solana:signTransaction` auf dem Roh-Wire und `signedTransaction.slice(-64)`. Nicht jede Wallet dekodiert v1-Transaktionen bereits, also teste deine, bevor du darauf aufbaust. Auf dem Devnet signiert Solflare sie heute; Phantom noch nicht.
* **Große Aufrufe werden relayed.** Ein Aufruf, der nicht in eine Transaktion passt, wird vom Proxy mit derselben Autorisierungs-Signatur weitergeleitet, und das Ergebnis hat `route: "relay"`. Ein relayed Aufruf wird mit dem nativen Gas der Chain bezahlt, also fülle das Synthetic mit `fundSyntheticGas(deps, amountWei)`. Siehe [Weitergeleitete Aufrufe und lange Aufrufe](/de/entwicklerleitfaden/call-evm-from-solana.md#relayed-calls-and-long-calls).
* **Gas ist USDC.** Kein Faucet — bridge USDC hinein (siehe [Finanzierung erhalten](/de/ressourcen/faucets.md)).

Das frühere `submitRomeTxSolanaLane` ist noch im SDK: Es baut im Client eine Legacy-Solana-Transaktion (ein Wallet-Prompt, mit ComputeBudget und der Treasure-Wallet, die vom SDK behandelt werden).

## Dieselbe App von MetaMask

Ein EVM-Nutzer ruft denselben Contract mit `submitRomeTx` — standardmäßige EVM-Tools, Gas in USDC. Ein Unterschied zum Solana-Nutzer: Ein MetaMask-Nutzer hält **natives Gas-USDC**, nicht wUSDC. Der Tresor nimmt den wUSDC-Wrapper, also wrapppt der Nutzer zuerst einen Teil seines Gases, erteilt dann einmal dem Wrapper die Freigabe und führt danach die ERC-20-Schritte wie gewohnt aus. Es gibt keine Fund- oder Sweep-Legs, weil die gewrappten Tokens bereits an ihrer EVM-Adresse liegen.

1. **Gas in wUSDC wrappen** — sende `withdraw_to_ata(wei)` an das Withdraw-Precompile bei `0x4200000000000000000000000000000000000016`. Es verbrennt diese Menge an nativen Gas und schreibt den entsprechenden Betrag dem Token-Konto des Nutzers im Gas-Token gut, das `wUSDC.balanceOf(user)` dann angezeigt wird. `wei` liegt in der 18-Dezimal-Gas-Einheit und muss ein Vielfaches von 10^12 sein (eine 6-Dezimal-wUSDC-Einheit), sonst schlägt der Aufruf fehl. Überspringe diesen Schritt, wenn der Nutzer bereits wUSDC hält.
2. **Dem Wrapper die Freigabe erteilen** (wenn `isEnabled` falsch ist) — derselbe `approve_spl` Aufruf wie oben, gesendet mit `submitRomeTx`. Siehe [Einmalig: dem Wrapper die Erlaubnis geben](#one-time-grant-the-wrapper).
3. **Genehmigen** — `approve(vault, amount)` auf wUSDC.
4. **Einzahlung** — `deposit(amount)` auf dem Tresor. Abheben ist `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) etwas Gas-USDC in wUSDC wrappen (weiAmount ist in 18-Dezimal-Gas-Einheiten)
await submitRomeTx(provider, { from: who, to: WITHDRAW, data: encodeFunctionData({ abi: withdrawAbi, functionName: "withdraw_to_ata", args: [weiAmount] }) });

// 2) dem Wrapper einmal die Freigabe erteilen: `grant` ist der unter „Einmalig: Dem Wrapper die Freigabe erteilen“ gebaute Aufruf oder null
if (grant) await submitRomeTx(provider, { from: who, ...grant });

// 3) genehmigen und 4) einzahlen, wie bei jedem 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] }) });
```

Beide Nutzer teilen sich denselben `balanceOf` Zustand im Tresor.

## Was als Nächstes kommt

* [EVM aus Solana aufrufen](/de/entwicklerleitfaden/call-evm-from-solana.md) — die Solana-Lane-Mechanik im Detail
* [Solana aus EVM aufrufen](/de/entwicklerleitfaden/call-solana-from-evm.md) — die andere Richtung (CPI)
* [Finanzierung erhalten](/de/ressourcen/faucets.md) — USDC als Gas-Token; bridge es hinein


---

# 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/de/entwicklerleitfaden/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.
