Skip to content

Commit 741186c

Browse files
authored
Merge pull request #112 from primev/network-error
fix(swap): surface actual errors instead of catch-all Network error
2 parents 57b1eb1 + cd23dca commit 741186c

3 files changed

Lines changed: 176 additions & 6 deletions

File tree

src/hooks/use-wait-for-tx-confirmation.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { fetchFastTxStatus } from "@/lib/fast-tx-status"
66
import { fetchTransactionReceiptFromDb } from "@/lib/transaction-receipt-utils"
77
import { fetchCommitmentStatus } from "@/lib/fast-rpc-status"
88
import { getTxConfirmationTimeoutMs } from "@/lib/tx-config"
9-
import { RPCError } from "@/lib/transaction-errors"
9+
import { RPCError, buildRevertMessage } from "@/lib/transaction-errors"
1010

1111
/**
1212
* Adaptive polling: starts fast to catch sub-second preconfirmations,
@@ -113,7 +113,7 @@ export function useWaitForTxConfirmation({
113113
try {
114114
if (receipt.status === "reverted") {
115115
hasConfirmedRef.current = true
116-
const e = new RPCError("RPC Error", receipt)
116+
const e = new RPCError(buildRevertMessage(receipt), receipt)
117117
setError(e)
118118
onErrorRef.current?.(e)
119119
return
@@ -238,7 +238,11 @@ export function useWaitForTxConfirmation({
238238
hasConfirmedRef.current = true
239239
abortController.abort()
240240
clearInterval(dbPollInterval)
241-
const e = new RPCError("RPC Error", rpcResult.receipt, rpcResult.rawResult)
241+
const e = new RPCError(
242+
buildRevertMessage(rpcResult.receipt, rpcResult.rawResult),
243+
rpcResult.receipt,
244+
rpcResult.rawResult
245+
)
242246
setError(e)
243247
onErrorRef.current?.(e)
244248
return
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { describe, it, expect } from "vitest"
2+
import type { TransactionReceipt } from "viem"
3+
import {
4+
RPCError,
5+
buildRevertMessage,
6+
getTransactionShortMessage,
7+
getTransactionFullMessage,
8+
getTransactionErrorMessage,
9+
getTransactionErrorTitle,
10+
} from "../transaction-errors"
11+
12+
const fakeReceipt: TransactionReceipt = {
13+
transactionHash: "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
14+
blockNumber: 12345n,
15+
gasUsed: 98765n,
16+
status: "reverted",
17+
transactionIndex: 0,
18+
blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
19+
from: "0x0000000000000000000000000000000000000001",
20+
to: "0x0000000000000000000000000000000000000002",
21+
cumulativeGasUsed: 98765n,
22+
contractAddress: null,
23+
logs: [],
24+
logsBloom: "0x00",
25+
type: "0x2",
26+
effectiveGasPrice: 1000000000n,
27+
}
28+
29+
describe("buildRevertMessage", () => {
30+
it("includes tx hash, block, and gas from receipt", () => {
31+
const msg = buildRevertMessage(fakeReceipt)
32+
expect(msg).toContain("0x12345678…90abcdef")
33+
expect(msg).toContain("12345")
34+
expect(msg).toContain("98765")
35+
expect(msg).toContain("reverted on-chain")
36+
})
37+
38+
it("includes statusReason from rawDbRecord when present", () => {
39+
const msg = buildRevertMessage(fakeReceipt, {
40+
statusReason: "barter minReturn (100) < user required (200)",
41+
})
42+
expect(msg).toContain("barter minReturn")
43+
})
44+
45+
it("includes revertReason from rawDbRecord when present", () => {
46+
const msg = buildRevertMessage(fakeReceipt, { revertReason: "INSUFFICIENT_OUTPUT_AMOUNT" })
47+
expect(msg).toContain("INSUFFICIENT_OUTPUT_AMOUNT")
48+
})
49+
})
50+
51+
describe("mapErrorMessage via getTransactionShortMessage", () => {
52+
it("RPCError with receipt passes through actual message, not 'Network error'", () => {
53+
const err = new RPCError(buildRevertMessage(fakeReceipt), fakeReceipt)
54+
const short = getTransactionShortMessage(err)
55+
expect(short).not.toContain("Network error")
56+
expect(short).toContain("reverted on-chain")
57+
})
58+
59+
it("non-RPCError with 'rpc' in message still gets Network error", () => {
60+
const err = new Error("rpc endpoint unreachable")
61+
const short = getTransactionShortMessage(err)
62+
expect(short).toBe("Network error")
63+
})
64+
65+
it("non-RPCError with 'fetch' in message still gets Network error", () => {
66+
const err = new Error("failed to fetch")
67+
const short = getTransactionShortMessage(err)
68+
expect(short).toBe("Network error")
69+
})
70+
71+
it("unknown error without matching keywords passes through raw message", () => {
72+
const err = new Error("something completely unexpected happened")
73+
const short = getTransactionShortMessage(err)
74+
expect(short).toBe("something completely unexpected happened")
75+
})
76+
77+
it("user rejection is recognized", () => {
78+
const err = new Error("user rejected the request")
79+
const short = getTransactionShortMessage(err)
80+
expect(short).toBe("Transaction Cancelled in Wallet")
81+
})
82+
83+
it("insufficient funds is recognized", () => {
84+
const err = new Error("insufficient funds for gas")
85+
const short = getTransactionShortMessage(err)
86+
expect(short).toBe("Insufficient funds for gas fees")
87+
})
88+
})
89+
90+
describe("getTransactionFullMessage", () => {
91+
it("RPCError with receipt shows full buildRevertMessage content", () => {
92+
const msg = buildRevertMessage(fakeReceipt, { statusReason: "slippage exceeded" })
93+
const err = new RPCError(msg, fakeReceipt)
94+
const full = getTransactionFullMessage(err)
95+
expect(full).toContain("reverted on-chain")
96+
expect(full).toContain("slippage exceeded")
97+
expect(full).toContain("0x12345678")
98+
})
99+
100+
it("plain Error preserves original message", () => {
101+
const err = new Error("something weird from the relayer: code 500 internal")
102+
const full = getTransactionFullMessage(err)
103+
expect(full).toContain("something weird from the relayer: code 500 internal")
104+
})
105+
})
106+
107+
describe("getTransactionErrorMessage", () => {
108+
it("RPCError with receipt does not return network error boilerplate", () => {
109+
const err = new RPCError(buildRevertMessage(fakeReceipt), fakeReceipt)
110+
const msg = getTransactionErrorMessage(err)
111+
expect(msg).not.toContain("Unable to connect to the blockchain")
112+
expect(msg).toContain("reverted on-chain")
113+
})
114+
115+
it("genuine network error surfaces the actual error after the prefix", () => {
116+
const err = new Error(
117+
"HttpRequestError: Request to https://fastrpc.mev-commit.xyz failed (status 503)"
118+
)
119+
const msg = getTransactionErrorMessage(err)
120+
expect(msg.startsWith("Network error: ")).toBe(true)
121+
expect(msg).toContain("503")
122+
expect(msg).toContain("fastrpc.mev-commit.xyz")
123+
expect(msg).not.toContain("Unable to connect to the blockchain")
124+
})
125+
})
126+
127+
describe("getTransactionErrorTitle", () => {
128+
it("reverted tx shows 'Failed' not 'Cancelled'", () => {
129+
const err = new RPCError(buildRevertMessage(fakeReceipt), fakeReceipt)
130+
expect(getTransactionErrorTitle(err, "swap")).toBe("Swap Failed")
131+
})
132+
})

src/lib/transaction-errors.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,32 @@ export class RPCError extends Error {
6262
}
6363
}
6464

65+
/**
66+
* Builds a human-readable message from a reverted transaction receipt.
67+
*/
68+
export function buildRevertMessage(receipt: TransactionReceipt, rawDbRecord?: unknown): string {
69+
const hash = receipt.transactionHash
70+
const short = hash ? `${hash.slice(0, 10)}${hash.slice(-8)}` : "unknown"
71+
const block = receipt.blockNumber != null ? String(receipt.blockNumber) : "unknown"
72+
const gasUsed = receipt.gasUsed != null ? String(receipt.gasUsed) : "unknown"
73+
74+
let reason = ""
75+
if (rawDbRecord != null && typeof rawDbRecord === "object") {
76+
const rec = rawDbRecord as Record<string, unknown>
77+
if (typeof rec.statusReason === "string" && rec.statusReason) {
78+
reason = rec.statusReason
79+
} else if (typeof rec.revertReason === "string" && rec.revertReason) {
80+
reason = rec.revertReason
81+
}
82+
}
83+
84+
const parts = [
85+
`Transaction reverted on-chain (tx: ${short}, block: ${block}, gas used: ${gasUsed})`,
86+
]
87+
if (reason) parts.push(`Reason: ${reason}`)
88+
return parts.join("\n")
89+
}
90+
6591
/**
6692
* Shared logic to map complex error strings to human-readable summaries.
6793
*/
@@ -89,6 +115,12 @@ function mapErrorMessage(error: unknown): string | null {
89115
return "Barter API key invalid or missing."
90116
}
91117

118+
// RPCError with a receipt is an on-chain revert, not a network issue — let
119+
// the actual message (from buildRevertMessage) flow through unmodified.
120+
if (error instanceof RPCError && error.receipt) {
121+
return null
122+
}
123+
92124
if (
93125
message.includes("failed to fetch") ||
94126
message.includes("rpc") ||
@@ -134,9 +166,11 @@ export function getTransactionErrorMessage(error: unknown): string {
134166

135167
const mapped = mapErrorMessage(error)
136168
if (mapped) {
137-
return mapped === "Network error"
138-
? "Network error: Unable to connect to the blockchain. Please check your internet connection or RPC settings."
139-
: mapped
169+
if (mapped === "Network error") {
170+
const raw = error instanceof Error ? error.message : String(error)
171+
return `Network error: ${raw}`
172+
}
173+
return mapped
140174
}
141175

142176
return error instanceof Error ? error.message : String(error)

0 commit comments

Comments
 (0)