Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ Chains resolve by id, name, or slug (`200010`, `hadrian`, `Rome Hadrian`) — by
| `deploy <chain> <artifact> [args]` | `ROME_EVM_KEY` | deploy a compiled artifact, handling Rome's gas quirks |
| `send <chain> <addr> <sig> [args]` | `ROME_EVM_KEY` | write to a contract via `submitRomeTx` (the correct Rome write path) |
| `fund <chain> --from <src> --amount <usdc>` | `ROME_EVM_KEY` | bridge USDC → Rome **gas** (CCTP); the "from home" on-ramp |
| `bridge <chain> --from <src> --amount <usdc> [--intent gas\|wrapper]` | `ROME_EVM_KEY` | bridge USDC **in** as gas or wUSDC |
| `bridge <chain> --to <dest> --amount <usdc> [--recipient 0x…]` | `ROME_EVM_KEY` | bridge wUSDC **out**: burn on Rome → claim handle for the destination (you claim there) |
| `bridge <chain> --from <src> --amount <n> [--asset usdc\|eth] [--intent gas\|wrapper]` | `ROME_EVM_KEY` | bridge **in**: USDC (CCTP) as gas or wUSDC · ETH (Wormhole) as wETH |
| `bridge <chain> --to <dest> --amount <n> [--asset usdc\|eth] [--recipient 0x…]` | `ROME_EVM_KEY` | bridge **out**: burn the wrapper on Rome → claim handle (you claim on the destination; ETH exits to Ethereum only) |
| `activate <chain>` | `ROME_EVM_KEY` | one-time PDA funding required before the first bridge **out** (idempotent; inbound needs none) |
| `new <app-name> [--chain <chain>]` | *none* (keyless) | scaffold a dual-lane app — wraps `create-rome-app`, pre-wires the chain from the registry into `.env`, prints the lifecycle next-steps (fund → deploy → demo → verify). CLI-only: MCP never writes to disk |
| `verify <chain> [--path solidity]` | `ROME_EVM_KEY` + `ROME_SOLANA_KEY` | the **both-lane works-gate**: deploy a probe, drive it from the EVM lane *and* the Solana lane, assert parity |
Expand Down
10 changes: 10 additions & 0 deletions docs/GUIDES.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,14 @@ rome bridge hadrian --from base-sepolia --amount 0.5 --intent wrapper # → wU

Supported source chains come from the registry's bridge config for the target Rome chain — Base Sepolia, Arbitrum Sepolia, Polygon Amoy, Avalanche Fuji, Monad Testnet, Sepolia. Resolve a source by id, name, or slug (`84532`, `"base sepolia"`, `base-sepolia`). CCTP standard attestation takes ~15–20 min, so a real transfer isn't instant; the command polls until it lands.

**Holding ETH instead?** Add `--asset eth` — it rides **Wormhole** and lands as **wETH** on Rome (never gas; gas is USDC, so `--intent` doesn't apply):

```bash
rome bridge hadrian --from sepolia --amount 0.002 --asset eth # ETH → wETH on Rome (~15 min VAA)
```

You sign one wrap-and-transfer tx on the source; Rome's sponsor completes the transfer on the Solana side once the VAA is ready.

> The bridge-api base defaults to the devnet orchestrator; override with `--bridge-api <url>` or `ROME_BRIDGE_API`.

---
Expand Down Expand Up @@ -301,6 +309,8 @@ $ rome bridge hadrian --to base-sepolia --amount 0.1

Destinations are the same registry CCTP chains as sources (resolve by id / name / slug). `--recipient` sets the destination address (default = your address).

**ETH out:** `--asset eth` burns your **wETH** on Rome (two txs: approve + burn) and exits via **Wormhole to Ethereum only** (`--to sepolia`; the CLI refuses any other destination). Same claims-are-yours rule — redeem the VAA on Ethereum when it's ready, and guard with `isTransferCompleted`: re-redeeming an already-completed VAA reverts with a misleading gas error. First outbound still needs `rome activate`.

### How the bridge actually works

- **The real bridge is on-chain.** `RomeBridgeWithdraw.burnUSDC` (egress) + Circle CCTP v2 do the work. The **bridge-api** ([`rome-protocol/rome-bridge-api`](https://github.com/rome-protocol/rome-bridge-api)) is an off-chain *orchestrator + tracker* — it quotes the route and follows the transfer; it **holds no funds and cannot move yours**.
Expand Down
44 changes: 42 additions & 2 deletions src/core/activate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ export const CCTP_BURN_RESERVE = 15_000_000n;

const CPI_PRECOMPILE = "0xff00000000000000000000000000000000000008" as const;
const LAMPORTS_ABI = parseAbi(["function account_lamports(bytes32) view returns (uint64)"]);
const ACTIVATOR_ABI = parseAbi(["function activate() payable", "function activationCost() view returns (uint256)"]);
const ACTIVATOR_ABI = parseAbi([
"function activate() payable",
"function activationCost() view returns (uint256)",
"function USER_PDA_FUNDING() view returns (uint64)",
"function topUpUserPda(uint64 lamports) payable",
]);

export interface ActivationStatus {
address: `0x${string}`;
Expand Down Expand Up @@ -53,6 +58,8 @@ export interface ActivateResult {
address: `0x${string}`;
pda: string;
alreadyActivated: boolean;
/** true when a drained (activated-but-below-reserve) PDA was refilled via topUpUserPda. */
toppedUp?: boolean;
lamports: string;
txHash?: `0x${string}`;
cost?: string;
Expand All @@ -63,16 +70,39 @@ export interface ActivateDeps {
pda: string;
readLamports(): Promise<bigint>;
getActivationCost(): Promise<bigint>;
/** SimpleActivator.USER_PDA_FUNDING — the full lamport level activation establishes. */
getPdaFunding(): Promise<bigint>;
activate(cost: bigint): Promise<{ hash: `0x${string}`; success: boolean }>;
/** SimpleActivator.topUpUserPda(lamports){value} — refill a drained PDA. */
topUp(lamports: bigint, value: bigint): Promise<{ hash: `0x${string}`; success: boolean }>;
}

/** Fund the actor's external-auth PDA once (idempotent — skips, no spend, if already activated). */
/**
* Three states, one command:
* - lamports >= reserve → already activated; skip (no spend)
* - lamports == 0 → never activated; SimpleActivator.activate{value: cost}
* - 0 < lamports < reserve → activated but DRAINED (each burn consumes rent);
* activate() would revert AlreadyActivated — refill via
* topUpUserPda back to the full funding level, priced at
* the on-chain rate (activationCost / USER_PDA_FUNDING).
*/
export async function runActivate(deps: ActivateDeps): Promise<ActivateResult> {
const before = await deps.readLamports();
if (before >= CCTP_BURN_RESERVE) {
return { address: deps.address, pda: deps.pda, alreadyActivated: true, lamports: before.toString() };
}
const cost = await deps.getActivationCost();

if (before > 0n) {
const funding = await deps.getPdaFunding();
const lamports = funding - before;
const value = (lamports * cost + funding - 1n) / funding; // ceil at the on-chain rate
const { hash, success } = await deps.topUp(lamports, value);
if (!success) throw new Error(`Top-up tx reverted (${hash}).`);
const after = await deps.readLamports();
return { address: deps.address, pda: deps.pda, alreadyActivated: false, toppedUp: true, txHash: hash, cost: value.toString(), lamports: after.toString() };
}

const { hash, success } = await deps.activate(cost);
if (!success) throw new Error(`Activation tx reverted (${hash}).`);
const after = await deps.readLamports();
Expand Down Expand Up @@ -113,11 +143,21 @@ export function defaultActivateDeps(chain: string | number): ActivateDeps {
const { data } = await pub.call({ to: activator, data: encodeFunctionData({ abi: ACTIVATOR_ABI, functionName: "activationCost" }) });
return BigInt(data && data !== "0x" ? data : "0x0");
},
async getPdaFunding() {
const { data } = await pub.call({ to: activator, data: encodeFunctionData({ abi: ACTIVATOR_ABI, functionName: "USER_PDA_FUNDING" }) });
return BigInt(data && data !== "0x" ? data : "0x0");
},
async activate(cost) {
const hash = await submitRomeTx(provider, { from: account.address, to: activator, data: encodeFunctionData({ abi: ACTIVATOR_ABI, functionName: "activate" }), value: cost });
const rcpt = await pub.waitForTransactionReceipt({ hash });
return { hash, success: rcpt.status === "success" };
},
async topUp(lamports, value) {
const data = encodeFunctionData({ abi: ACTIVATOR_ABI, functionName: "topUpUserPda", args: [lamports] });
const hash = await submitRomeTx(provider, { from: account.address, to: activator, data, value });
const rcpt = await pub.waitForTransactionReceipt({ hash });
return { hash, success: rcpt.status === "success" };
},
};
}

Expand Down
Loading
Loading