From 195b0218be590a1d3ccc26704cd15d3c29122ca9 Mon Sep 17 00:00:00 2001 From: owen-eth Date: Wed, 25 Mar 2026 14:11:13 -0400 Subject: [PATCH 01/10] perf: decouple slow DB poll from fast RPC polling for preconfirmation detection Phase 1 now only polls the two fast sources (commitment status + RPC receipt) that hit FastRPC directly. The slow mctransactions DB call (via StarRocks) runs independently on a 2s interval for failure detection only, never blocking the fast path. Previously Promise.all waited for all three sources including the slow DB query, meaning each poll cycle was bottlenecked by StarRocks latency (~1-2s) even though the RPC returns in ~50-100ms. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/hooks/use-wait-for-tx-confirmation.ts | 136 ++++++++++++---------- 1 file changed, 73 insertions(+), 63 deletions(-) diff --git a/src/hooks/use-wait-for-tx-confirmation.ts b/src/hooks/use-wait-for-tx-confirmation.ts index 747c3b6d..a736f7c0 100644 --- a/src/hooks/use-wait-for-tx-confirmation.ts +++ b/src/hooks/use-wait-for-tx-confirmation.ts @@ -15,6 +15,8 @@ import { RPCError } from "@/lib/transaction-errors" const FAST_POLL_INTERVAL_MS = 100 const NORMAL_POLL_INTERVAL_MS = 500 const FAST_POLL_COUNT = 5 +/** DB failure detection runs independently on a slower cadence */ +const DB_POLL_INTERVAL_MS = 2000 function getPollInterval(pollCount: number): number { return pollCount < FAST_POLL_COUNT ? FAST_POLL_INTERVAL_MS : NORMAL_POLL_INTERVAL_MS @@ -35,7 +37,7 @@ export interface UseWaitForTxConfirmationParams { receiptError?: Error | null mode: WaitForTxConfirmationMode onConfirmed: (result: TxConfirmationResult) => void - /** Called when RPC receipt or mctransactions reports preconfirmed. */ + /** Called when RPC receipt or commitments report preconfirmed. */ onPreConfirmed?: (result: TxConfirmationResult) => void onError?: (error: Error) => void } @@ -47,19 +49,20 @@ export interface UseWaitForTxConfirmationReturn { } /** - * Two-phase polling with Wagmi as parallel fallback: + * Two-phase polling with decoupled failure detection: * * Phase 1 (pending → preconfirmed): - * Poll BOTH eth_getTransactionReceipt (FastRPC) and mctransactions in parallel. - * First source to show success/preconfirmed fires onPreConfirmed. - * mctransactions "failed" in this phase fires onError immediately. + * Fast loop: poll commitment status + RPC receipt (both hit FastRPC directly). + * Background: poll mctransactions DB every 2s for failure detection only. + * First source to show preconfirmed fires onPreConfirmed. * * Phase 2 (preconfirmed → final): - * Stop RPC receipt polling. Poll only mctransactions for confirmed/failed. - * mctransactions "confirmed" → fire onConfirmed (final success). - * mctransactions "failed" → fire onError. + * Poll mctransactions + RPC receipt for confirmed/failed. * * Wagmi receipt (on-chain) stays active throughout as a parallel fallback. + * + * Key optimization: the slow DB call (mctransactions via StarRocks) never + * blocks the fast RPC calls. Preconfirmation detection runs at RPC speed. */ export function useWaitForTxConfirmation({ hash, @@ -144,7 +147,7 @@ export function useWaitForTxConfirmation({ onErrorRef.current?.(e) }, [hash, receiptError]) - // Effect: Two-phase polling + // Effect: Two-phase polling with decoupled DB failure detection useEffect(() => { if (!hash || processingHashRef.current === hash) return @@ -176,9 +179,44 @@ export function useWaitForTxConfirmation({ const startTime = Date.now() let pollCount = 0 - // ── Phase 1: Poll both RPC receipt and mctransactions ── + // Background DB poll for failure detection (runs independently, never blocks fast path) + const dbPollInterval = setInterval(async () => { + if (abortController.signal.aborted || hasConfirmedRef.current) return + try { + const mcStatus = await fetchFastTxStatus(hash, abortController.signal) + if (abortController.signal.aborted || hasConfirmedRef.current) return + + if (mcStatus === "failed") { + hasConfirmedRef.current = true + abortController.abort() + const e = new Error("Transaction was dropped by the network.") + setError(e) + onErrorRef.current?.(e) + } else if (mcStatus === "confirmed") { + // DB caught up — fire confirmed if we haven't already + if (!hasConfirmedRef.current) { + hasConfirmedRef.current = true + abortController.abort() + setIsConfirmed(true) + const result: TxConfirmationResult = + mode === "receipt" + ? { source: "db" } + : { source: "db", status: { success: true, hash } } + firePreConfirmed() + onConfirmedRef.current(result) + } + } else if (mcStatus === "preconfirmed") { + firePreConfirmed() + } + } catch { + // Ignore DB poll errors — fast path handles preconfirmation + } + }, DB_POLL_INTERVAL_MS) + + // ── Phase 1: Fast RPC polling for preconfirmation ── while (!abortController.signal.aborted && !hasConfirmedRef.current) { if (Date.now() - startTime > timeoutMs) { + clearInterval(dbPollInterval) const e = new Error( "Transaction confirmation timed out — your swap may have still succeeded. Check your wallet." ) @@ -187,69 +225,42 @@ export function useWaitForTxConfirmation({ return } - // Poll three sources in parallel: - // 1. FastRPC commitment status (fastest — node knows instantly) - // 2. RPC eth_getTransactionReceipt - // 3. mctransactions DB status (slowest — lags ~15s) - const [commitStatus, rpcResult, mcStatus] = await Promise.all([ + // Poll only fast sources — both hit FastRPC directly + const [commitStatus, rpcResult] = await Promise.all([ fetchCommitmentStatus(hash, abortController.signal), fetchTransactionReceiptFromDb(hash, abortController.signal), - fetchFastTxStatus(hash, abortController.signal), ]) - if (abortController.signal.aborted || hasConfirmedRef.current) return - - // mctransactions "failed" → immediate error (dropped tx) - if (mcStatus === "failed") { - hasConfirmedRef.current = true - abortController.abort() - const e = rpcResult - ? new RPCError("Transaction failed", rpcResult.receipt, rpcResult.rawResult) - : new Error("Transaction was dropped by the network.") - setError(e) - onErrorRef.current?.(e) - return - } + if (abortController.signal.aborted || hasConfirmedRef.current) break // RPC receipt with reverted status → immediate error if (rpcResult && rpcResult.receipt.status === "reverted") { hasConfirmedRef.current = true abortController.abort() + clearInterval(dbPollInterval) const e = new RPCError("RPC Error", rpcResult.receipt, rpcResult.rawResult) setError(e) onErrorRef.current?.(e) return } - // Any source signals preconfirmed → fire and move to phase 2 + // Either fast source signals preconfirmed → fire and move to phase 2 if ( commitStatus === "preconfirmed" || - mcStatus === "preconfirmed" || - mcStatus === "confirmed" || (rpcResult && rpcResult.receipt.status === "success") ) { firePreConfirmed() - // If mctransactions already says confirmed, finish now - if (mcStatus === "confirmed") { - hasConfirmedRef.current = true - abortController.abort() - setIsConfirmed(true) - const result: TxConfirmationResult = - mode === "receipt" - ? { source: "db" } - : { source: "db", status: { success: true, hash } } - onConfirmedRef.current(result) - return - } break // → Phase 2 } await new Promise((r) => setTimeout(r, getPollInterval(pollCount++))) } - // ── Phase 2: Poll only mctransactions for confirmed/failed ── + // ── Phase 2: Poll for confirmed/failed ── + // RPC receipt + DB status both useful here (L1 inclusion takes 12s+ so DB lag doesn't matter) while (!abortController.signal.aborted && !hasConfirmedRef.current) { if (Date.now() - startTime > timeoutMs) { + clearInterval(dbPollInterval) const e = new Error( "Transaction confirmation timed out — your swap may have still succeeded. Check your wallet." ) @@ -258,17 +269,25 @@ export function useWaitForTxConfirmation({ return } - const mcStatus = await fetchFastTxStatus(hash, abortController.signal) + const [mcStatus, rpcResult] = await Promise.all([ + fetchFastTxStatus(hash, abortController.signal), + fetchTransactionReceiptFromDb(hash, abortController.signal), + ]) - if (abortController.signal.aborted || hasConfirmedRef.current) return + if (abortController.signal.aborted || hasConfirmedRef.current) break - if (mcStatus === "confirmed") { + // Either source confirms + if ( + mcStatus === "confirmed" || + (rpcResult && rpcResult.receipt.status === "success" && rpcResult.receipt.blockNumber > 0n) + ) { hasConfirmedRef.current = true abortController.abort() + clearInterval(dbPollInterval) setIsConfirmed(true) const result: TxConfirmationResult = mode === "receipt" - ? { source: "db" } + ? { source: "db", receipt: rpcResult?.receipt } : { source: "db", status: { success: true, hash } } onConfirmedRef.current(result) return @@ -277,21 +296,10 @@ export function useWaitForTxConfirmation({ if (mcStatus === "failed") { hasConfirmedRef.current = true abortController.abort() + clearInterval(dbPollInterval) - let dbReceipt: TransactionReceipt | undefined - let rawResult: unknown - try { - const receiptResult = await fetchTransactionReceiptFromDb(hash) - if (receiptResult) { - dbReceipt = receiptResult.receipt - rawResult = receiptResult.rawResult - } - } catch { - // Best-effort for error details - } - - const e = dbReceipt - ? new RPCError("Transaction failed", dbReceipt, rawResult) + const e = rpcResult + ? new RPCError("Transaction failed", rpcResult.receipt, rpcResult.rawResult) : new Error("Transaction was dropped by the network.") setError(e) onErrorRef.current?.(e) @@ -300,6 +308,8 @@ export function useWaitForTxConfirmation({ await new Promise((r) => setTimeout(r, NORMAL_POLL_INTERVAL_MS)) } + + clearInterval(dbPollInterval) } catch (err) { if ((err as Error).name !== "AbortError" && !hasConfirmedRef.current) { const e = err instanceof Error ? err : new Error(String(err)) From ab5ba1ec7cacfe6d760bfdbcf9f6bf22425a7b4c Mon Sep 17 00:00:00 2001 From: owen-eth Date: Wed, 25 Mar 2026 15:52:12 -0400 Subject: [PATCH 02/10] fix: remove wagmi receipt from toast to prevent premature confirmed status Wagmi's useWaitForTransactionReceipt treated FastRPC's simulated preconf receipt as a real on-chain receipt, causing the toast to jump straight to "Tokens Available" (confirmed) and skip the "Preconfirmed" state entirely. Now passes receipt: undefined to useWaitForTxConfirmation so only our custom polling detects status transitions. Also adds console logging for preconf/confirmed/error events for debugging. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/swap/SwapToast.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/swap/SwapToast.tsx b/src/components/swap/SwapToast.tsx index 75852195..f62d82f7 100644 --- a/src/components/swap/SwapToast.tsx +++ b/src/components/swap/SwapToast.tsx @@ -3,8 +3,6 @@ import { useEffect, useRef } from "react" import Image from "next/image" import { motion, AnimatePresence } from "motion/react" -import { useWaitForTransactionReceipt } from "wagmi" -import type { TransactionReceipt } from "viem" import { X, RefreshCw, ExternalLink } from "lucide-react" import { FaXTwitter } from "react-icons/fa6" import { useSwapToastStore } from "@/stores/swapToastStore" @@ -36,22 +34,24 @@ export function SwapToast({ hash }: { hash: string }) { const toastRef = useRef(null) const effectiveHash = hash.startsWith("pending-") ? undefined : hash - const { data: receipt, error: receiptError } = useWaitForTransactionReceipt({ - hash: effectiveHash as `0x${string}` | undefined, - }) - + // Poll for tx status — NO wagmi useWaitForTransactionReceipt here. + // Wagmi treats FastRPC's simulated preconf receipt as a real on-chain receipt, + // which caused "confirmed" to fire before "preconfirmed". Our custom polling + // hook handles the preconfirmed → confirmed lifecycle correctly. useWaitForTxConfirmation({ hash: effectiveHash ?? undefined, - receipt: (receipt as TransactionReceipt | undefined) ?? undefined, - receiptError, + receipt: undefined, // No wagmi receipt — avoids the premature confirmed race + receiptError: undefined, mode: "status", onConfirmed: () => { + console.log(`[SwapToast] onConfirmed hash=${effectiveHash}`) if (effectiveHash) setStatus(hash, "confirmed") const t = useSwapToastStore.getState().toasts.find((x) => x.hash === hash) t?.onConfirm?.() }, onPreConfirmed: () => { const currentStatus = useSwapToastStore.getState().toasts.find((t) => t.hash === hash)?.status + console.log(`[SwapToast] onPreConfirmed hash=${effectiveHash} currentStatus=${currentStatus}`) if (effectiveHash && currentStatus !== "confirmed") { setStatus(hash, "preconfirmed") playPreconfirmSound() @@ -60,6 +60,7 @@ export function SwapToast({ hash }: { hash: string }) { } }, onError: (err) => { + console.log(`[SwapToast] onError hash=${effectiveHash}`, err.message) const txReceipt = err instanceof RPCError ? err.receipt : undefined const rawDbRecord = err instanceof RPCError ? err.rawDbRecord : undefined const message = From 785566eb153520cd645ebe6ee82aebe90a69df08 Mon Sep 17 00:00:00 2001 From: owen-eth Date: Wed, 25 Mar 2026 16:06:49 -0400 Subject: [PATCH 03/10] chore: add timestamps to SwapToast console logs Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/swap/SwapToast.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/swap/SwapToast.tsx b/src/components/swap/SwapToast.tsx index f62d82f7..c33cb35d 100644 --- a/src/components/swap/SwapToast.tsx +++ b/src/components/swap/SwapToast.tsx @@ -44,14 +44,14 @@ export function SwapToast({ hash }: { hash: string }) { receiptError: undefined, mode: "status", onConfirmed: () => { - console.log(`[SwapToast] onConfirmed hash=${effectiveHash}`) + console.log(`[SwapToast] onConfirmed hash=${effectiveHash} t=${Date.now()}`) if (effectiveHash) setStatus(hash, "confirmed") const t = useSwapToastStore.getState().toasts.find((x) => x.hash === hash) t?.onConfirm?.() }, onPreConfirmed: () => { const currentStatus = useSwapToastStore.getState().toasts.find((t) => t.hash === hash)?.status - console.log(`[SwapToast] onPreConfirmed hash=${effectiveHash} currentStatus=${currentStatus}`) + console.log(`[SwapToast] onPreConfirmed hash=${effectiveHash} currentStatus=${currentStatus} t=${Date.now()}`) if (effectiveHash && currentStatus !== "confirmed") { setStatus(hash, "preconfirmed") playPreconfirmSound() @@ -60,7 +60,7 @@ export function SwapToast({ hash }: { hash: string }) { } }, onError: (err) => { - console.log(`[SwapToast] onError hash=${effectiveHash}`, err.message) + console.log(`[SwapToast] onError hash=${effectiveHash} t=${Date.now()}`, err.message) const txReceipt = err instanceof RPCError ? err.receipt : undefined const rawDbRecord = err instanceof RPCError ? err.rawDbRecord : undefined const message = From 5fd596cc25de2f914273012af23271b7c691af31 Mon Sep 17 00:00:00 2001 From: owen-eth Date: Wed, 25 Mar 2026 16:12:33 -0400 Subject: [PATCH 04/10] chore: human-readable timing in SwapToast logs Shows elapsed seconds from submit and gap between preconf/confirmed. Example: [SwapToast] PRECONFIRMED | +1.23s from submit [SwapToast] CONFIRMED | +13.45s from submit | +12.22s from preconf Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/swap/SwapToast.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/swap/SwapToast.tsx b/src/components/swap/SwapToast.tsx index c33cb35d..a552d09a 100644 --- a/src/components/swap/SwapToast.tsx +++ b/src/components/swap/SwapToast.tsx @@ -44,23 +44,28 @@ export function SwapToast({ hash }: { hash: string }) { receiptError: undefined, mode: "status", onConfirmed: () => { - console.log(`[SwapToast] onConfirmed hash=${effectiveHash} t=${Date.now()}`) - if (effectiveHash) setStatus(hash, "confirmed") const t = useSwapToastStore.getState().toasts.find((x) => x.hash === hash) + const elapsed = t?.createdAt ? ((Date.now() - t.createdAt) / 1000).toFixed(2) : "?" + const sincePreconf = t?.preconfirmedAt ? ((Date.now() - t.preconfirmedAt) / 1000).toFixed(2) : "n/a" + console.log(`[SwapToast] CONFIRMED | +${elapsed}s from submit | +${sincePreconf}s from preconf | hash=${effectiveHash?.slice(0, 14)}...`) + if (effectiveHash) setStatus(hash, "confirmed") t?.onConfirm?.() }, onPreConfirmed: () => { const currentStatus = useSwapToastStore.getState().toasts.find((t) => t.hash === hash)?.status - console.log(`[SwapToast] onPreConfirmed hash=${effectiveHash} currentStatus=${currentStatus} t=${Date.now()}`) + const t = useSwapToastStore.getState().toasts.find((x) => x.hash === hash) + const elapsed = t?.createdAt ? ((Date.now() - t.createdAt) / 1000).toFixed(2) : "?" + console.log(`[SwapToast] PRECONFIRMED | +${elapsed}s from submit | status was ${currentStatus} | hash=${effectiveHash?.slice(0, 14)}...`) if (effectiveHash && currentStatus !== "confirmed") { setStatus(hash, "preconfirmed") playPreconfirmSound() - const t = useSwapToastStore.getState().toasts.find((x) => x.hash === hash) t?.onPreConfirm?.() } }, onError: (err) => { - console.log(`[SwapToast] onError hash=${effectiveHash} t=${Date.now()}`, err.message) + const t = useSwapToastStore.getState().toasts.find((x) => x.hash === hash) + const elapsed = t?.createdAt ? ((Date.now() - t.createdAt) / 1000).toFixed(2) : "?" + console.log(`[SwapToast] ERROR | +${elapsed}s from submit | hash=${effectiveHash?.slice(0, 14)}...`, err.message) const txReceipt = err instanceof RPCError ? err.receipt : undefined const rawDbRecord = err instanceof RPCError ? err.rawDbRecord : undefined const message = From 3c3cf1a1a03b03e8140cef19aabe38af600844e9 Mon Sep 17 00:00:00 2001 From: owen-eth Date: Wed, 25 Mar 2026 16:21:12 -0400 Subject: [PATCH 05/10] chore: show full tx hash in logs, add polling start log Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/swap/SwapToast.tsx | 6 +++--- src/hooks/use-wait-for-tx-confirmation.ts | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/swap/SwapToast.tsx b/src/components/swap/SwapToast.tsx index a552d09a..4bdb81ae 100644 --- a/src/components/swap/SwapToast.tsx +++ b/src/components/swap/SwapToast.tsx @@ -47,7 +47,7 @@ export function SwapToast({ hash }: { hash: string }) { const t = useSwapToastStore.getState().toasts.find((x) => x.hash === hash) const elapsed = t?.createdAt ? ((Date.now() - t.createdAt) / 1000).toFixed(2) : "?" const sincePreconf = t?.preconfirmedAt ? ((Date.now() - t.preconfirmedAt) / 1000).toFixed(2) : "n/a" - console.log(`[SwapToast] CONFIRMED | +${elapsed}s from submit | +${sincePreconf}s from preconf | hash=${effectiveHash?.slice(0, 14)}...`) + console.log(`[SwapToast] CONFIRMED | +${elapsed}s from submit | +${sincePreconf}s from preconf | hash=${effectiveHash}`) if (effectiveHash) setStatus(hash, "confirmed") t?.onConfirm?.() }, @@ -55,7 +55,7 @@ export function SwapToast({ hash }: { hash: string }) { const currentStatus = useSwapToastStore.getState().toasts.find((t) => t.hash === hash)?.status const t = useSwapToastStore.getState().toasts.find((x) => x.hash === hash) const elapsed = t?.createdAt ? ((Date.now() - t.createdAt) / 1000).toFixed(2) : "?" - console.log(`[SwapToast] PRECONFIRMED | +${elapsed}s from submit | status was ${currentStatus} | hash=${effectiveHash?.slice(0, 14)}...`) + console.log(`[SwapToast] PRECONFIRMED | +${elapsed}s from submit | status was ${currentStatus} | hash=${effectiveHash}`) if (effectiveHash && currentStatus !== "confirmed") { setStatus(hash, "preconfirmed") playPreconfirmSound() @@ -65,7 +65,7 @@ export function SwapToast({ hash }: { hash: string }) { onError: (err) => { const t = useSwapToastStore.getState().toasts.find((x) => x.hash === hash) const elapsed = t?.createdAt ? ((Date.now() - t.createdAt) / 1000).toFixed(2) : "?" - console.log(`[SwapToast] ERROR | +${elapsed}s from submit | hash=${effectiveHash?.slice(0, 14)}...`, err.message) + console.log(`[SwapToast] ERROR | +${elapsed}s from submit | hash=${effectiveHash}`, err.message) const txReceipt = err instanceof RPCError ? err.receipt : undefined const rawDbRecord = err instanceof RPCError ? err.rawDbRecord : undefined const message = diff --git a/src/hooks/use-wait-for-tx-confirmation.ts b/src/hooks/use-wait-for-tx-confirmation.ts index a736f7c0..d26f5db5 100644 --- a/src/hooks/use-wait-for-tx-confirmation.ts +++ b/src/hooks/use-wait-for-tx-confirmation.ts @@ -153,6 +153,7 @@ export function useWaitForTxConfirmation({ processingHashRef.current = hash preConfirmedFiredRef.current = false + console.log(`[TxConfirmation] Polling started | hash=${hash}`) const abortController = new AbortController() abortRef.current = abortController setIsConfirmed(false) From 1dca26a173db2cd7861fc7721763a6a97c9c0505 Mon Sep 17 00:00:00 2001 From: owen-eth Date: Wed, 25 Mar 2026 16:47:22 -0400 Subject: [PATCH 06/10] chore: add absolute timestamps to hash ready + preconfirmed logs Allows comparison with commitment dispatch_timestamp from RPC to measure exact UI detection delay. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/swap/SwapToast.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/swap/SwapToast.tsx b/src/components/swap/SwapToast.tsx index 4bdb81ae..964d4640 100644 --- a/src/components/swap/SwapToast.tsx +++ b/src/components/swap/SwapToast.tsx @@ -33,6 +33,14 @@ export function SwapToast({ hash }: { hash: string }) { const toastRef = useRef(null) const effectiveHash = hash.startsWith("pending-") ? undefined : hash + const prevEffectiveHashRef = useRef(undefined) + + // Log when real hash arrives (permit path: placeholder → real hash swap) + if (effectiveHash && effectiveHash !== prevEffectiveHashRef.current) { + prevEffectiveHashRef.current = effectiveHash + const elapsed = toast?.createdAt ? ((Date.now() - toast.createdAt) / 1000).toFixed(2) : "?" + console.log(`[SwapToast] Hash ready | +${elapsed}s from submit | now=${Date.now()} | hash=${effectiveHash}`) + } // Poll for tx status — NO wagmi useWaitForTransactionReceipt here. // Wagmi treats FastRPC's simulated preconf receipt as a real on-chain receipt, @@ -55,7 +63,7 @@ export function SwapToast({ hash }: { hash: string }) { const currentStatus = useSwapToastStore.getState().toasts.find((t) => t.hash === hash)?.status const t = useSwapToastStore.getState().toasts.find((x) => x.hash === hash) const elapsed = t?.createdAt ? ((Date.now() - t.createdAt) / 1000).toFixed(2) : "?" - console.log(`[SwapToast] PRECONFIRMED | +${elapsed}s from submit | status was ${currentStatus} | hash=${effectiveHash}`) + console.log(`[SwapToast] PRECONFIRMED | +${elapsed}s from submit | status was ${currentStatus} | now=${Date.now()} | hash=${effectiveHash}`) if (effectiveHash && currentStatus !== "confirmed") { setStatus(hash, "preconfirmed") playPreconfirmSound() From ca2eafcf419e891d78b72ec856c3eb40de074b3f Mon Sep 17 00:00:00 2001 From: owen-eth Date: Wed, 25 Mar 2026 18:47:40 -0400 Subject: [PATCH 07/10] fix: use wagmi for confirmed detection, decoupled polling for preconfirmed Phase 1 (preconfirmation): fast polling of commitment status + RPC receipt against FastRPC directly, with DB failure detection on independent 2s interval. No Promise.all bottleneck from slow StarRocks queries. Phase 2 (confirmation): wagmi watches for real L1 on-chain receipt. Previously Phase 2 used FastRPC's eth_getTransactionReceipt which returns a simulated receipt for preconfirmed txs, causing premature "confirmed" status. Wagmi correctly distinguishes real L1 receipts from simulated ones. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/swap/SwapToast.tsx | 17 ++++++++++------ src/hooks/use-wait-for-tx-confirmation.ts | 24 ++++++++--------------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/components/swap/SwapToast.tsx b/src/components/swap/SwapToast.tsx index 964d4640..ee2ddeb1 100644 --- a/src/components/swap/SwapToast.tsx +++ b/src/components/swap/SwapToast.tsx @@ -3,6 +3,8 @@ import { useEffect, useRef } from "react" import Image from "next/image" import { motion, AnimatePresence } from "motion/react" +import { useWaitForTransactionReceipt } from "wagmi" +import type { TransactionReceipt } from "viem" import { X, RefreshCw, ExternalLink } from "lucide-react" import { FaXTwitter } from "react-icons/fa6" import { useSwapToastStore } from "@/stores/swapToastStore" @@ -42,14 +44,17 @@ export function SwapToast({ hash }: { hash: string }) { console.log(`[SwapToast] Hash ready | +${elapsed}s from submit | now=${Date.now()} | hash=${effectiveHash}`) } - // Poll for tx status — NO wagmi useWaitForTransactionReceipt here. - // Wagmi treats FastRPC's simulated preconf receipt as a real on-chain receipt, - // which caused "confirmed" to fire before "preconfirmed". Our custom polling - // hook handles the preconfirmed → confirmed lifecycle correctly. + // Wagmi watches for real on-chain receipt — used for confirmed detection only. + // Our custom polling hook handles preconfirmed detection via FastRPC commitment/receipt polling. + // Wagmi correctly distinguishes real L1 receipts from FastRPC's simulated preconf receipts. + const { data: receipt, error: receiptError } = useWaitForTransactionReceipt({ + hash: effectiveHash as `0x${string}` | undefined, + }) + useWaitForTxConfirmation({ hash: effectiveHash ?? undefined, - receipt: undefined, // No wagmi receipt — avoids the premature confirmed race - receiptError: undefined, + receipt: (receipt as TransactionReceipt | undefined) ?? undefined, + receiptError, mode: "status", onConfirmed: () => { const t = useSwapToastStore.getState().toasts.find((x) => x.hash === hash) diff --git a/src/hooks/use-wait-for-tx-confirmation.ts b/src/hooks/use-wait-for-tx-confirmation.ts index d26f5db5..941d2239 100644 --- a/src/hooks/use-wait-for-tx-confirmation.ts +++ b/src/hooks/use-wait-for-tx-confirmation.ts @@ -257,8 +257,10 @@ export function useWaitForTxConfirmation({ await new Promise((r) => setTimeout(r, getPollInterval(pollCount++))) } - // ── Phase 2: Poll for confirmed/failed ── - // RPC receipt + DB status both useful here (L1 inclusion takes 12s+ so DB lag doesn't matter) + // ── Phase 2: Wait for confirmed/failed ── + // Wagmi handles confirmed detection via real L1 receipt (passed as prop). + // We only poll DB here for failure detection. Wagmi's onConfirmed effect + // fires when a real on-chain receipt arrives — no FastRPC simulated receipt issue. while (!abortController.signal.aborted && !hasConfirmedRef.current) { if (Date.now() - startTime > timeoutMs) { clearInterval(dbPollInterval) @@ -270,25 +272,18 @@ export function useWaitForTxConfirmation({ return } - const [mcStatus, rpcResult] = await Promise.all([ - fetchFastTxStatus(hash, abortController.signal), - fetchTransactionReceiptFromDb(hash, abortController.signal), - ]) + const mcStatus = await fetchFastTxStatus(hash, abortController.signal) if (abortController.signal.aborted || hasConfirmedRef.current) break - // Either source confirms - if ( - mcStatus === "confirmed" || - (rpcResult && rpcResult.receipt.status === "success" && rpcResult.receipt.blockNumber > 0n) - ) { + if (mcStatus === "confirmed") { hasConfirmedRef.current = true abortController.abort() clearInterval(dbPollInterval) setIsConfirmed(true) const result: TxConfirmationResult = mode === "receipt" - ? { source: "db", receipt: rpcResult?.receipt } + ? { source: "db" } : { source: "db", status: { success: true, hash } } onConfirmedRef.current(result) return @@ -298,10 +293,7 @@ export function useWaitForTxConfirmation({ hasConfirmedRef.current = true abortController.abort() clearInterval(dbPollInterval) - - const e = rpcResult - ? new RPCError("Transaction failed", rpcResult.receipt, rpcResult.rawResult) - : new Error("Transaction was dropped by the network.") + const e = new Error("Transaction was dropped by the network.") setError(e) onErrorRef.current?.(e) return From a72ef4b3831b4196ba96ca7c6338811eb78853e1 Mon Sep 17 00:00:00 2001 From: owen-eth Date: Wed, 25 Mar 2026 20:01:53 -0400 Subject: [PATCH 08/10] chore: add verbose poll logging to diagnose slow intent preconf detection Logs each poll cycle result (commit status + rpc receipt status) for the first 10 polls and every 10th after that. Will show exactly what the frontend sees during the delay window. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/hooks/use-wait-for-tx-confirmation.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/hooks/use-wait-for-tx-confirmation.ts b/src/hooks/use-wait-for-tx-confirmation.ts index 941d2239..1285b5ee 100644 --- a/src/hooks/use-wait-for-tx-confirmation.ts +++ b/src/hooks/use-wait-for-tx-confirmation.ts @@ -232,6 +232,11 @@ export function useWaitForTxConfirmation({ fetchTransactionReceiptFromDb(hash, abortController.signal), ]) + if (pollCount <= 10 || pollCount % 10 === 0) { + const elapsed = ((Date.now() - startTime) / 1000).toFixed(2) + console.log(`[TxConfirmation] Poll #${pollCount} | +${elapsed}s | commit=${commitStatus} | rpc=${rpcResult?.receipt?.status ?? "null"} | hash=${hash.slice(0, 14)}...`) + } + if (abortController.signal.aborted || hasConfirmedRef.current) break // RPC receipt with reverted status → immediate error From 78742325e9e17ec585ea237903ccd203f99018e9 Mon Sep 17 00:00:00 2001 From: owen-eth Date: Fri, 27 Mar 2026 13:05:37 -0400 Subject: [PATCH 09/10] chore: log RPC errors from commitment and receipt polling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously JSON-RPC errors (like "failed to get transaction commitments") were silently swallowed — the catch blocks returned null indistinguishable from "not found yet". Now logs warn-level messages so we can see exactly what FastRPC returns during the polling window. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/fast-rpc-status.ts | 8 +++++++- src/lib/transaction-receipt-utils.ts | 4 +++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/lib/fast-rpc-status.ts b/src/lib/fast-rpc-status.ts index 028754f6..ed281348 100644 --- a/src/lib/fast-rpc-status.ts +++ b/src/lib/fast-rpc-status.ts @@ -43,14 +43,20 @@ export async function fetchCommitmentStatus( const data = await response.json() + // Log RPC errors that would otherwise be silently swallowed + if (data?.error) { + console.warn(`[commitmentStatus] RPC error: ${data.error.message} | hash=${txHash.slice(0, 14)}...`) + } + // Only trust array results with actual commitment objects if (data?.result && Array.isArray(data.result) && data.result.length > 0) { return "preconfirmed" } return null - } catch { + } catch (err) { clearTimeout(timeoutId) + console.warn(`[commitmentStatus] fetch error: ${err instanceof Error ? err.message : err} | hash=${txHash.slice(0, 14)}...`) return null } } diff --git a/src/lib/transaction-receipt-utils.ts b/src/lib/transaction-receipt-utils.ts index 35df92ee..c0f00ad1 100644 --- a/src/lib/transaction-receipt-utils.ts +++ b/src/lib/transaction-receipt-utils.ts @@ -90,7 +90,9 @@ async function fetchTransactionReceipt( // No result means transaction not found in DB yet if (!data.result) { - // This is normal - DB hasn't indexed the transaction yet + if (data.error) { + console.warn(`[txReceipt] RPC error: ${data.error.message || JSON.stringify(data.error)} | hash=${txHash.slice(0, 14)}...`) + } return null } From 8ce57eb465d4791b2d5b73f73de6be51dd65c0e9 Mon Sep 17 00:00:00 2001 From: owen-eth Date: Mon, 30 Mar 2026 19:11:06 -0400 Subject: [PATCH 10/10] chore: remove debug logging, reduce barter validation debounce to 300ms Co-Authored-By: Claude Opus 4.6 (1M context) --- src/hooks/use-barter-validation.ts | 2 +- src/hooks/use-wait-for-tx-confirmation.ts | 6 ------ src/lib/fast-rpc-status.ts | 8 +------- src/lib/transaction-receipt-utils.ts | 4 ---- 4 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/hooks/use-barter-validation.ts b/src/hooks/use-barter-validation.ts index aec74abf..fac231b6 100644 --- a/src/hooks/use-barter-validation.ts +++ b/src/hooks/use-barter-validation.ts @@ -6,7 +6,7 @@ import { fetchBarterRoute } from "@/lib/barter-api" import { ZERO_ADDRESS, WETH_ADDRESS } from "@/lib/swap-constants" import type { Token } from "@/types/swap" -const DEBOUNCE_MS = 1500 +const DEBOUNCE_MS = 300 const MAX_SLIPPAGE_PCT = 2.0 interface UseBarterValidationParams { diff --git a/src/hooks/use-wait-for-tx-confirmation.ts b/src/hooks/use-wait-for-tx-confirmation.ts index 1285b5ee..9355895f 100644 --- a/src/hooks/use-wait-for-tx-confirmation.ts +++ b/src/hooks/use-wait-for-tx-confirmation.ts @@ -153,7 +153,6 @@ export function useWaitForTxConfirmation({ processingHashRef.current = hash preConfirmedFiredRef.current = false - console.log(`[TxConfirmation] Polling started | hash=${hash}`) const abortController = new AbortController() abortRef.current = abortController setIsConfirmed(false) @@ -232,11 +231,6 @@ export function useWaitForTxConfirmation({ fetchTransactionReceiptFromDb(hash, abortController.signal), ]) - if (pollCount <= 10 || pollCount % 10 === 0) { - const elapsed = ((Date.now() - startTime) / 1000).toFixed(2) - console.log(`[TxConfirmation] Poll #${pollCount} | +${elapsed}s | commit=${commitStatus} | rpc=${rpcResult?.receipt?.status ?? "null"} | hash=${hash.slice(0, 14)}...`) - } - if (abortController.signal.aborted || hasConfirmedRef.current) break // RPC receipt with reverted status → immediate error diff --git a/src/lib/fast-rpc-status.ts b/src/lib/fast-rpc-status.ts index ed281348..028754f6 100644 --- a/src/lib/fast-rpc-status.ts +++ b/src/lib/fast-rpc-status.ts @@ -43,20 +43,14 @@ export async function fetchCommitmentStatus( const data = await response.json() - // Log RPC errors that would otherwise be silently swallowed - if (data?.error) { - console.warn(`[commitmentStatus] RPC error: ${data.error.message} | hash=${txHash.slice(0, 14)}...`) - } - // Only trust array results with actual commitment objects if (data?.result && Array.isArray(data.result) && data.result.length > 0) { return "preconfirmed" } return null - } catch (err) { + } catch { clearTimeout(timeoutId) - console.warn(`[commitmentStatus] fetch error: ${err instanceof Error ? err.message : err} | hash=${txHash.slice(0, 14)}...`) return null } } diff --git a/src/lib/transaction-receipt-utils.ts b/src/lib/transaction-receipt-utils.ts index c0f00ad1..15b1a98d 100644 --- a/src/lib/transaction-receipt-utils.ts +++ b/src/lib/transaction-receipt-utils.ts @@ -90,15 +90,11 @@ async function fetchTransactionReceipt( // No result means transaction not found in DB yet if (!data.result) { - if (data.error) { - console.warn(`[txReceipt] RPC error: ${data.error.message || JSON.stringify(data.error)} | hash=${txHash.slice(0, 14)}...`) - } return null } // Has result but no status means pending/not confirmed if (!data.result.status) { - console.log(`[fetchTransactionReceipt] Receipt found but no status (pending) for ${txHash}`) return null }