> 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/pt-br/guias-do-desenvolvedor/dual-lane-app.md).

# Construir um app de duas lanes

Um passo a passo de um app de duas vias — um único contrato Solidity usado tanto por um usuário MetaMask quanto por um usuário de carteira Solana, com exatamente o que acontece em cada etapa.

Um app **app de duas vias** é um contrato Solidity que tanto um **MetaMask (EVM)** usuário e um **wallet Solana** usuário usam diretamente — mesmo contrato, mesmo estado, cada um com a carteira que já possui. Esta página mostra *exatamente* o que acontece em cada etapa, de cada lado.

O exemplo é um pequeno **cofre**: você `deposit` USDC e depois `withdraw` ele. "Stake / unstake", "supply / redeem", "tip / claim" têm a mesma estrutura.

## O que você escreve — um cofre ERC-20 padrão

No Rome, o USDC de um usuário Solana aparece no lado EVM como um **token ERC-20** — o wrapper SPL para esse mint (por exemplo, `wUSDC`). Então seu contrato é um cofre de tokens normal: ele puxa tokens com `transferFrom` e os devolve com `transfer`. Nada específico do 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;                 // o 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 que ERC-20, e não `pagável` / `msg.value`?** O saldo utilizável de um usuário Solana é a **conta de token SPL da carteira**, exposta 1:1 como este wrapper ERC-20 — *não* o saldo nativo do EVM. Então um usuário Solana sempre pode abastecer um `transferFrom`, mas um `deposit() payable` precisaria de valor nativo que ele não mantém parado. Construa em torno do token e as duas vias funcionam do mesmo jeito.

Faça o deploy com Foundry ou Hardhat, apontando o construtor para o endereço do wrapper do seu token (da [registro](https://github.com/rome-protocol/rome-registry)). No Rome, o token de gas é USDC, então você precisa de um pequeno saldo de gas em USDC para fazer o deploy.

## As duas vias

| Via        | Carteira                                               | Como o app o chama                                                                   |
| ---------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------ |
| **EVM**    | MetaMask (uma chave EVM)                               | `submitRomeTx` — a chamada de escrita padrão do Rome                                 |
| **Solana** | uma carteira Solana como a Solflare (uma chave Solana) | `submitRomeTxV1` — uma transação Solana v1 por chamada, sem necessidade de chave EVM |

A via EVM é comum. O resto desta página é a **rota Solana** — a metade interessante.

## A ideia principal: o sintético é um encaminhamento

A identidade EVM de um usuário Solana é o **endereço sintético** — `keccak256(solana_pubkey)[12:]`. É o seu `msg.sender` no contrato, mas **não mantém nada parado.** O dinheiro do usuário fica na **wallet Solana** (como SPL USDC), e, no lado EVM, esse mesmo saldo é o que `wUSDC.balanceOf(synthetic)` lê. O valor passa pelo *por meio de* sintético:

* **Entrada** (depósito): conta de token da carteira → conta de token do sintético → contrato (via `transferFrom`).
* **Saída** (saque): contrato → conta de token do sintético → conta de token da carteira.

O sintético volta a zero após cada ida e volta.

## Uma vez: ativar (provisionar o sintético)

A conta on-chain de um sintético recém-criado não existe até você criá-la. Na primeira vez que um usuário Solana age, o sintético dele é **provisionado** com uma `create_pda` chamada — depois disso, chamadas que movem valor (o ERC-20 `transferFrom`, o sweep) podem ser assinadas por ele.

`submitRomeTxV1` não faz isso por você, então faça o provisionamento explicitamente, uma vez, antes da primeira chamada que move valor. Um bom lugar é uma etapa "Ativar", ou logo antes do primeiro approve:

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

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

## Uma única vez: conceda ao wrapper

O wrapper wUSDC move os tokens de um usuário como o **delegado SPL** da conta de token dele, então cada usuário precisa conceder essa permissão uma vez por wrapper, antes do primeiro depósito. Isso vale tanto para um usuário MetaMask quanto para o endereço sintético de um usuário Solana. Sem a concessão, cada `transferFrom` saída da carteira falha e o depósito reverte com erro SPL `0x4` (owner mismatch).

Verifique com a view do wrapper `isEnabled(user)`. Se ela retornar `false`, envie `approve_spl(wrapper, 2^64-1, mint)` diretamente ao precompile Helper em `0xff00000000000000000000000000000000000009`; o mint é `mint_id()` no wrapper. Na via EVM isso é uma `submitRomeTx` chamada, na via Solana uma `submitRomeTxV1` chamada:

```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) }); // um cliente viem no RPC da cadeia

// quem: o endereço MetaMask, ou syntheticAddress(wallet.publicKey) para um usuário Solana
let grant = null; // permanece null quando isEnabled é 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] }) };
}
// via EVM:    if (grant) await submitRomeTx(provider, { from: who, ...grant });
// via Solana: if (grant) await submitRomeTxV1(deps, grant);
```

## O que cada lado precisa

|                            | Precisa                                                              | Por quê                                                                                                                          |
| -------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| **Usuário Solana**         | **SOL** (um pouco)                                                   | paga a taxa da transação Solana em cada tx da via                                                                                |
|                            | **USDC como token SPL** na carteira dele                             | o valor que ele deposita (visto no lado EVM como `wUSDC`)                                                                        |
| **Usuário EVM (MetaMask)** | **USDC** como seu saldo de gas no Rome                               | gas, e o valor que ele deposita depois de embrulhado; ele recarrega isso ao **fazer bridge de USDC** para o Rome (não há faucet) |
|                            | um **embrulhar** de parte desse gas em **wUSDC** (`withdraw_to_ata`) | o cofre aceita wUSDC, o wrapper ERC-20, não gas nativo. Um saldo MetaMask de USDC de gas não é wUSDC até ser embrulhado          |
|                            | a concessão única **concessão do wrapper** (`approve_spl`)           | permite que o wrapper wUSDC mova os tokens dele; veja [Uma única vez: conceda ao wrapper](#one-time-grant-the-wrapper)           |
| **Você (o desenvolvedor)** | uma carteira com **USDC** gas no Rome                                | para fazer o deploy do contrato                                                                                                  |

## Valor ENTRADA — depósito (passo a passo)

O usuário Solana tem SOL + USDC na carteira Solana. Cada etapa é uma transação Solana v1, **assinada por essa carteira**, e o app a envia para o RPC da Solana. A perna de funding é montada no app e a carteira a assina com um único prompt; cada chamada de contrato é composta pelo proxy, e a carteira a assina depois de uma mensagem de autorização legível:

1. **Perna de funding** — `buildFundLeg(...)` → `submitSolanaInstructionsV1(...)`. Cria a conta de token USDC do sintético (se necessário) e executa **`ActivateAta`**, movendo `quantidade` de USDC da conta de token da carteira **para a**. Agora `wUSDC.balanceOf(synthetic)` mostra esse saldo.
2. **Conceda permissão ao wrapper** (quando `permanece` é false) — `submitRomeTxV1(deps, { to: HELPER, data: approve_spl(wUSDC, 2^64-1, mint) })`. Veja [Uma única vez: conceda ao wrapper](#one-time-grant-the-wrapper). *(Faça o provisionamento do sintético antes disso se for a primeira chamada da via do usuário.)*
3. **Aprovar** — `submitRomeTxV1(deps, { to: wUSDC, data: approve(vault, amount) })`. Permite que o cofre puxe os tokens.
4. **Depósito** — `submitRomeTxV1(deps, { to: vault, data: deposit(amount) })`. O cofre executa `transferFrom(synthetic, vault, amount)` — o USDC se move do sintético para o cofre, creditado no endereço do sintético.

**Efeito líquido:** O USDC foi **carteira Solana → (sintético) → o cofre.**

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

// 1) perna de funding — USDC da carteira → conta de token do sintético (a carteira assina)
await submitSolanaInstructionsV1(
  buildFundLeg({ programId, chainId, mint: usdcMint, amount: depositAmount, wallet: wallet.publicKey, synthetic }),
  { connection, signer },
);

// 2) conceda permissão ao wrapper, uma vez (o sintético já está provisionado); `grant` é a chamada construída em "Uma vez: ativar o sintético", ou null quando isEnabled é true
if (grant) await submitRomeTxV1(deps, grant);

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

// 4) depósito — o cofre puxa via transferFrom
await submitRomeTxV1(deps, { to: vault, data: encodeFunctionData({ abi, functionName: "deposit", args: [depositAmount] }) });
```

## Valor SAÍDA — saque (passo a passo)

Agora o usuário saca. Também assinado pela carteira, dois prompts por chamada:

1. **Withdraw** — `submitRomeTxV1(deps, { to: vault, data: withdraw(amount) })`. `vault.withdraw` executa `transfer(synthetic, amount)` — o USDC se move do cofre de volta para a **conta de token do sintético** .
2. **Perna de sweep** — `buildSweepLeg(...)` te dá a `HelperProgram.transfer_spl` chamada + contas; execute-a (crie a conta de token da carteira se necessário com `submitSolanaInstructionsV1`, depois envie a chamada ao Helper por meio de `submitRomeTxV1`; o proxy encontra as contas de token) para mover o USDC do sintético **de volta para a própria carteira Solana do usuário**. O sintético volta a zero.

**Efeito líquido:** O USDC foi **o cofre → (sintético) → a carteira Solana do usuário.** Nada fica preso.

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

// 1) saque — o cofre devolve USDC para a conta de token do sintético
await submitRomeTxV1(deps, { to: vault, data: encodeFunctionData({ abi, functionName: "withdraw", args: [amount] }) });

// 2) perna de sweep — conta de token do sintético → conta de token da própria carteira do usuário
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 no Hadrian

Cada etapa da via Solana abaixo foi executada como uma transação versão 1 no Hadrian:

| Etapa                                | Unidades de computação | Bytes | Contas |
| ------------------------------------ | ---------------------- | ----- | ------ |
| Provisionamento (`create_pda`)       | 39,008                 | 703   | 9      |
| Concessão de wrapper (`approve_spl`) | 50,687                 | 899   | 13     |
| Aprovar                              | 79,335                 | 769   | 10     |
| Depósito no cofre                    | 242,880                | 1,070 | 20     |
| Saque do cofre                       | 280,903                | 1,037 | 19     |
| Sweep (`transfer_spl`)               | 46,810                 | 899   | 13     |

## As pegadinhas

Estas são as coisas que uma transação da via Solana construída manualmente faz errado. O SDK e o compositor do proxy cuidam da maioria delas:

* **Provisionamento.** A conta de um sintético recém-criado deve ser criada (`create_pda`) antes de qualquer chamada que mova valor, ou ele não consegue assinar a transferência. `submitRomeTxV1` não faz isso por você: verifique `isSyntheticProvisioned` e envie `buildCreatePdaCall(synthetic)` uma vez.
* **Conceda permissão ao wrapper uma vez.** O wrapper move tokens como delegado SPL do usuário. Verifique `permanece` e envie `approve_spl` ao precompile Helper antes do primeiro depósito, ou o depósito reverte com erro SPL `0x4`.
* **Gaste o wrapper, não `msg.value`.** O saldo de um usuário Solana é sua conta de token SPL, exposta como o wrapper ERC-20 — mova-o com `transfer` / `transferFrom`, nunca valor nativo.
* **Montado para você.** ComputeBudget, a carteira do tesouro e a descoberta de contas são feitas pelo compositor do proxy, então nenhuma delas aparece no seu código. Antes de a carteira assinar, o SDK verifica a transação composta e recusa qualquer coisa além da chamada autorizada pelo usuário.
* **Para onde é enviada.** A carteira assina a transação v1 e o **app a envia para o RPC da Solana** — não para o proxy, que a compõe (uma chamada retransmitida é a única exceção, abaixo; veja [Chamadas retransmitidas e chamadas longas](/pt-br/guias-do-desenvolvedor/call-evm-from-solana.md#relayed-calls-and-long-calls)). On-chain, o programa deriva `msg.sender` a partir do signer Solana.
* **Dois prompts por chamada.** A carteira assina uma mensagem de autorização legível (`Autorização Rome` e o hash da transação EVM), depois a transação, ambos com a mesma chave.
* **Verifique se sua carteira assina v1.** `signV1Wire` deve retornar a assinatura de 64 bytes da mensagem v1; via Wallet Standard isso é `solana:signTransaction` no wire bruto e `signedTransaction.slice(-64)`. Nem toda carteira decodifica transações v1 ainda, então teste a sua antes de construir em cima disso. No devnet hoje, a Solflare as assina; a Phantom ainda não.
* **Chamadas grandes são retransmitidas.** Uma chamada que não cabe em uma transação é retransmitida pelo proxy com a mesma assinatura de autorização, e o resultado tem `route: "relay"`. Uma chamada retransmitida é paga no gas nativo da cadeia, então recarregue o sintético com `fundSyntheticGas(deps, amountWei)`. Veja [Chamadas retransmitidas e chamadas longas](/pt-br/guias-do-desenvolvedor/call-evm-from-solana.md#relayed-calls-and-long-calls).
* **Gas é USDC.** Sem faucet — faça bridge de USDC para cá (veja [Recebendo fundos](/pt-br/recursos/faucets.md)).

A anterior `submitRomeTxSolanaLane` ainda está no SDK: ele monta uma transação Solana legada no cliente (um prompt de carteira, com ComputeBudget e a carteira do tesouro tratados pelo SDK).

## O mesmo app no MetaMask

Um usuário EVM chama o contrato idêntico com `submitRomeTx` — ferramentas EVM padrão, gas em USDC. Uma diferença em relação ao usuário Solana: um usuário MetaMask mantém **USDC de gas nativo**, não wUSDC. O cofre aceita o wrapper wUSDC, então o usuário primeiro embrulha parte do seu gas, depois concede permissão ao wrapper uma vez, e então faz as etapas ERC-20 normalmente. Não há pernas de funding ou sweep, porque os tokens embrulhados já vivem no endereço EVM dele.

1. **Embrulhe o gas em wUSDC** — envie `withdraw_to_ata(wei)` ao precompile Withdraw em `0x4200000000000000000000000000000000000016`. Ele queima essa mesma quantidade de gas nativo e credita o valor equivalente à conta de token do usuário no token de gas, que `wUSDC.balanceOf(user)` então mostra. `wei` está na unidade de gas de 18 casas decimais e precisa ser um múltiplo de 10^12 (uma unidade de wUSDC de 6 casas decimais), ou a chamada reverte. Pule esta etapa se o usuário já tiver wUSDC.
2. **Conceda permissão ao wrapper** (quando `permanece` é false) — a mesma `approve_spl` chamada acima, enviada com `submitRomeTx`. Veja [Uma única vez: conceda ao wrapper](#one-time-grant-the-wrapper).
3. **Aprovar** — `approve(vault, amount)` no wUSDC.
4. **Depósito** — `deposit(amount)` no cofre. O saque é `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) embrulhe parte do USDC de gas em wUSDC (weiAmount está em unidades de gas de 18 casas decimais)
await submitRomeTx(provider, { from: who, to: WITHDRAW, data: encodeFunctionData({ abi: withdrawAbi, functionName: "withdraw_to_ata", args: [weiAmount] }) });

// 2) conceda permissão ao wrapper, uma vez: `grant` é a chamada construída em "Uma vez: conceda permissão ao wrapper", ou null
if (grant) await submitRomeTx(provider, { from: who, ...grant });

// 3) aprovar e 4) depositar, como em qualquer 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 os usuários compartilham o mesmo `balanceOf` estado no cofre.

## O que vem a seguir

* [Chamar EVM a partir da Solana](/pt-br/guias-do-desenvolvedor/call-evm-from-solana.md) — a mecânica da via Solana em detalhe
* [Chamar Solana a partir da EVM](/pt-br/guias-do-desenvolvedor/call-solana-from-evm.md) — a outra direção (CPI)
* [Recebendo fundos](/pt-br/recursos/faucets.md) — USDC como token de gas; faça bridge para cá


---

# 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/pt-br/guias-do-desenvolvedor/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.
