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
10 changes: 7 additions & 3 deletions src/hooks/use-wait-for-tx-confirmation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
132 changes: 132 additions & 0 deletions src/lib/__tests__/transaction-errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
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")
})
})
40 changes: 37 additions & 3 deletions src/lib/transaction-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,32 @@ 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<string, unknown>
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.
*/
Expand Down Expand Up @@ -89,6 +115,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") ||
Expand Down Expand Up @@ -134,9 +166,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)
Expand Down
Loading