> 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/ru/rukovodstva-dlya-razrabotchikov/dual-lane-app.md).

# Построение приложения с двумя линиями

Пошаговый разбор двуполосного приложения — один контракт Solidity, используемый как пользователем MetaMask, так и пользователем кошелька Solana, с точным описанием того, что происходит на каждом шаге.

Приложение **приложение с двумя каналами** — это один контракт Solidity, которым пользуются и **MetaMask (EVM)** пользователь и **кошелька Solana** пользователь используют его напрямую — один и тот же контракт, одно и то же состояние, у каждого — уже имеющийся у него кошелёк. На этой странице пошагово разбирается *точно* что происходит на каждом шаге, с каждой стороны.

Пример — это небольшой **хранилище**: вы `deposit` USDC, а затем `withdraw` его. «Стейкинг/анстейкинг», «внесение/выкуп», «чаевые/получение» — это одна и та же схема.

## Что вы пишете — стандартное хранилище ERC-20

В Rome USDC пользователя Solana на стороне EVM отображается как обычный **токен ERC-20** — SPL-обёртка для этого mint'а (например, `wUSDC`). Поэтому ваш контракт — обычное токенное хранилище: он забирает токены с помощью `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-токен-аккаунт кошелька**, отображаемый 1:1 как эта ERC-20-обёртка — *не* нативный баланс EVM. Поэтому пользователь Solana всегда может пополнить `transferFrom`, но `deposit() payable` потребовал бы нативную стоимость, которой у него нет в состоянии покоя. Стройте вокруг токена, и оба канала будут работать одинаково.

Разверните его с помощью Foundry или Hardhat, указав в конструкторе адрес обёртки для вашего токена (из [реестре](https://github.com/rome-protocol/rome-registry)). В Rome газовым токеном является USDC, так что для развертывания вам нужен небольшой баланс USDC на газ.

## Два канала

| Канал      | Кошелёк                                          | Как приложение вызывает его                                                             |
| ---------- | ------------------------------------------------ | --------------------------------------------------------------------------------------- |
| **EVM**    | MetaMask (ключ EVM)                              | `submitRomeTx` — стандартная запись в Rome                                              |
| **Solana** | кошелёк Solana, такой как Solflare (ключ Solana) | `submitRomeTxV1` — по одной транзакции Solana v1 на каждый вызов, ключ EVM не требуется |

Канал EVM — обычный. Остальная часть этой страницы — это **маршрута Solana** — интересная половина.

## Ключевая идея: синтетический адрес — это лишь проводник

EVM-идентичность пользователя Solana — это его **синтетический адрес** — `keccak256(solana_pubkey)[12:]`. Это его `msg.sender` в контракте, но **в состоянии покоя он ничего не хранит.** Деньги пользователя находятся в его **кошелька Solana** (в виде SPL USDC), а на стороне EVM этот же баланс — это то, что `wUSDC.balanceOf(synthetic)` читает. Стоимость течёт *через* через синтетический адрес:

* **В** (внесение): токен-аккаунт кошелька → токен-аккаунт синтетического адреса → контракт (через `transferFrom`).
* **Из** (вывод): контракт → токен-аккаунт синтетического адреса → токен-аккаунт кошелька.

После каждого прохода синтетический объект сводится к нулю.

## Одноразово: активируйте (подготовьте синтетический адрес)

У совершенно нового синтетического адреса ончейн-аккаунт не существует, пока вы его не создадите. В первый раз, когда пользователь Solana совершает действие, его синтетический адрес **подготавливается** с помощью `create_pda` вызова — после этого вызовы, перемещающие стоимость (ERC-20 `transferFrom`, sweep), могут быть подписаны им.

`submitRomeTxV1` не делает этого за вас, поэтому подготовьте его явно, один раз, до первого вызова, перемещающего стоимость. Хорошее место — шаг «Активировать» или сразу перед первым 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)); // один create_pda
}
```

## Однократно: предоставьте обёртке

Обёртка wUSDC перемещает токены пользователя как **SPL-делегат** правообладатель его токен-аккаунта, поэтому каждому пользователю нужно один раз для каждой обёртки предоставить это право до первого депозита. Это относится и к пользователю MetaMask, и к синтетическому адресу пользователя Solana. Без этого разрешения каждая `transferFrom` операция из кошелька завершается ошибкой, а депозит откатывается с ошибкой SPL `0x4` (несоответствие владельца).

Проверьте с помощью view-метода обёртки `isEnabled(user)`. Если он возвращает `false`, отправьте `approve_spl(wrapper, 2^64-1, mint)` напрямую в Helper precompile по адресу `0xff00000000000000000000000000000000000009`; 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) }); // клиент viem для RPC сети

// кто: адрес MetaMask или syntheticAddress(wallet.publicKey) для пользователя Solana
// остаётся null, когда 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] }) };
}
// Канал EVM:    если grant существует, await submitRomeTx(provider, { from: who, ...grant });
// Канал Solana: если grant существует, await submitRomeTxV1(deps, grant);
```

## Что нужно каждой стороне

|                                 | Нужно                                                              | Почему                                                                                                                                    |
| ------------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| **Пользователь Solana**         | **SOL** (немного)                                                  | оплачивает комиссию за транзакцию Solana на каждом транзакционном шаге                                                                    |
|                                 | **USDC как SPL-токен** в его кошельке                              | стоимость, которую он вносит (на стороне EVM видна как `wUSDC`)                                                                           |
| **Пользователь EVM (MetaMask)** | **USDC** как его баланс газа Rome                                  | газ, а стоимость, которую он вносит после обёртывания; он пополняет его, **мостя USDC** в Rome (faucet отсутствует)                       |
|                                 | один **обернуть** часть этого газа в **wUSDC** (`withdraw_to_ata`) | хранилище принимает wUSDC, обёртку ERC-20, а не нативный газ. Баланс газового USDC в MetaMask не является wUSDC, пока он не будет обёрнут |
|                                 | одноразовое **разрешение обёртке** (`approve_spl`)                 | позволяет обёртке wUSDC перемещать их токены; см. [Однократно: предоставьте обёртке](#one-time-grant-the-wrapper)                         |
| **Вы (разработчик)**            | кошелёк с **USDC** газом в Rome                                    | для развертывания контракта                                                                                                               |

## Стоимость ВХОД — депозит (пошагово)

У пользователя Solana есть SOL + USDC в его кошельке Solana. Каждый шаг — это транзакция Solana v1, **подписанная этим кошельком**, и приложение отправляет её в Solana RPC. Логика пополнения строится в приложении, и кошелёк подписывает её одним запросом; каждый вызов контракта формируется прокси, и кошелёк подписывает его после читаемого сообщения авторизации:

1. **Логика пополнения** — `buildFundLeg(...)` → `submitSolanaInstructionsV1(...)`. Создаёт токен-аккаунт USDC синтетического адреса (если нужно) и выполняет **`ActivateAta`**, перемещая `количество` USDC из токен-аккаунта кошелька **в**. Теперь `wUSDC.balanceOf(synthetic)` показывает этот баланс.
2. **Предоставьте разрешение обёртке** (когда `isEnabled` является false) — `submitRomeTxV1(deps, { to: HELPER, data: approve_spl(wUSDC, 2^64-1, mint) })`. См. [Однократно: предоставьте обёртке](#one-time-grant-the-wrapper). *(подготовьте синтетический адрес до этого, если это первый вызов пользователя в канале.)*
3. **Одобрите** — `submitRomeTxV1(deps, { to: wUSDC, data: approve(vault, amount) })`. Позволяет хранилищу забрать токены.
4. **Депозит** — `submitRomeTxV1(deps, { to: vault, data: deposit(amount) })`. Хранилище выполняет `transferFrom(synthetic, vault, amount)` — USDC перемещается из синтетического адреса в хранилище, зачисляясь на адрес синтетического объекта.

**Итоговый эффект:** USDC прошёл **Кошелёк Solana → (синтетический адрес) → хранилище.**

```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) логика пополнения — USDC кошелька → токен-аккаунт синтетического адреса (кошелёк подписывает)
await submitSolanaInstructionsV1(
  buildFundLeg({ programId, chainId, mint: usdcMint, amount: depositAmount, wallet: wallet.publicKey, synthetic }),
  { connection, signer },
);

// 2) один раз предоставьте разрешение обёртке; `grant` — это вызов, собранный в разделе «Одноразово: предоставьте разрешение обёртке», либо null, когда isEnabled равно true
if (grant) await submitRomeTxV1(deps, grant);

// 3) одобрить хранилище
await submitRomeTxV1(deps, { to: wUSDC, data: encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [vault, depositAmount] }) });

// 4) депозит — хранилище забирает через transferFrom
await submitRomeTxV1(deps, { to: vault, data: encodeFunctionData({ abi, functionName: "deposit", args: [depositAmount] }) });
```

## Стоимость ВЫХОД — вывод (пошагово)

Теперь пользователь выводит средства. Тоже подписывается кошельком, по два запроса на каждый вызов:

1. **Withdraw** — `submitRomeTxV1(deps, { to: vault, data: withdraw(amount) })`. `vault.withdraw` выполняет `transfer(synthetic, amount)` — USDC перемещается из хранилища обратно в **токен-аккаунт синтетического адреса** .
2. **Логика зачистки** — `buildSweepLeg(...)` предоставляет вам `HelperProgram.transfer_spl` вызов + аккаунты; выполните его (при необходимости создайте токен-аккаунт кошелька с помощью `submitSolanaInstructionsV1`, затем отправьте вызов Helper через `submitRomeTxV1`; прокси находит токен-аккаунты) чтобы переместить USDC из синтетического адреса **обратно в собственный кошелёк Solana пользователя**. Синтетический объект сводится к нулю.

**Итоговый эффект:** USDC прошёл **хранилище → (синтетический адрес) → кошелёк Solana пользователя.** Ничего не остаётся заблокированным.

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

// 1) вывод — хранилище возвращает USDC в токен-аккаунт синтетического адреса
await submitRomeTxV1(deps, { to: vault, data: encodeFunctionData({ abi, functionName: "withdraw", args: [amount] }) });

// 2) логика зачистки — токен-аккаунт синтетического адреса → токен-аккаунт собственного кошелька пользователя
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 ниже выполнялся как одна транзакция версии 1 на Hadrian:

| Шаг                                       | Единицы вычисления | Байты | Аккаунты |
| ----------------------------------------- | ------------------ | ----- | -------- |
| Подготовка (`create_pda`)                 | 39,008             | 703   | 9        |
| Выдать разрешение обёртке (`approve_spl`) | 50,687             | 899   | 13       |
| Одобрите                                  | 79,335             | 769   | 10       |
| Депозит в хранилище                       | 242,880            | 1,070 | 20       |
| Вывод из хранилища                        | 280,903            | 1,037 | 19       |
| Зачистка (`transfer_spl`)                 | 46,810             | 899   | 13       |

## Подводные камни

Это те вещи, которые вручную собранная транзакция канала Solana делает неправильно. SDK и composer прокси берут большинство из них на себя:

* **Подготовка.** Аккаунт свежего синтетического адреса должен быть создан (`create_pda`) до любого вызова, перемещающего стоимость, иначе он не сможет подписать перевод. `submitRomeTxV1` не делает этого за вас: проверьте `isSyntheticProvisioned` и отправьте `buildCreatePdaCall(synthetic)` один раз.
* **Один раз предоставьте разрешение обёртке.** Обёртка перемещает токены как SPL-доверенный представитель пользователя. Проверьте `isEnabled` и отправьте `approve_spl` в Helper precompile до первого депозита, иначе депозит откатится с ошибкой SPL `0x4`.
* **Расходуйте обёртку, а не `msg.value`.** Баланс пользователя Solana — это его SPL token account, отображаемый как ERC-20-обёртка — перемещайте его с помощью `transfer` / `transferFrom`, никогда не нативную стоимость.
* **Скомпоновано за вас.** ComputeBudget, treasury-кошелёк и обнаружение аккаунтов выполняются composer'ом прокси, поэтому в вашем коде этого нет. Перед подписанием кошельком SDK проверяет составленную транзакцию и отклоняет всё, кроме вызова, который авторизовал пользователь.
* **Куда это отправляется.** Кошелёк подписывает транзакцию v1, а **приложение отправляет её в Solana RPC** — не в прокси, который её компонувает (проксированный вызов — единственное исключение ниже; см. [Ретранслируемые вызовы и длинные вызовы](/ru/rukovodstva-dlya-razrabotchikov/call-evm-from-solana.md#relayed-calls-and-long-calls)). На цепочке программа выводит `msg.sender` из солана-подписанта.
* **Два запроса на каждый вызов.** Кошелёк подписывает читаемое сообщение авторизации (`Авторизация Rome` и хэш EVM-транзакции), затем саму транзакцию, обе — одним и тем же ключом.
* **Проверьте, что ваш кошелёк подписывает v1.** `signV1Wire` должен возвращать 64-байтовую подпись для сообщения v1; через Wallet Standard это `solana:signTransaction` в raw-виде и `signedTransaction.slice(-64)`. Пока не каждый кошелёк умеет декодировать транзакции v1, поэтому протестируйте свой до того, как строить на нём. Сегодня в devnet Solflare подписывает их; Phantom пока нет.
* **Крупные вызовы идут через реле.** Вызов, который не помещается в одну транзакцию, проксируется прокси с той же подписью авторизации, и результат имеет `route: "relay"`. Проксированный вызов оплачивается в нативном газе цепочки, поэтому пополните синтетический адрес `fundSyntheticGas(deps, amountWei)`. См. [Ретранслируемые вызовы и длинные вызовы](/ru/rukovodstva-dlya-razrabotchikov/call-evm-from-solana.md#relayed-calls-and-long-calls).
* **Газ — это USDC.** Фaucet отсутствует — введите USDC через мост (см. [Получение финансирования](/ru/resursy/faucets.md)).

Ранее `submitRomeTxSolanaLane` по-прежнему есть в SDK: он собирает устаревшую транзакцию Solana на клиенте (один запрос кошельку, при этом ComputeBudget и treasury-кошелёк обрабатываются SDK).

## То же приложение из MetaMask

Пользователь EVM вызывает тот же самый контракт с `submitRomeTx` — стандартный EVM-инструментарий, газ в USDC. Одно отличие от пользователя Solana: пользователь MetaMask держит **нативный газ USDC**, а не wUSDC. Хранилище принимает обёртку wUSDC, поэтому пользователь сначала оборачивает часть своего газа, затем один раз предоставляет разрешение обёртке, а затем выполняет шаги ERC-20 как обычно. Шагов пополнения или зачистки нет, потому что обёрнутые токены уже находятся по его EVM-адресу.

1. **Оберните газ в wUSDC** — отправьте `withdraw_to_ata(wei)` в Withdraw precompile по адресу `0x4200000000000000000000000000000000000016`. Он сжигает такое количество нативного газа и зачисляет эквивалентную сумму на токен-аккаунт пользователя в газовом токене, который `wUSDC.balanceOf(user)` затем отображается. `wei` указан в газовой единице с 18 знаками после запятой и должен быть кратен 10^12 (одной единице wUSDC с 6 знаками после запятой), иначе вызов откатится. Пропустите этот шаг, если у пользователя уже есть wUSDC.
2. **Предоставьте разрешение обёртке** (когда `isEnabled` является false) — тот же `approve_spl` вызов, что и выше, отправленный с `submitRomeTx`. См. [Однократно: предоставьте обёртке](#one-time-grant-the-wrapper).
3. **Одобрите** — `approve(vault, amount)` на wUSDC.
4. **Депозит** — `адаптера` в хранилище. Вывод — это `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 (weiAmount задаётся в газовых единицах с 18 знаками после запятой)
await submitRomeTx(provider, { from: who, to: WITHDRAW, data: encodeFunctionData({ abi: withdrawAbi, functionName: "withdraw_to_ata", args: [weiAmount] }) });

// 2) один раз предоставьте разрешение обёртке: `grant` — это вызов, собранный в разделе «Одноразово: предоставьте разрешение обёртке», либо null
if (grant) await submitRomeTx(provider, { from: who, ...grant });

// 3) одобрить и 4) внести, как для любого 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` состояние в хранилище.

## Что дальше

* [Вызов EVM из Solana](/ru/rukovodstva-dlya-razrabotchikov/call-evm-from-solana.md) — механика канала Solana в деталях
* [Вызов Solana из EVM](/ru/rukovodstva-dlya-razrabotchikov/call-solana-from-evm.md) — другое направление (CPI)
* [Получение финансирования](/ru/resursy/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/ru/rukovodstva-dlya-razrabotchikov/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.
