> 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/es/guias-para-desarrolladores/dual-lane-app.md).

# Construir una app de doble carril

Un recorrido paso a paso de una aplicación de doble carril: un contrato Solidity usado tanto por un usuario de MetaMask como por un usuario con cartera de Solana, con exactamente lo que ocurre en cada paso.

Una app **aplicación de doble carril** es un contrato de Solidity que tanto un **MetaMask (EVM)** usuario y un **wallet de Solana** usuario usan directamente — mismo contrato, mismo estado, cada uno con la wallet que ya tiene. Esta página recorre *exactamente* qué ocurre en cada paso, en cada lado.

El ejemplo es una pequeña **bóveda**: tú `deposit` USDC y luego `withdraw` lo. "Stake / unstake", "supply / redeem", "tip / claim" tienen la misma estructura.

## Lo que escribes — una bóveda ERC-20 estándar

En Rome, el USDC de un usuario de Solana aparece en el lado EVM como un **token ERC-20** — el wrapper SPL para ese mint (p. ej., `wUSDC`). Así que tu contrato es una bóveda normal de tokens: extrae tokens con `transferFrom` y los devuelve con `transfer`. Nada específico de 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;                 // el wrapper 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));
    }
}
```

> **¿Por qué ERC-20 y no `payable` / `msg.value`?** El saldo gastable de un usuario de Solana es su **cuenta de token SPL de su wallet**, expuesta 1:1 como este wrapper ERC-20 — *no* el saldo nativo de EVM. Así que un usuario de Solana siempre puede financiar un `transferFrom`, pero un `deposit() payable` necesitaría valor nativo que no poseen en reposo. Construye alrededor del token y ambos carriles funcionarán igual.

Desplígalo con Foundry o Hardhat, apuntando el constructor a la dirección del wrapper de tu token (desde el [registro](https://github.com/rome-protocol/rome-registry)). En Rome el token de gas es USDC, así que necesitas un poco de saldo de gas en USDC para desplegar.

## Los dos carriles

| Carril     | Billetera                                                | Cómo lo llama la app                                                               |
| ---------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| **EVM**    | MetaMask (una clave EVM)                                 | `submitRomeTx` — la escritura estándar de Rome                                     |
| **Solana** | una wallet de Solana como Solflare (una clave de Solana) | `submitRomeTxV1` — una transacción Solana v1 por llamada, no se necesita clave EVM |

El carril EVM es normal. El resto de esta página es el **vía Solana** — la mitad interesante.

## La idea clave: el sintético es un conducto

La identidad EVM de un usuario de Solana es su **dirección sintética** — `keccak256(solana_pubkey)[12:]`. Es su `msg.sender` en el contrato, pero **no guarda nada en reposo.** El dinero del usuario vive en su **wallet de Solana** (como USDC SPL), y en el lado EVM ese mismo saldo es lo que `wUSDC.balanceOf(synthetic)` lee. El valor fluye *a través de* a través del sintético:

* **En** (depósito): cuenta de token de la wallet → cuenta de token del sintético → contrato (vía `transferFrom`).
* **Salida** (retirada): contrato → cuenta de token del sintético → cuenta de token de la wallet.

El sintético vuelve a cero después de cada ida y vuelta.

## Una vez: activar (aprovisionar el sintético)

La cuenta en cadena de un sintético nuevo no existe hasta que la creas. La primera vez que un usuario de Solana actúa, su sintético está **aprovisionado** con una `create_pda` llamada — después de eso, las llamadas que mueven valor (el ERC-20 `transferFrom`, el sweep) pueden ser firmadas por él.

`submitRomeTxV1` no hace esto por ti, así que aprovisiona explícitamente, una vez, antes de la primera llamada que mueve valor. Un buen lugar es un paso de "Activar", o justo antes del primer approve:

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

const synthetic = syntheticAddress(wallet.publicKey);
const deps = { connection, proxyUrl, chainId, programId, signer }; // signer: a Keypair in scripts, a LaneSigner in a wallet app
if (!(await isSyntheticProvisioned(connection, programId, synthetic))) {
  await submitRomeTxV1(deps, buildCreatePdaCall(synthetic)); // one create_pda
}
```

## Una sola vez: concede al wrapper

El wrapper wUSDC mueve los tokens de un usuario como el **delegado SPL** de su cuenta de token, así que cada usuario tiene que concederle ese derecho una vez por wrapper, antes del primer depósito. Esto aplica tanto a un usuario de MetaMask como a la dirección sintética de un usuario de Solana. Sin la concesión, cada `transferFrom` salida de la wallet falla y el depósito revierte con error SPL `0x4` (discordancia de propietario).

Compruébalo con la vista del wrapper `isEnabled(user)`. Si devuelve `false`, envía `approve_spl(wrapper, 2^64-1, mint)` directamente al precompile Helper en `0xff00000000000000000000000000000000000009`; el mint es `mint_id()` en el wrapper. En el carril EVM eso es una `submitRomeTx` llamada, en el carril Solana una `submitRomeTxV1` llamada:

```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) }); // a viem client on the chain's RPC

// who: the MetaMask address, or syntheticAddress(wallet.publicKey) for a Solana user
let grant = null; // stays null when isEnabled is 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] }) };
}
// EVM lane:    if (grant) await submitRomeTx(provider, { from: who, ...grant });
// Solana lane: if (grant) await submitRomeTxV1(deps, grant);
```

## Lo que necesita cada lado

|                            | Necesita                                                         | Por qué                                                                                                                       |
| -------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Usuario de Solana**      | **SOL** (un poco)                                                | paga la tarifa de transacción de Solana en cada tx del carril                                                                 |
|                            | **USDC como token SPL** en su wallet                             | el valor que depositan (visto en el lado EVM como `wUSDC`)                                                                    |
| **Usuario EVM (MetaMask)** | **USDC** como su saldo de gas en Rome                            | gas, y el valor que depositan una vez envuelto; lo recargan mediante **el puente de USDC** hacia Rome (no hay faucet)         |
|                            | un **wrap** de parte de ese gas en **wUSDC** (`withdraw_to_ata`) | la bóveda toma wUSDC, el wrapper ERC-20, no gas nativo. Un saldo de USDC de gas en MetaMask no es wUSDC hasta que se envuelve |
|                            | la concesión única **del wrapper** (`approve_spl`)               | permite que el wrapper wUSDC mueva sus tokens; véase [Una sola vez: concede al wrapper](#one-time-grant-the-wrapper)          |
| **Tú (el desarrollador)**  | una wallet con **USDC** gas en Rome                              | para desplegar el contrato                                                                                                    |

## Valor ENTRANTE — depósito (paso a paso)

El usuario de Solana tiene SOL + USDC en su wallet de Solana. Cada paso es una transacción Solana v1, **firmada por esa wallet**, y la app la envía al RPC de Solana. La pata de financiación se construye en la app y la wallet la firma con un solo aviso; cada llamada al contrato la compone el proxy, y la wallet la firma después de un mensaje de autorización legible:

1. **Pata de financiación** — `buildFundLeg(...)` → `submitSolanaInstructionsV1(...)`. Crea la cuenta de token USDC del sintético (si es necesario) y ejecuta **`ActivateAta`**, moviendo `amount` de USDC desde la cuenta de token de la wallet **hacia la**. Ahora `wUSDC.balanceOf(synthetic)` muestra ese saldo.
2. **Concede el wrapper** (cuando `isEnabled` es falso) — `submitRomeTxV1(deps, { to: HELPER, data: approve_spl(wUSDC, 2^64-1, mint) })`. Véase [Una sola vez: concede al wrapper](#one-time-grant-the-wrapper). *(Aprovisiona el sintético antes de esto si es la primera llamada del usuario en el carril.)*
3. **Aprobar** — `submitRomeTxV1(deps, { to: wUSDC, data: approve(vault, amount) })`. Permite que la bóveda extraiga los tokens.
4. **Depósito** — `submitRomeTxV1(deps, { to: vault, data: deposit(amount) })`. La bóveda ejecuta `transferFrom(synthetic, vault, amount)` — el USDC se mueve del sintético a la bóveda, acreditado a la dirección del sintético.

**Efecto neto:** El USDC fue **wallet de Solana → (sintético) → la bóveda.**

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

// 1) pata de financiación — USDC de la wallet → cuenta de token del sintético (la wallet firma)
await submitSolanaInstructionsV1(
  buildFundLeg({ programId, chainId, mint: usdcMint, amount: depositAmount, wallet: wallet.publicKey, synthetic }),
  { connection, signer },
);

// 2) concede el wrapper, una vez (el sintético ya está aprovisionado); `grant` es la llamada creada bajo "Una vez: concede el wrapper", o null cuando isEnabled es true
if (grant) await submitRomeTxV1(deps, grant);

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

// 4) depositar — la bóveda extrae mediante transferFrom
await submitRomeTxV1(deps, { to: vault, data: encodeFunctionData({ abi, functionName: "deposit", args: [depositAmount] }) });
```

## Valor DE SALIDA — retirada (paso a paso)

Ahora el usuario retira. También firmado por la wallet, dos avisos por llamada:

1. **Withdraw** — `submitRomeTxV1(deps, { to: vault, data: withdraw(amount) })`. `vault.withdraw` ejecuta `transfer(synthetic, amount)` — el USDC se mueve de la bóveda de vuelta a la **del sintético** cuenta de token.
2. **Pata de barrido** — `buildSweepLeg(...)` te da la `HelperProgram.transfer_spl` llamada + cuentas; ejecútala (crea la cuenta de token de la wallet si es necesario con `submitSolanaInstructionsV1`, luego envía la llamada al Helper a través de `submitRomeTxV1`; el proxy encuentra las cuentas de token) para mover el USDC desde el sintético **de vuelta a la propia wallet de Solana del usuario**. El sintético vuelve a cero.

**Efecto neto:** El USDC fue **la bóveda → (sintético) → la wallet de Solana del usuario.** Nada queda varado.

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

// 1) retirar — la bóveda devuelve USDC a la cuenta de token del sintético
await submitRomeTxV1(deps, { to: vault, data: encodeFunctionData({ abi, functionName: "withdraw", args: [amount] }) });

// 2) pata de barrido — cuenta de token del sintético → cuenta de token de la propia wallet del usuario
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 });
```

## Medido en Hadrian

Cada paso del carril Solana a continuación se ejecutó como una transacción versión 1 en Hadrian:

| Paso                                  | Unidades de cómputo | Bytes | Cuentas |
| ------------------------------------- | ------------------- | ----- | ------- |
| Aprovisionamiento (`create_pda`)      | 39,008              | 703   | 9       |
| Concesión del wrapper (`approve_spl`) | 50,687              | 899   | 13      |
| Aprobar                               | 79,335              | 769   | 10      |
| Depósito en bóveda                    | 242,880             | 1,070 | 20      |
| Retirada de bóveda                    | 280,903             | 1,037 | 19      |
| Barrido (`transfer_spl`)              | 46,810              | 899   | 13      |

## Los detalles importantes

Estas son las cosas que una transacción del carril Solana hecha a mano hace mal. El SDK y el compositor del proxy se encargan de la mayoría:

* **Aprovisionamiento.** La cuenta de un sintético nuevo debe crearse (`create_pda`) antes de cualquier llamada que mueva valor, o no podrá firmar la transferencia. `submitRomeTxV1` no lo hace por ti: comprueba `isSyntheticProvisioned` y envía `buildCreatePdaCall(synthetic)` una vez.
* **Concede el wrapper una vez.** El wrapper mueve tokens como delegado SPL del usuario. Comprueba `isEnabled` y envía `approve_spl` al precompile Helper antes del primer depósito, o el depósito revierte con error SPL `0x4`.
* **Gasta el wrapper, no `msg.value`.** El saldo de un usuario de Solana es su cuenta de token SPL, expuesta como el wrapper ERC-20 — muévelo con `transfer` / `transferFrom`, nunca valor nativo.
* **Compuesto para ti.** ComputeBudget, la wallet del tesoro y la detección de cuentas las hace el compositor del proxy, así que ninguna aparece en tu código. Antes de que la wallet firme, el SDK verifica la transacción compuesta y rechaza cualquier cosa que no sea la llamada que el usuario autorizó.
* **A dónde se envía.** La wallet firma la transacción v1 y la **app la envía al RPC de Solana** — no al proxy, que la compone (una llamada retransmitida es la única excepción, abajo; véase [Llamadas reenviadas y llamadas largas](/es/guias-para-desarrolladores/call-evm-from-solana.md#relayed-calls-and-long-calls)). En cadena, el programa deriva `msg.sender` a partir del firmante de Solana.
* **Dos avisos por llamada.** La wallet firma un mensaje de autorización legible (`autorización de Rome` y el hash de la transacción EVM), y luego la transacción, ambos con la misma clave.
* **Comprueba que tu wallet firma v1.** `signV1Wire` debe devolver la firma de 64 bytes sobre el mensaje v1; mediante Wallet Standard eso es `solana:signTransaction` en el wire en bruto y `signedTransaction.slice(-64)`. No todas las wallets decodifican transacciones v1 todavía, así que prueba la tuya antes de construir sobre ello. Hoy en devnet, Solflare las firma; Phantom aún no.
* **Las llamadas grandes se retransmiten.** Una llamada que no cabe en una transacción es retransmitida por el proxy con la misma firma de autorización, y el resultado tiene `route: "relay"`. Una llamada retransmitida se paga con el gas nativo de la cadena, así que recarga el sintético con `fundSyntheticGas(deps, amountWei)`. Véase [Llamadas reenviadas y llamadas largas](/es/guias-para-desarrolladores/call-evm-from-solana.md#relayed-calls-and-long-calls).
* **El gas es USDC.** No hay faucet — puentea USDC hacia dentro (véase [Obtener financiación](/es/recursos/faucets.md)).

La anterior `submitRomeTxSolanaLane` sigue en el SDK: construye una transacción Solana heredada en el cliente (un aviso de la wallet, con ComputeBudget y la wallet del tesoro gestionados por el SDK).

## La misma app desde MetaMask

Un usuario EVM llama al mismo contrato con `submitRomeTx` — herramientas EVM estándar, gas en USDC. Una diferencia respecto al usuario de Solana: un usuario de MetaMask posee **USDC de gas nativo**, no wUSDC. La bóveda toma el wrapper wUSDC, así que el usuario primero envuelve parte de su gas, luego concede el wrapper una vez, y luego hace los pasos ERC-20 como de costumbre. No hay patas de financiación ni de barrido, porque los tokens envueltos ya viven en su dirección EVM.

1. **Envuelve el gas en wUSDC** — envía `withdraw_to_ata(wei)` al precompile Withdraw en `0x4200000000000000000000000000000000000016`. Quema esa cantidad de gas nativo y acredita la cantidad equivalente a la cuenta de token del usuario en el token de gas, lo que `wUSDC.balanceOf(user)` luego se muestra. `wei` está en la unidad de gas de 18 decimales y debe ser múltiplo de 10^12 (una unidad de wUSDC de 6 decimales), o la llamada revierte. Omite este paso si el usuario ya tiene wUSDC.
2. **Concede el wrapper** (cuando `isEnabled` es falso) — la misma `approve_spl` llamada que arriba, enviada con `submitRomeTx`. Véase [Una sola vez: concede al wrapper](#one-time-grant-the-wrapper).
3. **Aprobar** — `approve(vault, amount)` en wUSDC.
4. **Depósito** — `deposit(amount)` en la bóveda. Withdraw es `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) envuelve algo de USDC de gas en wUSDC (weiAmount está en unidades de gas de 18 decimales)
await submitRomeTx(provider, { from: who, to: WITHDRAW, data: encodeFunctionData({ abi: withdrawAbi, functionName: "withdraw_to_ata", args: [weiAmount] }) });

// 2) concede el wrapper, una vez: `grant` es la llamada creada bajo "Una vez: concede el wrapper", o null
if (grant) await submitRomeTx(provider, { from: who, ...grant });

// 3) aprobar y 4) depositar, como para cualquier 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] }) });
```

Ambos usuarios comparten el mismo `balanceOf` estado en la bóveda.

## Qué sigue

* [Llamar a EVM desde Solana](/es/guias-para-desarrolladores/call-evm-from-solana.md) — la mecánica del carril Solana en detalle
* [Llamar a Solana desde EVM](/es/guias-para-desarrolladores/call-solana-from-evm.md) — la otra dirección (CPI)
* [Obtener financiación](/es/recursos/faucets.md) — USDC como token de gas; puentéalo hacia dentro


---

# 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/es/guias-para-desarrolladores/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.
