diff --git a/README.md b/README.md index e296278..ce453e2 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/core/capabilities.ts b/src/core/capabilities.ts index 9aac5bd..31cffc7 100644 --- a/src/core/capabilities.ts +++ b/src/core/capabilities.ts @@ -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"; @@ -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( diff --git a/src/core/cookbook.ts b/src/core/cookbook.ts index 2485db9..440ff16 100644 --- a/src/core/cookbook.ts +++ b/src/core/cookbook.ts @@ -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; +} diff --git a/test/cookbook-errors.test.ts b/test/cookbook-errors.test.ts new file mode 100644 index 0000000..cd2f1ed --- /dev/null +++ b/test/cookbook-errors.test.ts @@ -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); + }); +});