From 98c9b00f61ba57b327458a10ea7865ea75082e05 Mon Sep 17 00:00:00 2001 From: Anil Kumar <150643132+anil-rome@users.noreply.github.com> Date: Tue, 21 Jul 2026 06:13:14 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20tx=20=E2=80=94=20cross-VM=20diagnosis?= =?UTF-8?q?=20(EVM=20receipt=20+=20Solana=20settlement=20+=20Via=20link)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `rome tx ` maps an EVM tx to its Solana settlement via rome_solanaTxForEvmTx (Rome has no debug_trace*) alongside the receipt/status and a Via explorer link. Read-only, no key → CLI + MCP. One injectable RpcCall; verified live on Hadrian. --- README.md | 1 + src/core/capabilities.ts | 10 ++++++ src/core/tx.ts | 63 +++++++++++++++++++++++++++++++++++++ test/tx.test.ts | 67 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 141 insertions(+) create mode 100644 src/core/tx.ts create mode 100644 test/tx.test.ts diff --git a/README.md b/README.md index 419d25b..e296278 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ rome cookbook cpi-recipe # the CPI account-rules + SDK encoders (groun rome cookbook patterns lending # which example repo + guide fits a goal rome call hadrian 0x… "balanceOf(address) returns (uint256)" 0x… # read a contract (no key) rome doctor hadrian --address 0x… # preflight: chain live? RPC reachable? program set? wallet funded? +rome tx hadrian 0x… # diagnose a tx: EVM receipt + the Solana settlement tx(s) + a Via link # actions — sign on-chain, need ROME_EVM_KEY (never a flag/log/MCP): rome deploy hadrian ./out/Store.json # deploy a compiled artifact diff --git a/src/core/capabilities.ts b/src/core/capabilities.ts index fb3cf9d..9aac5bd 100644 --- a/src/core/capabilities.ts +++ b/src/core/capabilities.ts @@ -3,6 +3,7 @@ import { getCpiRecipe, getPatterns } from "./cookbook.js"; import { callContract, deployContract, sendContract } from "./actions.js"; import { fundHandler, bridgeHandler } from "./bridge.js"; import { doctor } from "./doctor.js"; +import { diagnoseTx } from "./tx.js"; import { type Deps } from "./deps.js"; export interface ArgSpec { @@ -153,6 +154,15 @@ export const CAPABILITIES: Capability[] = [ (a, deps) => doctor(a.chain, a.address, deps), ), + // ── tx: cross-VM diagnosis (read; CLI + MCP) ── + verbCap( + "tx", + "tx", + "Diagnose a tx: EVM receipt + status, the Solana settlement tx(s) via rome_solanaTxForEvmTx (Rome has no debug_trace*), and a Via link. e.g. rome tx hadrian 0x…", + [chainArg, { name: "hash", required: true, description: "the EVM tx hash (0x…)" }], + (a) => diagnoseTx(a.chain, a.hash), + ), + // ── fund / bridge: the "from home" on-ramp (CCTP USDC inbound). CLI-only actions. ── verbAction( "fund", diff --git a/src/core/tx.ts b/src/core/tx.ts new file mode 100644 index 0000000..e4dd575 --- /dev/null +++ b/src/core/tx.ts @@ -0,0 +1,63 @@ +import { getChainFacts } from "./facts.js"; + +// `tx ` — read-only cross-VM diagnosis. Rome has no debug_trace*; the substitute +// is the EVM receipt + `rome_solanaTxForEvmTx` (EVM hash → the Solana settlement tx(s), +// raw JSON-RPC, no SDK wrapper) + a Via explorer link. One injectable RpcCall keeps it +// testable without a network. + +export type RpcCall = (method: string, params: unknown[]) => Promise; + +export interface TxDiagnosis { + chainId: number; + hash: string; + status: "success" | "reverted" | "pending"; + receipt: { blockNumber: string; gasUsed: string; from: string; to: string | null } | null; + solanaSettlement: string[]; + explorer: string; + note: string; +} + +function realRpcCall(rpcUrl: string): RpcCall { + return async (method, params) => { + const res = await fetch(rpcUrl, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }), + }); + const j = (await res.json()) as { result?: unknown; error?: { message?: string } }; + if (j.error) throw new Error(j.error.message ?? "rpc error"); + return j.result; + }; +} + +export async function diagnoseTx(chain: string | number, hash: string, call?: RpcCall): Promise { + const c = getChainFacts(chain); + const rpc = call ?? realRpcCall(c.rpcUrl); + + const receiptRaw = (await rpc("eth_getTransactionReceipt", [hash])) as Record | null; + + // Tolerate the method being unavailable on a given proxy (→ []). + let solanaSettlement: string[] = []; + try { + const sol = await rpc("rome_solanaTxForEvmTx", [hash]); + if (Array.isArray(sol)) solanaSettlement = sol as string[]; + } catch { + solanaSettlement = []; + } + + const status: TxDiagnosis["status"] = !receiptRaw ? "pending" : receiptRaw.status === "0x1" ? "success" : "reverted"; + const receipt = receiptRaw + ? { blockNumber: receiptRaw.blockNumber, gasUsed: receiptRaw.gasUsed, from: receiptRaw.from, to: receiptRaw.to ?? null } + : null; + + const explorerBase = ((c as { explorerUrl?: string }).explorerUrl ?? "").replace(/\/?$/, "/"); + const explorer = explorerBase ? `${explorerBase}tx/${hash}` : hash; + const note = + status === "pending" + ? "No receipt yet — the tx may still be settling (Rome indexes the EVM block after the Solana tx lands)." + : status === "reverted" + ? "Reverted. Rome has no debug_trace*; re-run the same inputs via rome_emulateTx to see the failure." + : "Confirmed. solanaSettlement lists the Solana tx(s) that settled this EVM tx."; + + return { chainId: c.chainId, hash, status, receipt, solanaSettlement, explorer, note }; +} diff --git a/test/tx.test.ts b/test/tx.test.ts new file mode 100644 index 0000000..2a11a69 --- /dev/null +++ b/test/tx.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect } from "vitest"; +import { diagnoseTx, type RpcCall } from "../src/core/tx.js"; +import { CAPABILITIES } from "../src/core/capabilities.js"; +import { buildMcpTools } from "../src/mcp.js"; + +// `tx ` — read-only cross-VM diagnosis: EVM receipt + the Solana settlement +// tx(s) via rome_solanaTxForEvmTx (Rome has no debug_trace*) + a Via explorer link. +// One injectable RpcCall (method,params)→result keeps it testable without a network. + +const HASH = "0x8d993f2a9802ea0633a1e8d5fa32194f85524d9d6adc9332f41e2b82295c5d74"; +const SOL = "5Lwke7W1w3nxrLNai4w7j5rFCMpVSYHffaJKTKC5231jgZVdgQF59ZnWNTxmMDPzAwuVuqTNgN9DRV8Pdxv1DNZU"; + +/** Build a mock RpcCall from a per-method response map. */ +function mockCall(map: Record): RpcCall { + return async (method) => map[method]; +} + +describe("diagnoseTx — cross-VM tx diagnosis", () => { + it("maps a confirmed EVM tx to its Solana settlement + a Via link", async () => { + const call = mockCall({ + eth_getTransactionReceipt: { status: "0x1", gasUsed: "0x1607b0", blockNumber: "0x1c7a083d", from: "0xaa", to: "0xbb" }, + rome_solanaTxForEvmTx: [SOL], + }); + const r = await diagnoseTx("hadrian", HASH, call); + expect(r.status).toBe("success"); + expect(r.hash).toBe(HASH); + expect(r.receipt?.gasUsed).toBe("0x1607b0"); + expect(r.solanaSettlement).toEqual([SOL]); + expect(r.explorer).toContain(HASH); + }); + + it("reports a reverted tx", async () => { + const call = mockCall({ + eth_getTransactionReceipt: { status: "0x0", gasUsed: "0x5208", blockNumber: "0x10", from: "0xaa", to: "0xbb" }, + rome_solanaTxForEvmTx: [SOL], + }); + const r = await diagnoseTx("hadrian", HASH, call); + expect(r.status).toBe("reverted"); + }); + + it("reports pending when the receipt isn't indexed yet (settlement empty)", async () => { + const call = mockCall({ eth_getTransactionReceipt: null, rome_solanaTxForEvmTx: [] }); + const r = await diagnoseTx("hadrian", HASH, call); + expect(r.status).toBe("pending"); + expect(r.receipt).toBeNull(); + expect(r.solanaSettlement).toEqual([]); + }); + + it("tolerates rome_solanaTxForEvmTx being unavailable (returns [])", async () => { + const call: RpcCall = async (m) => { + if (m === "eth_getTransactionReceipt") return { status: "0x1", gasUsed: "0x1", blockNumber: "0x1", from: "0xaa", to: "0xbb" }; + throw new Error("method not found"); + }; + const r = await diagnoseTx("hadrian", HASH, call); + expect(r.status).toBe("success"); + expect(r.solanaSettlement).toEqual([]); + }); +}); + +describe("tx is a read capability, on MCP", () => { + it("registered as a read verb and present on the MCP surface", () => { + const cap = CAPABILITIES.find((c) => c.id === "tx.tx"); + expect(cap?.kind).toBe("read"); + expect(cap?.requiresKey).toBe(false); + expect(new Set(buildMcpTools().map((t) => t.name)).has("tx")).toBe(true); + }); +});