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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ rome facts balance hadrian 0x… # native (gas-token) balance for an address
rome facts programs devnet # Solana program ids for a network
rome cookbook cpi-recipe # the CPI account-rules + SDK encoders (grounded addresses)
rome cookbook patterns lending # which example repo + guide fits a goal
rome cookbook errors "Custom(1)" # decode a Rome failure → cause + fix (the error taxonomy)
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
Expand Down
9 changes: 8 additions & 1 deletion src/core/capabilities.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getChainFacts, getTokenFacts, getContractFacts, getGasFacts, getBalanceFacts, getProgramFacts } from "./facts.js";
import { getCpiRecipe, getPatterns } from "./cookbook.js";
import { getCpiRecipe, getPatterns, getErrors } from "./cookbook.js";
import { callContract, deployContract, sendContract } from "./actions.js";
import { fundHandler, bridgeHandler } from "./bridge.js";
import { doctor } from "./doctor.js";
Expand Down Expand Up @@ -107,6 +107,13 @@ export const CAPABILITIES: Capability[] = [
[{ name: "goal", required: false, description: "optional goal keyword (e.g. lending, amm, oracle)" }],
(a) => getPatterns(a.goal),
),
cap(
"cookbook",
"errors",
"Decode a Rome failure → cause + fix (the Rome error taxonomy). A query filters; no query returns the full list.",
[{ name: "query", required: false, description: "optional error text / keyword (e.g. Custom(1), gas, cpi, forge)" }],
(a) => getErrors(a.query),
),

// ── contract verbs: `call` is a read (CLI+MCP); `deploy`/`send` are CLI-only actions (key) ──
verbCap(
Expand Down
86 changes: 86 additions & 0 deletions src/core/cookbook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,89 @@ export function getPatterns(goal?: string): Pattern[] {
);
return hits.length ? hits : PATTERNS;
}

export interface ErrorEntry {
symptom: string;
cause: string;
fix: string;
tags: string[];
}

// The net-new Rome error taxonomy — the failure modes a builder/agent actually hits,
// each as symptom → cause → fix. Rome-specific behaviours that differ from vanilla EVM.
const ERRORS: ErrorEntry[] = [
{
symptom: "eth_estimateGas returns a huge value (often 10-50× the real charge); budgets or hard-fails blow up.",
cause: "Rome charges the EXACT gas used; the estimate is a loose upper bound, not the amount charged.",
fix: "Don't hard-fail or size budgets off the estimate. A plain native-token transfer is ~1.48M gas (not 21k) — use a high fixed gas ceiling and let Rome charge exact.",
tags: ["gas", "estimate", "estimategas", "budget", "21000", "out-of-gas"],
},
{
symptom: "A write or eth_estimateGas fails with Custom(1) attributed to the System Program.",
cause: "The emulation pool payer is rent-starved — not a fault in your transaction's calldata.",
fix: "Retry; if it persists the chain's pool payer needs funding (operator side). Your transaction is fine.",
tags: ["custom(1)", "custom1", "system-program", "pool-payer", "estimate", "rent"],
},
{
symptom: "A transaction fails with Custom(0) and no obvious reason.",
cause: "An account is TTL-locked (~3-4s) by a concurrent iterative (large) transaction; AccountLocked surfaces as Custom(0).",
fix: "Retry after a few seconds once the lock clears. Avoid racing the same account from parallel large transactions.",
tags: ["custom(0)", "custom0", "accountlocked", "locked", "iterative", "concurrency", "retry"],
},
{
symptom: "A transaction that does a CPI fails with CpiProhibitedInIterativeTx.",
cause: "The transaction was large enough to run in iterative mode, where CPI is prohibited.",
fix: "Keep CPI transactions small enough to execute atomically (one Solana tx). Split work so the CPI leg stays under the iterative threshold.",
tags: ["cpi", "iterative", "cpiprohibited", "atomic", "size"],
},
{
symptom: "An on-chain call fails with `Program log: Error: UnknownInstruction(N)` and `custom program error: 0x0`.",
cause: "The rome-evm program on that chain doesn't implement instruction N — usually a newer feature not yet deployed there.",
fix: "That chain's rome-evm program needs an upgrade/redeploy to add the instruction. Confirm the feature is deployed on the chain you're calling.",
tags: ["unknowninstruction", "instruction", "0x0", "program-error", "deploy", "upgrade", "settle"],
},
{
symptom: "`forge script` fails in the simulation step before broadcasting.",
cause: "Rome's execution model diverges from forge's local EVM simulation.",
fix: "Run `forge script --skip-simulation`. `cast` and `forge create` work as-is.",
tags: ["forge", "foundry", "script", "simulation", "skip-simulation", "deploy"],
},
{
symptom: "eth_getLogs returns error -32005 (query range too large).",
cause: "Rome caps the block range per eth_getLogs query.",
fix: "Narrow the fromBlock→toBlock window and page the range.",
tags: ["eth_getlogs", "getlogs", "-32005", "32005", "logs", "range", "query"],
},
{
symptom: "A Solana-lane (Phantom / DoTxUnsigned) action fails with Failure(Custom(1)).",
cause: "The Solana-lane sender lacks SOL for the on-chain step (the synthetic sender isn't funded with lamports).",
fix: "Fund the sender's SOL (swap gas to lamports) before the action; submitRomeTxSolanaLane handles the fund leg when wired.",
tags: ["solana-lane", "custom(1)", "phantom", "sol", "lamports", "sender", "dotxunsigned"],
},
{
symptom: "A Wormhole redeem reverts with 'gas limit too high'.",
cause: "The VAA was already redeemed.",
fix: "Guard with isTransferCompleted before redeeming; treat already-redeemed as success.",
tags: ["wormhole", "vaa", "redeem", "gas-limit-too-high", "already-redeemed", "bridge"],
},
{
symptom: "An inbound `fund --intent gas` completes the CCTP legs but native Rome gas never credits (or ends `settle-failed`).",
cause: "The final sponsored settle depends on the chain's rome-evm settle instruction; if it's missing/failing the gas conversion can't land.",
fix: "Read the transfer's degradationReason for the on-chain error; `--intent wrapper` bypasses the settle (delivers wUSDC). The USDC is always safe in the recipient's account.",
tags: ["bridge", "fund", "settle", "gas", "wrapper", "cctp", "from-home", "degradation"],
},
];

/** Decode a Rome failure → cause + fix. No query → the full taxonomy. */
export function getErrors(query?: string): ErrorEntry[] {
if (!query) return ERRORS;
const q = query.trim().toLowerCase();
const hits = ERRORS.filter(
(e) =>
e.symptom.toLowerCase().includes(q) ||
e.cause.toLowerCase().includes(q) ||
e.fix.toLowerCase().includes(q) ||
e.tags.some((t) => t.includes(q) || q.includes(t)),
);
return hits.length ? hits : ERRORS;
}
39 changes: 39 additions & 0 deletions test/cookbook-errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, it, expect } from "vitest";
import { getErrors } from "../src/core/cookbook.js";
import { CAPABILITIES } from "../src/core/capabilities.js";
import { buildMcpTools } from "../src/mcp.js";

// `cookbook errors` — the net-new Rome error taxonomy: decode a failure → cause + fix.
// A read (CLI + MCP), same shape as `cookbook patterns`: filter by query, else the full list.

describe("cookbook errors — the Rome error taxonomy", () => {
it("returns the full taxonomy with no query; every entry has symptom/cause/fix", () => {
const all = getErrors();
expect(all.length).toBeGreaterThanOrEqual(8);
for (const e of all) {
expect(e.symptom).toBeTruthy();
expect(e.cause).toBeTruthy();
expect(e.fix).toBeTruthy();
}
});

it("filters by symptom / tag", () => {
const s = (e: { symptom: string; cause: string; fix: string }) => `${e.symptom} ${e.cause} ${e.fix}`;
expect(getErrors("gas").some((e) => /estimate/i.test(s(e)))).toBe(true);
expect(getErrors("cpi").some((e) => /iterative/i.test(s(e)))).toBe(true);
expect(getErrors("UnknownInstruction").some((e) => /instruction/i.test(e.symptom))).toBe(true);
});

it("falls back to the full list on an unknown query (agent still sees everything)", () => {
expect(getErrors("zzzz-nope").length).toBe(getErrors().length);
});
});

describe("cookbook errors is a read capability on MCP", () => {
it("registered as a read + present on the MCP surface", () => {
const cap = CAPABILITIES.find((c) => c.id === "cookbook.errors");
expect(cap?.kind).toBe("read");
expect(cap?.requiresKey).toBe(false);
expect(new Set(buildMcpTools().map((t) => t.name)).has("cookbook_errors")).toBe(true);
});
});
Loading