explorerUrl && window.open(explorerUrl, "_blank")}
+ className={cn(
+ "relative h-[84px] p-4 flex items-center gap-4",
+ explorerUrl && settled && "cursor-pointer"
+ )}
+ >
+ {/* LEFT: Icon */}
+
+
+
+
-
+ {settled && (
+
+
+
)}
- >
-
-
+
+
+ {/* Green checkmark badge */}
+
+ {isConfirmed && (
+
+
+
+ )}
+
- {/* MIDDLE TEXT: Status label + amounts */}
+ {/* MIDDLE: Text */}
-
+ {isConfirmed ? (
+
+ Tokens Available
+
+ {toast.amountIn ?? "—"} {toast.tokenIn?.symbol} → {toast.amountOut ?? "—"}{" "}
+ {toast.tokenOut?.symbol}
+
+
+ ) : isPreConfirmed ? (
+
+ Preconfirmed
+
+ {toast.amountIn ?? "—"} {toast.tokenIn?.symbol} → {toast.amountOut ?? "—"}{" "}
+ {toast.tokenOut?.symbol}
+
+
+ ) : (
+
+ Swapping...
+
+ {toast.amountIn ?? "—"} {toast.tokenIn?.symbol} → {toast.amountOut ?? "—"}{" "}
+ {toast.tokenOut?.symbol}
+
+
)}
- >
- {isConfirmed
- ? "Tokens Available"
- : isPreConfirmed
- ? "Tokens Preconfirmed"
- : "Swapping..."}
-
-
- {/* Amount Subtext */}
-
- {toast.amountIn ?? "—"} {toast.tokenIn?.symbol} → {toast.amountOut ?? "—"}{" "}
- {toast.tokenOut?.symbol}
-
+
- {/* RIGHT ACTION/STATUS */}
-
- {isConfirmed || isPreConfirmed ? (
-
- ) : (
-
- {/* The Spinner */}
-
+ {/* RIGHT: Speed badge + explorer link */}
+
+ {/* Speed badge — prominent */}
+
+ {settled && elapsedSec && (
+
+ {elapsedSec}s
+
+ )}
+
- {/* Background ring for visual depth */}
+ {/* Explorer link */}
+ {settled && explorerUrl ? (
+
+
+
+ ) : isPending ? (
+
- )}
+ ) : null}
-
+
+ {/* ── Bottom accent line ── */}
+
+ {isConfirmed && (
+
+ )}
+ {isPreConfirmed && !isConfirmed && (
+
+ )}
+
+
+ {/* ── Share on X: flush strip below card body ── */}
+
+ {(isPreConfirmed || isConfirmed) && elapsedSec && (
+
+
+
+
+
+ )}
+
+
)
}
diff --git a/src/components/swap/SwapToastContainer.tsx b/src/components/swap/SwapToastContainer.tsx
index 85a7e996..0869eaf6 100644
--- a/src/components/swap/SwapToastContainer.tsx
+++ b/src/components/swap/SwapToastContainer.tsx
@@ -1,6 +1,7 @@
"use client"
import { useEffect } from "react"
+import { AnimatePresence } from "motion/react"
import { useSwapToastStore } from "@/stores/swapToastStore"
import { SwapToast } from "./SwapToast"
import { FEATURE_FLAGS, TEST_SWAP_TOAST_PLACEHOLDER } from "@/lib/feature-flags"
@@ -27,12 +28,12 @@ export function SwapToastContainer() {
}, [addToast])
return (
-
- {toasts.map((t, i) => (
-
-
-
- ))}
+
+
+ {toasts.map((t) => (
+
+ ))}
+
)
}
diff --git a/src/hooks/use-balance-flash.ts b/src/hooks/use-balance-flash.ts
new file mode 100644
index 00000000..c0428322
--- /dev/null
+++ b/src/hooks/use-balance-flash.ts
@@ -0,0 +1,31 @@
+"use client"
+
+import { useState, useEffect, useRef } from "react"
+
+/**
+ * Returns true briefly when a numeric balance changes upward.
+ * Used to flash/pulse the balance display after tokens arrive.
+ */
+export function useBalanceFlash(value: number, enabled: boolean = true): boolean {
+ const [isFlashing, setIsFlashing] = useState(false)
+ const prevValue = useRef(value)
+
+ useEffect(() => {
+ if (!enabled || prevValue.current === value) {
+ prevValue.current = value
+ return
+ }
+
+ // Only flash on increases (tokens arriving, not spending)
+ if (value > prevValue.current && prevValue.current > 0) {
+ setIsFlashing(true)
+ const timer = setTimeout(() => setIsFlashing(false), 1500)
+ prevValue.current = value
+ return () => clearTimeout(timer)
+ }
+
+ prevValue.current = value
+ }, [value, enabled])
+
+ return isFlashing
+}
diff --git a/src/hooks/use-swap-confirmation.ts b/src/hooks/use-swap-confirmation.ts
index 9aeb7b80..66f350fe 100644
--- a/src/hooks/use-swap-confirmation.ts
+++ b/src/hooks/use-swap-confirmation.ts
@@ -1,22 +1,14 @@
"use client"
-import { useState, useCallback, useEffect } from "react"
-import {
- useAccount,
- usePublicClient,
- useSendTransaction,
- useWaitForTransactionReceipt,
-} from "wagmi"
-import {
- useBroadcastGasPrice,
- ETH_PATH_GAS_LIMIT_MULTIPLIER,
-} from "@/hooks/use-broadcast-gas-price"
+import { useState, useCallback } from "react"
+import { useAccount, usePublicClient, useSendTransaction } from "wagmi"
+import { ETH_PATH_GAS_LIMIT_MULTIPLIER } from "@/hooks/use-broadcast-gas-price"
import { mainnet } from "wagmi/chains"
-import { parseUnits, formatUnits, type TransactionReceipt } from "viem"
+import { parseUnits, formatUnits } from "viem"
import { useSwapIntent } from "@/hooks/use-swap-intent"
import { usePermit2Nonce } from "@/hooks/use-permit2-nonce"
-import { useWaitForTxConfirmation } from "@/hooks/use-wait-for-tx-confirmation"
import { ZERO_ADDRESS, WETH_ADDRESS } from "@/lib/swap-constants"
+import { FASTSWAP_API_BASE } from "@/lib/network-config"
import { fetchEthPathTxAndEstimate } from "@/lib/eth-path-tx"
import type { Token } from "@/types/swap"
@@ -51,69 +43,22 @@ export function useSwapConfirmation({
onSuccess,
}: UseSwapConfirmationParams) {
const { isConnected, address } = useAccount()
- const { getFreshGasFees } = useBroadcastGasPrice()
const publicClient = usePublicClient({ chainId: mainnet.id })
const { createIntentSignature } = useSwapIntent()
- const {
- getFreshNonce,
- releaseNonce,
- syncFromChain,
- isLoading: isNonceLoading,
- } = usePermit2Nonce()
+ const { getFreshNonce, releaseNonce, isLoading: isNonceLoading } = usePermit2Nonce()
const { sendTransactionAsync } = useSendTransaction()
// --- Transaction State ---
+ // Note: Confirmation polling is handled by SwapToast (single source of truth).
const [isSigning, setIsSigning] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
- const [isConfirming, setIsConfirming] = useState(false)
- const [isSuccess, setIsSuccess] = useState(false)
const [hash, setHash] = useState
(null)
const [error, setError] = useState(null)
- // Wagmi receipt hook used as a data source for the race-condition confirmation hook
- const { data: receipt, error: receiptError } = useWaitForTransactionReceipt({
- hash: hash ? (hash as `0x${string}`) : undefined,
- })
-
- const onConfirmed = useCallback(() => {
- setIsSubmitting(false)
- setIsConfirming(false)
- setIsSuccess(true)
- syncFromChain() // Refresh nonce state
- onSuccess?.()
- }, [onSuccess, syncFromChain])
-
- const onConfirmationError = useCallback((err: Error) => {
- setIsSubmitting(false)
- setIsConfirming(false)
- setError(err instanceof Error ? err : new Error(String(err)))
- }, [])
-
- // Races DB polling against on-chain receipt
- useWaitForTxConfirmation({
- hash: hash ?? undefined,
- receipt: (receipt as TransactionReceipt | undefined) ?? undefined,
- receiptError,
- mode: "status",
- onConfirmed,
- onError: onConfirmationError,
- })
-
- // Sync confirmation status based on hash availability
- useEffect(() => {
- if (hash && !isSuccess && !error) {
- setIsConfirming(true)
- } else if (error) {
- setIsConfirming(false)
- }
- }, [hash, isSuccess, error])
-
const reset = useCallback(() => {
setIsSigning(false)
setIsSubmitting(false)
- setIsConfirming(false)
- setIsSuccess(false)
setHash(null)
setError(null)
}, [])
@@ -121,7 +66,6 @@ export function useSwapConfirmation({
const handleSwapError = useCallback((err: unknown) => {
setIsSigning(false)
setIsSubmitting(false)
- setIsConfirming(false)
setError(err instanceof Error ? err : new Error(String(err)))
}, [])
@@ -202,13 +146,6 @@ export function useSwapConfirmation({
const deadlineUnix = Math.floor(Date.now() / 1000) + deadline * 60
let result
- console.log("body", {
- outputToken: toToken.address,
- inputAmt: inputAmtWei,
- userAmtOut: userAmtOutWei,
- sender: address,
- deadline: String(deadlineUnix),
- })
try {
result = await fetchEthPathTxAndEstimate(
{
@@ -224,15 +161,12 @@ export function useSwapConfirmation({
} catch (err) {
const apiError = err instanceof Error ? err.message : "FastSwap API error"
let errorMessage = apiError
- console.log("apiError", apiError)
if (apiError.toLowerCase().includes("api error")) {
errorMessage += `\n\nContext:\nInput token: ${fromToken.symbol} (${fromToken.address})\nOutput token: ${toToken.symbol} (${toToken.address})\nSlippage: ${slippage}\nMinimum Output: ${userAmtOutWei}\nDeadline (minutes): ${deadline}`
}
throw new Error(errorMessage)
}
- await getFreshGasFees()
-
const bufferedGas = (result.gasEstimate * ETH_PATH_GAS_LIMIT_MULTIPLIER) / 100n
const txHash = await sendTransactionAsync({
@@ -295,14 +229,14 @@ export function useSwapConfirmation({
slippage: (parseFloat(slippage || "0.5") || 0.5).toFixed(1),
}
- const resp = await fetch("/api/fastswap", {
+ // Call FastRPC directly — CORS allows it, skip Vercel serverless proxy
+ const resp = await fetch(`${FASTSWAP_API_BASE}/fastswap`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
})
const result = await resp.json()
- console.log("[Permit Path] fastswap response:", { status: resp.status, result })
if (!resp.ok || !result?.txHash) {
releaseNonce(nonce)
const rawError = result?.error || "FastSwap API error"
@@ -319,8 +253,6 @@ export function useSwapConfirmation({
confirmSwap,
isSigning,
isSubmitting,
- isConfirming,
- isSuccess,
hash,
error,
reset,
diff --git a/src/hooks/use-wait-for-tx-confirmation.ts b/src/hooks/use-wait-for-tx-confirmation.ts
index 815aefe7..747c3b6d 100644
--- a/src/hooks/use-wait-for-tx-confirmation.ts
+++ b/src/hooks/use-wait-for-tx-confirmation.ts
@@ -4,10 +4,21 @@ import { useState, useEffect, useRef, useCallback } from "react"
import type { TransactionReceipt } from "viem"
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"
-const STATUS_CHECK_INTERVAL_MS = 500
+/**
+ * Adaptive polling: starts fast to catch sub-second preconfirmations,
+ * then backs off. First 5 polls at 100ms (~500ms window), then 500ms.
+ */
+const FAST_POLL_INTERVAL_MS = 100
+const NORMAL_POLL_INTERVAL_MS = 500
+const FAST_POLL_COUNT = 5
+
+function getPollInterval(pollCount: number): number {
+ return pollCount < FAST_POLL_COUNT ? FAST_POLL_INTERVAL_MS : NORMAL_POLL_INTERVAL_MS
+}
export type WaitForTxConfirmationMode = "receipt" | "status"
@@ -24,7 +35,7 @@ export interface UseWaitForTxConfirmationParams {
receiptError?: Error | null
mode: WaitForTxConfirmationMode
onConfirmed: (result: TxConfirmationResult) => void
- /** Called when RPC receipt or mctransactions reports pre-confirmed. */
+ /** Called when RPC receipt or mctransactions reports preconfirmed. */
onPreConfirmed?: (result: TxConfirmationResult) => void
onError?: (error: Error) => void
}
@@ -38,12 +49,12 @@ export interface UseWaitForTxConfirmationReturn {
/**
* Two-phase polling with Wagmi as parallel fallback:
*
- * Phase 1 (pending → pre-confirmed):
+ * Phase 1 (pending → preconfirmed):
* Poll BOTH eth_getTransactionReceipt (FastRPC) and mctransactions in parallel.
- * First source to show success/pre-confirmed fires onPreConfirmed.
+ * First source to show success/preconfirmed fires onPreConfirmed.
* mctransactions "failed" in this phase fires onError immediately.
*
- * Phase 2 (pre-confirmed → final):
+ * Phase 2 (preconfirmed → final):
* Stop RPC receipt polling. Poll only mctransactions for confirmed/failed.
* mctransactions "confirmed" → fire onConfirmed (final success).
* mctransactions "failed" → fire onError.
@@ -163,6 +174,7 @@ export function useWaitForTxConfirmation({
try {
const timeoutMs = await getTxConfirmationTimeoutMs()
const startTime = Date.now()
+ let pollCount = 0
// ── Phase 1: Poll both RPC receipt and mctransactions ──
while (!abortController.signal.aborted && !hasConfirmedRef.current) {
@@ -175,8 +187,12 @@ export function useWaitForTxConfirmation({
return
}
- // Poll both sources in parallel
- const [rpcResult, mcStatus] = await Promise.all([
+ // 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([
+ fetchCommitmentStatus(hash, abortController.signal),
fetchTransactionReceiptFromDb(hash, abortController.signal),
fetchFastTxStatus(hash, abortController.signal),
])
@@ -205,9 +221,10 @@ export function useWaitForTxConfirmation({
return
}
- // Either source signals pre-confirmed → fire and move to phase 2
+ // Any source signals preconfirmed → fire and move to phase 2
if (
- mcStatus === "pre-confirmed" ||
+ commitStatus === "preconfirmed" ||
+ mcStatus === "preconfirmed" ||
mcStatus === "confirmed" ||
(rpcResult && rpcResult.receipt.status === "success")
) {
@@ -227,7 +244,7 @@ export function useWaitForTxConfirmation({
break // → Phase 2
}
- await new Promise((r) => setTimeout(r, STATUS_CHECK_INTERVAL_MS))
+ await new Promise((r) => setTimeout(r, getPollInterval(pollCount++)))
}
// ── Phase 2: Poll only mctransactions for confirmed/failed ──
@@ -281,7 +298,7 @@ export function useWaitForTxConfirmation({
return
}
- await new Promise((r) => setTimeout(r, STATUS_CHECK_INTERVAL_MS))
+ await new Promise((r) => setTimeout(r, NORMAL_POLL_INTERVAL_MS))
}
} catch (err) {
if ((err as Error).name !== "AbortError" && !hasConfirmedRef.current) {
diff --git a/src/lib/eth-path-tx.ts b/src/lib/eth-path-tx.ts
index a7477f7a..1e2a7787 100644
--- a/src/lib/eth-path-tx.ts
+++ b/src/lib/eth-path-tx.ts
@@ -48,12 +48,20 @@ export async function fetchEthPathTxAndEstimate(
throw new Error(apiError)
}
- const estimated = await publicClient.estimateGas({
- account,
- to: data.to as `0x${string}`,
- data: data.data as `0x${string}`,
- value: BigInt(data.value || 0),
- })
+ let estimated: bigint
+ try {
+ estimated = await publicClient.estimateGas({
+ account,
+ to: data.to as `0x${string}`,
+ data: data.data as `0x${string}`,
+ value: BigInt(data.value || 0),
+ })
+ } catch (err) {
+ // Surface a clear message instead of the raw viem dump
+ throw new Error(
+ "This swap would fail on-chain — the price may have moved. Try increasing slippage or refreshing the quote."
+ )
+ }
return {
to: data.to,
diff --git a/src/lib/fast-rpc-status.ts b/src/lib/fast-rpc-status.ts
new file mode 100644
index 00000000..028754f6
--- /dev/null
+++ b/src/lib/fast-rpc-status.ts
@@ -0,0 +1,56 @@
+/**
+ * Polls FastRPC's mevcommit_getTransactionCommitments JSON-RPC method.
+ * This queries the FastRPC node directly — it knows about preconfirmation
+ * commitments instantly, unlike the mctransactions DB which lags ~15s.
+ *
+ * Returns "preconfirmed" if commitments exist for this tx, null otherwise.
+ */
+
+const RPC_URL = "https://fastrpc.mev-commit.xyz"
+const REQUEST_TIMEOUT_MS = 3000
+
+export async function fetchCommitmentStatus(
+ txHash: string,
+ abortSignal?: AbortSignal
+): Promise<"preconfirmed" | null> {
+ const controller = new AbortController()
+ const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS)
+
+ if (abortSignal) {
+ if (abortSignal.aborted) {
+ clearTimeout(timeoutId)
+ return null
+ }
+ abortSignal.addEventListener("abort", () => controller.abort(), { once: true })
+ }
+
+ try {
+ const response = await fetch(RPC_URL, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ jsonrpc: "2.0",
+ method: "mevcommit_getTransactionCommitments",
+ params: [txHash],
+ id: 1,
+ }),
+ signal: controller.signal,
+ })
+
+ clearTimeout(timeoutId)
+
+ if (!response.ok) return null
+
+ const data = await response.json()
+
+ // Only trust array results with actual commitment objects
+ if (data?.result && Array.isArray(data.result) && data.result.length > 0) {
+ return "preconfirmed"
+ }
+
+ return null
+ } catch {
+ clearTimeout(timeoutId)
+ return null
+ }
+}
diff --git a/src/lib/fast-tx-status.ts b/src/lib/fast-tx-status.ts
index 5e8004a2..e84b251c 100644
--- a/src/lib/fast-tx-status.ts
+++ b/src/lib/fast-tx-status.ts
@@ -1,10 +1,18 @@
-export type FastTxStatus = "pre-confirmed" | "confirmed" | "failed" | null
+export type FastTxStatus = "preconfirmed" | "confirmed" | "failed" | null
const REQUEST_TIMEOUT_MS = 5000
+/** Normalize DB status values (e.g. "pre-confirmed") to frontend values ("preconfirmed"). */
+function normalizeStatus(raw: string): FastTxStatus {
+ if (raw === "pre-confirmed" || raw === "preconfirmed") return "preconfirmed"
+ if (raw === "confirmed") return "confirmed"
+ if (raw === "failed") return "failed"
+ return null
+}
+
/**
* Fetches the mctransactions status for a swap tx hash.
- * Returns "pre-confirmed" | "confirmed" | "failed" | null (not found yet).
+ * Returns "preconfirmed" | "confirmed" | "failed" | null (not found yet).
*/
export async function fetchFastTxStatus(
txHash: string,
@@ -13,6 +21,15 @@ export async function fetchFastTxStatus(
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS)
+ // Link parent abort signal so in-flight requests cancel immediately
+ if (abortSignal) {
+ if (abortSignal.aborted) {
+ clearTimeout(timeoutId)
+ return null
+ }
+ abortSignal.addEventListener("abort", () => controller.abort(), { once: true })
+ }
+
try {
const response = await fetch(`/api/fast-tx-status/${txHash}`, {
signal: controller.signal,
@@ -25,7 +42,7 @@ export async function fetchFastTxStatus(
if (!response.ok) return null
const data = await response.json()
- return data.status as FastTxStatus
+ return data.status ? normalizeStatus(data.status) : null
} catch {
clearTimeout(timeoutId)
return null
diff --git a/src/lib/preconfirm-sound.ts b/src/lib/preconfirm-sound.ts
new file mode 100644
index 00000000..490ac439
--- /dev/null
+++ b/src/lib/preconfirm-sound.ts
@@ -0,0 +1,50 @@
+/**
+ * "Level Up" sound for preconfirmation.
+ * Three-note rising arpeggio (C5-E5-G5) via Web Audio API.
+ * No external files, instant playback, ~350ms.
+ */
+
+let audioCtx: AudioContext | null = null
+
+function getAudioContext(): AudioContext | null {
+ if (typeof window === "undefined") return null
+ if (!audioCtx) {
+ try {
+ audioCtx = new AudioContext()
+ } catch {
+ return null
+ }
+ }
+ return audioCtx
+}
+
+export function playPreconfirmSound() {
+ const ctx = getAudioContext()
+ if (!ctx) return
+
+ if (ctx.state === "suspended") {
+ ctx.resume().catch(() => {})
+ }
+
+ const now = ctx.currentTime
+
+ // C5 → E5 → G5 rising arpeggio
+ const notes: [number, number, number][] = [
+ [523, 0, 0.12], // C5
+ [659, 0.1, 0.12], // E5
+ [784, 0.2, 0.15], // G5
+ ]
+
+ for (const [freq, start, dur] of notes) {
+ const osc = ctx.createOscillator()
+ const gain = ctx.createGain()
+ osc.type = "sine"
+ osc.frequency.value = freq
+ gain.gain.setValueAtTime(0.08, now + start)
+ gain.gain.exponentialRampToValueAtTime(0.001, now + start + dur)
+ osc.connect(gain)
+ gain.connect(ctx.destination)
+ osc.start(now + start)
+ osc.stop(now + start + dur)
+ }
+}
diff --git a/src/lib/transaction-receipt-utils.ts b/src/lib/transaction-receipt-utils.ts
index 9a11286f..35df92ee 100644
--- a/src/lib/transaction-receipt-utils.ts
+++ b/src/lib/transaction-receipt-utils.ts
@@ -56,6 +56,15 @@ async function fetchTransactionReceipt(
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS)
+ // Link parent abort signal so in-flight requests cancel immediately
+ if (abortSignal) {
+ if (abortSignal.aborted) {
+ clearTimeout(timeoutId)
+ return null
+ }
+ abortSignal.addEventListener("abort", () => controller.abort(), { once: true })
+ }
+
try {
const response = await fetch(RPC_URL, {
method: "POST",
diff --git a/src/lib/tx-config.ts b/src/lib/tx-config.ts
index f29de912..a49905b7 100644
--- a/src/lib/tx-config.ts
+++ b/src/lib/tx-config.ts
@@ -2,9 +2,27 @@ const DEFAULT_TIMEOUT_MS = 60000
let cachedTimeoutMs: number | null = null
+/** Pre-warm the timeout cache on app load so first poll has zero delay. */
+const warmupPromise =
+ typeof window !== "undefined"
+ ? fetch("/api/config/tx-timeout")
+ .then((res) => (res.ok ? res.json() : null))
+ .then((data) => {
+ if (data && typeof data.timeoutMs === "number" && data.timeoutMs > 0) {
+ cachedTimeoutMs = data.timeoutMs
+ }
+ })
+ .catch(() => {})
+ : Promise.resolve()
+
export async function getTxConfirmationTimeoutMs(): Promise {
if (cachedTimeoutMs !== null) return cachedTimeoutMs
+ // Wait for warmup if it's still in-flight (usually already resolved by now)
+ await warmupPromise
+
+ if (cachedTimeoutMs !== null) return cachedTimeoutMs
+
try {
const res = await fetch("/api/config/tx-timeout")
if (!res.ok) throw new Error(`HTTP ${res.status}`)
diff --git a/src/lib/wagmi.ts b/src/lib/wagmi.ts
index 614ac9c0..88c6353b 100644
--- a/src/lib/wagmi.ts
+++ b/src/lib/wagmi.ts
@@ -91,12 +91,8 @@ const rpcFallbacks = [
}),
// SECONDARY: Public Nodes
- // http("https://eth.llamarpc.com", { timeout: 10000 }),
http("https://rpc.ankr.com/eth", { timeout: 10000 }),
http("https://1rpc.io/eth", { timeout: 10000 }),
-
- // LAST RESORT: Standard Public Node
- http(),
]
const bscRpcFallbacks = [
diff --git a/src/stores/swapToastStore.ts b/src/stores/swapToastStore.ts
index 68e60a66..4edb36b3 100644
--- a/src/stores/swapToastStore.ts
+++ b/src/stores/swapToastStore.ts
@@ -2,7 +2,7 @@ import { create } from "zustand"
import type { Token } from "@/types/swap"
import type { TransactionReceipt } from "viem"
-export type SwapToastStatus = "pending" | "pre-confirmed" | "confirmed" | "failed"
+export type SwapToastStatus = "pending" | "preconfirmed" | "confirmed" | "failed"
export type SwapToast = {
/** Stable id so key doesn't change when we update hash (Permit path); avoids remount jank. */
@@ -10,12 +10,16 @@ export type SwapToast = {
hash: string
status: SwapToastStatus
collapsed: boolean
+ /** Timestamp when toast was created (pending). Used for elapsed time display. */
+ createdAt: number
+ /** Timestamp when preconfirmed status was set. Freezes the elapsed timer. */
+ preconfirmedAt?: number
tokenIn?: Token
tokenOut?: Token
amountIn?: string
amountOut?: string
onConfirm?: () => void
- /** Called when DB has success receipt (pre-confirmation). Use to reset form state. */
+ /** Called when DB has success receipt (preconfirmation). Use to reset form state. */
onPreConfirm?: () => void
/** Error info stored on toast when tx fails (status "failed"). */
errorMessage?: string
@@ -30,7 +34,7 @@ export type SwapTxError = {
receipt?: TransactionReceipt
/** Raw RPC result from DB as returned (unmodified). Shown in Error Log when user clicks. */
rawDbRecord?: unknown
- /** True when the tx had already reached pre-confirmed before failing (e.g. reverted after DB 0x1). Hide Try Again. */
+ /** True when the tx had already reached preconfirmed before failing (e.g. reverted after DB 0x1). Hide Try Again. */
occurredAfterPreConfirm?: boolean
}
@@ -84,6 +88,7 @@ export const useSwapToastStore = create((set, get) => ({
hash,
status: "pending",
collapsed: false,
+ createdAt: Date.now(),
tokenIn,
tokenOut,
amountIn,
@@ -96,13 +101,23 @@ export const useSwapToastStore = create((set, get) => ({
setStatus: (hash, status) =>
set((s) => ({
- toasts: s.toasts.map((t) => (t.hash === hash ? { ...t, status } : t)),
+ toasts: s.toasts.map((t) =>
+ t.hash === hash
+ ? {
+ ...t,
+ status,
+ ...(status === "preconfirmed" && !t.preconfirmedAt
+ ? { preconfirmedAt: Date.now() }
+ : {}),
+ }
+ : t
+ ),
})),
setFailed: (hash, receipt, message, rawDbRecord) =>
set((s) => {
const toast = s.toasts.find((t) => t.hash === hash)
- const occurredAfterPreConfirm = toast?.status === "pre-confirmed"
+ const occurredAfterPreConfirm = toast?.status === "preconfirmed"
return {
toasts: s.toasts.map((t) =>
t.hash === hash
@@ -146,7 +161,7 @@ export const useSwapToastStore = create((set, get) => ({
collapse: (hash) =>
set((s) => ({
toasts: s.toasts.map((t) =>
- t.hash === hash && (t.status === "pending" || t.status === "pre-confirmed")
+ t.hash === hash && (t.status === "pending" || t.status === "preconfirmed")
? { ...t, collapsed: true }
: t
),
@@ -162,8 +177,9 @@ export const useSwapToastStore = create((set, get) => ({
toasts: s.toasts.filter((t) => t.hash !== hash),
})),
- updateToastHash: (placeholderHash, realHash) =>
+ updateToastHash: (placeholderHash, realHash) => {
set((s) => ({
toasts: s.toasts.map((t) => (t.hash === placeholderHash ? { ...t, hash: realHash } : t)),
- })),
+ }))
+ },
}))