From f16ab1932cd4cbd525ea01f050da457aaa120945 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Mon, 13 Apr 2026 11:35:01 -0300 Subject: [PATCH 1/3] fix(swap): surface actual error in error modal instead of catch-all "Network error" On-chain reverts were shown as "Network error" because RPCError used the literal message "RPC Error", which matched the network substring catch-all. Network errors now surface the real error (e.g. status 503, endpoint URL) instead of generic "check your internet" boilerplate. --- src/hooks/use-wait-for-tx-confirmation.ts | 10 +- src/lib/__tests__/transaction-errors.test.ts | 128 +++++++++++++++++++ src/lib/transaction-errors.ts | 38 +++++- 3 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 src/lib/__tests__/transaction-errors.test.ts diff --git a/src/hooks/use-wait-for-tx-confirmation.ts b/src/hooks/use-wait-for-tx-confirmation.ts index 9355895f..69b56c8a 100644 --- a/src/hooks/use-wait-for-tx-confirmation.ts +++ b/src/hooks/use-wait-for-tx-confirmation.ts @@ -6,7 +6,7 @@ import { fetchFastTxStatus } from "@/lib/fast-tx-status" import { fetchTransactionReceiptFromDb } from "@/lib/transaction-receipt-utils" import { fetchCommitmentStatus } from "@/lib/fast-rpc-status" import { getTxConfirmationTimeoutMs } from "@/lib/tx-config" -import { RPCError } from "@/lib/transaction-errors" +import { RPCError, buildRevertMessage } from "@/lib/transaction-errors" /** * Adaptive polling: starts fast to catch sub-second preconfirmations, @@ -113,7 +113,7 @@ export function useWaitForTxConfirmation({ try { if (receipt.status === "reverted") { hasConfirmedRef.current = true - const e = new RPCError("RPC Error", receipt) + const e = new RPCError(buildRevertMessage(receipt), receipt) setError(e) onErrorRef.current?.(e) return @@ -238,7 +238,11 @@ export function useWaitForTxConfirmation({ hasConfirmedRef.current = true abortController.abort() clearInterval(dbPollInterval) - const e = new RPCError("RPC Error", rpcResult.receipt, rpcResult.rawResult) + const e = new RPCError( + buildRevertMessage(rpcResult.receipt, rpcResult.rawResult), + rpcResult.receipt, + rpcResult.rawResult + ) setError(e) onErrorRef.current?.(e) return diff --git a/src/lib/__tests__/transaction-errors.test.ts b/src/lib/__tests__/transaction-errors.test.ts new file mode 100644 index 00000000..6fd56cac --- /dev/null +++ b/src/lib/__tests__/transaction-errors.test.ts @@ -0,0 +1,128 @@ +import { describe, it, expect } from "vitest" +import type { TransactionReceipt } from "viem" +import { + RPCError, + buildRevertMessage, + getTransactionShortMessage, + getTransactionFullMessage, + getTransactionErrorMessage, + getTransactionErrorTitle, +} from "../transaction-errors" + +const fakeReceipt: TransactionReceipt = { + transactionHash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + blockNumber: 12345n, + gasUsed: 98765n, + status: "reverted", + transactionIndex: 0, + blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000", + from: "0x0000000000000000000000000000000000000001", + to: "0x0000000000000000000000000000000000000002", + cumulativeGasUsed: 98765n, + contractAddress: null, + logs: [], + logsBloom: "0x00", + type: "0x2", + effectiveGasPrice: 1000000000n, +} + +describe("buildRevertMessage", () => { + it("includes tx hash, block, and gas from receipt", () => { + const msg = buildRevertMessage(fakeReceipt) + expect(msg).toContain("0x12345678…90abcdef") + expect(msg).toContain("12345") + expect(msg).toContain("98765") + expect(msg).toContain("reverted on-chain") + }) + + it("includes statusReason from rawDbRecord when present", () => { + const msg = buildRevertMessage(fakeReceipt, { statusReason: "barter minReturn (100) < user required (200)" }) + expect(msg).toContain("barter minReturn") + }) + + it("includes revertReason from rawDbRecord when present", () => { + const msg = buildRevertMessage(fakeReceipt, { revertReason: "INSUFFICIENT_OUTPUT_AMOUNT" }) + expect(msg).toContain("INSUFFICIENT_OUTPUT_AMOUNT") + }) +}) + +describe("mapErrorMessage via getTransactionShortMessage", () => { + it("RPCError with receipt passes through actual message, not 'Network error'", () => { + const err = new RPCError(buildRevertMessage(fakeReceipt), fakeReceipt) + const short = getTransactionShortMessage(err) + expect(short).not.toContain("Network error") + expect(short).toContain("reverted on-chain") + }) + + it("non-RPCError with 'rpc' in message still gets Network error", () => { + const err = new Error("rpc endpoint unreachable") + const short = getTransactionShortMessage(err) + expect(short).toBe("Network error") + }) + + it("non-RPCError with 'fetch' in message still gets Network error", () => { + const err = new Error("failed to fetch") + const short = getTransactionShortMessage(err) + expect(short).toBe("Network error") + }) + + it("unknown error without matching keywords passes through raw message", () => { + const err = new Error("something completely unexpected happened") + const short = getTransactionShortMessage(err) + expect(short).toBe("something completely unexpected happened") + }) + + it("user rejection is recognized", () => { + const err = new Error("user rejected the request") + const short = getTransactionShortMessage(err) + expect(short).toBe("Transaction Cancelled in Wallet") + }) + + it("insufficient funds is recognized", () => { + const err = new Error("insufficient funds for gas") + const short = getTransactionShortMessage(err) + expect(short).toBe("Insufficient funds for gas fees") + }) +}) + +describe("getTransactionFullMessage", () => { + it("RPCError with receipt shows full buildRevertMessage content", () => { + const msg = buildRevertMessage(fakeReceipt, { statusReason: "slippage exceeded" }) + const err = new RPCError(msg, fakeReceipt) + const full = getTransactionFullMessage(err) + expect(full).toContain("reverted on-chain") + expect(full).toContain("slippage exceeded") + expect(full).toContain("0x12345678") + }) + + it("plain Error preserves original message", () => { + const err = new Error("something weird from the relayer: code 500 internal") + const full = getTransactionFullMessage(err) + expect(full).toContain("something weird from the relayer: code 500 internal") + }) +}) + +describe("getTransactionErrorMessage", () => { + it("RPCError with receipt does not return network error boilerplate", () => { + const err = new RPCError(buildRevertMessage(fakeReceipt), fakeReceipt) + const msg = getTransactionErrorMessage(err) + expect(msg).not.toContain("Unable to connect to the blockchain") + expect(msg).toContain("reverted on-chain") + }) + + it("genuine network error surfaces the actual error after the prefix", () => { + const err = new Error("HttpRequestError: Request to https://fastrpc.mev-commit.xyz failed (status 503)") + const msg = getTransactionErrorMessage(err) + expect(msg.startsWith("Network error: ")).toBe(true) + expect(msg).toContain("503") + expect(msg).toContain("fastrpc.mev-commit.xyz") + expect(msg).not.toContain("Unable to connect to the blockchain") + }) +}) + +describe("getTransactionErrorTitle", () => { + it("reverted tx shows 'Failed' not 'Cancelled'", () => { + const err = new RPCError(buildRevertMessage(fakeReceipt), fakeReceipt) + expect(getTransactionErrorTitle(err, "swap")).toBe("Swap Failed") + }) +}) diff --git a/src/lib/transaction-errors.ts b/src/lib/transaction-errors.ts index d0341bd2..ff703934 100644 --- a/src/lib/transaction-errors.ts +++ b/src/lib/transaction-errors.ts @@ -62,6 +62,30 @@ export class RPCError extends Error { } } +/** + * Builds a human-readable message from a reverted transaction receipt. + */ +export function buildRevertMessage(receipt: TransactionReceipt, rawDbRecord?: unknown): string { + const hash = receipt.transactionHash + const short = hash ? `${hash.slice(0, 10)}…${hash.slice(-8)}` : "unknown" + const block = receipt.blockNumber != null ? String(receipt.blockNumber) : "unknown" + const gasUsed = receipt.gasUsed != null ? String(receipt.gasUsed) : "unknown" + + let reason = "" + if (rawDbRecord != null && typeof rawDbRecord === "object") { + const rec = rawDbRecord as Record + if (typeof rec.statusReason === "string" && rec.statusReason) { + reason = rec.statusReason + } else if (typeof rec.revertReason === "string" && rec.revertReason) { + reason = rec.revertReason + } + } + + const parts = [`Transaction reverted on-chain (tx: ${short}, block: ${block}, gas used: ${gasUsed})`] + if (reason) parts.push(`Reason: ${reason}`) + return parts.join("\n") +} + /** * Shared logic to map complex error strings to human-readable summaries. */ @@ -89,6 +113,12 @@ function mapErrorMessage(error: unknown): string | null { return "Barter API key invalid or missing." } + // RPCError with a receipt is an on-chain revert, not a network issue — let + // the actual message (from buildRevertMessage) flow through unmodified. + if (error instanceof RPCError && error.receipt) { + return null + } + if ( message.includes("failed to fetch") || message.includes("rpc") || @@ -134,9 +164,11 @@ export function getTransactionErrorMessage(error: unknown): string { const mapped = mapErrorMessage(error) if (mapped) { - return mapped === "Network error" - ? "Network error: Unable to connect to the blockchain. Please check your internet connection or RPC settings." - : mapped + if (mapped === "Network error") { + const raw = error instanceof Error ? error.message : String(error) + return `Network error: ${raw}` + } + return mapped } return error instanceof Error ? error.message : String(error) From edc8f6f34f010f817f3450d75c22675c4099d260 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Mon, 13 Apr 2026 11:38:52 -0300 Subject: [PATCH 2/3] style: format test file --- src/lib/__tests__/transaction-errors.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib/__tests__/transaction-errors.test.ts b/src/lib/__tests__/transaction-errors.test.ts index 6fd56cac..9824d9a0 100644 --- a/src/lib/__tests__/transaction-errors.test.ts +++ b/src/lib/__tests__/transaction-errors.test.ts @@ -36,7 +36,9 @@ describe("buildRevertMessage", () => { }) it("includes statusReason from rawDbRecord when present", () => { - const msg = buildRevertMessage(fakeReceipt, { statusReason: "barter minReturn (100) < user required (200)" }) + const msg = buildRevertMessage(fakeReceipt, { + statusReason: "barter minReturn (100) < user required (200)", + }) expect(msg).toContain("barter minReturn") }) @@ -111,7 +113,9 @@ describe("getTransactionErrorMessage", () => { }) it("genuine network error surfaces the actual error after the prefix", () => { - const err = new Error("HttpRequestError: Request to https://fastrpc.mev-commit.xyz failed (status 503)") + const err = new Error( + "HttpRequestError: Request to https://fastrpc.mev-commit.xyz failed (status 503)" + ) const msg = getTransactionErrorMessage(err) expect(msg.startsWith("Network error: ")).toBe(true) expect(msg).toContain("503") From cd23dca1e675901ee85c04fe6be7a18b50ffe7c1 Mon Sep 17 00:00:00 2001 From: Jason Schwarz Date: Mon, 13 Apr 2026 11:49:55 -0300 Subject: [PATCH 3/3] style: format transaction-errors --- src/lib/transaction-errors.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/transaction-errors.ts b/src/lib/transaction-errors.ts index ff703934..f110869b 100644 --- a/src/lib/transaction-errors.ts +++ b/src/lib/transaction-errors.ts @@ -81,7 +81,9 @@ export function buildRevertMessage(receipt: TransactionReceipt, rawDbRecord?: un } } - const parts = [`Transaction reverted on-chain (tx: ${short}, block: ${block}, gas used: ${gasUsed})`] + const parts = [ + `Transaction reverted on-chain (tx: ${short}, block: ${block}, gas used: ${gasUsed})`, + ] if (reason) parts.push(`Reason: ${reason}`) return parts.join("\n") }