-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuse-wait-for-tx-confirmation.ts
More file actions
330 lines (289 loc) · 11.4 KB
/
use-wait-for-tx-confirmation.ts
File metadata and controls
330 lines (289 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
"use client"
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, buildRevertMessage } from "@/lib/transaction-errors"
/**
* 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
/** 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
}
export type WaitForTxConfirmationMode = "receipt" | "status"
export interface TxConfirmationResult {
source: "db" | "wagmi"
receipt?: TransactionReceipt
status?: { success: boolean; hash: string }
}
export interface UseWaitForTxConfirmationParams {
hash: string | undefined
receipt: TransactionReceipt | undefined
/** Error from wagmi's useWaitForTransactionReceipt (e.g. tx dropped, replaced, RPC failure). */
receiptError?: Error | null
mode: WaitForTxConfirmationMode
onConfirmed: (result: TxConfirmationResult) => void
/** Called when RPC receipt or commitments report preconfirmed. */
onPreConfirmed?: (result: TxConfirmationResult) => void
onError?: (error: Error) => void
}
export interface UseWaitForTxConfirmationReturn {
isConfirming: boolean
error: Error | null
reset: () => void
}
/**
* Two-phase polling with decoupled failure detection:
*
* Phase 1 (pending → preconfirmed):
* 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):
* 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,
receipt,
receiptError,
mode,
onConfirmed,
onPreConfirmed,
onError,
}: UseWaitForTxConfirmationParams): UseWaitForTxConfirmationReturn {
const [error, setError] = useState<Error | null>(null)
const [isConfirmed, setIsConfirmed] = useState(false)
const abortRef = useRef<AbortController | null>(null)
const hasConfirmedRef = useRef(false)
const processingHashRef = useRef<string | null>(null)
const preConfirmedFiredRef = useRef(false)
// Refs to ensure callbacks stay fresh without re-triggering effects
const onConfirmedRef = useRef(onConfirmed)
const onPreConfirmedRef = useRef(onPreConfirmed)
const onErrorRef = useRef(onError)
useEffect(() => {
onConfirmedRef.current = onConfirmed
onPreConfirmedRef.current = onPreConfirmed
onErrorRef.current = onError
}, [onConfirmed, onPreConfirmed, onError])
const reset = useCallback(() => {
setIsConfirmed(false)
setError(null)
hasConfirmedRef.current = false
processingHashRef.current = null
preConfirmedFiredRef.current = false
if (abortRef.current) {
abortRef.current.abort()
abortRef.current = null
}
}, [])
// Effect: Watch for Wagmi (on-chain) receipt — parallel fallback throughout
useEffect(() => {
if (!hash || !receipt || hasConfirmedRef.current) return
if (abortRef.current) abortRef.current.abort()
try {
if (receipt.status === "reverted") {
hasConfirmedRef.current = true
const e = new RPCError(buildRevertMessage(receipt), receipt)
setError(e)
onErrorRef.current?.(e)
return
}
hasConfirmedRef.current = true
setIsConfirmed(true)
if (mode === "receipt") {
onConfirmedRef.current({ source: "wagmi", receipt })
} else {
onConfirmedRef.current({
source: "wagmi",
status: { success: receipt.status === "success", hash },
})
}
} catch (err) {
const e = err instanceof Error ? err : new Error(String(err))
setError(e)
onErrorRef.current?.(e)
}
}, [hash, receipt, mode])
// Effect: Watch for wagmi receipt error (tx dropped, replaced, RPC failure)
useEffect(() => {
if (!receiptError) return
hasConfirmedRef.current = true
if (abortRef.current) abortRef.current.abort()
const e = receiptError instanceof Error ? receiptError : new Error(String(receiptError))
setError(e)
onErrorRef.current?.(e)
}, [hash, receiptError])
// Effect: Two-phase polling with decoupled DB failure detection
useEffect(() => {
if (!hash || processingHashRef.current === hash) return
processingHashRef.current = hash
preConfirmedFiredRef.current = false
const abortController = new AbortController()
abortRef.current = abortController
setIsConfirmed(false)
setError(null)
/** Fire onPreConfirmed once, from whichever source wins the race. */
const firePreConfirmed = () => {
if (preConfirmedFiredRef.current) return
preConfirmedFiredRef.current = true
const result: TxConfirmationResult =
mode === "receipt" ? { source: "db" } : { source: "db", status: { success: true, hash } }
try {
onPreConfirmedRef.current?.(result)
} catch (err) {
const e = err instanceof Error ? err : new Error(String(err))
setError(e)
onErrorRef.current?.(e)
}
}
const poll = async () => {
try {
const timeoutMs = await getTxConfirmationTimeoutMs()
const startTime = Date.now()
let pollCount = 0
// 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."
)
setError(e)
onErrorRef.current?.(e)
return
}
// Poll only fast sources — both hit FastRPC directly
const [commitStatus, rpcResult] = await Promise.all([
fetchCommitmentStatus(hash, abortController.signal),
fetchTransactionReceiptFromDb(hash, abortController.signal),
])
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(
buildRevertMessage(rpcResult.receipt, rpcResult.rawResult),
rpcResult.receipt,
rpcResult.rawResult
)
setError(e)
onErrorRef.current?.(e)
return
}
// Either fast source signals preconfirmed → fire and move to phase 2
if (
commitStatus === "preconfirmed" ||
(rpcResult && rpcResult.receipt.status === "success")
) {
firePreConfirmed()
break // → Phase 2
}
await new Promise((r) => setTimeout(r, getPollInterval(pollCount++)))
}
// ── 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)
const e = new Error(
"Transaction confirmation timed out — your swap may have still succeeded. Check your wallet."
)
setError(e)
onErrorRef.current?.(e)
return
}
const mcStatus = await fetchFastTxStatus(hash, abortController.signal)
if (abortController.signal.aborted || hasConfirmedRef.current) break
if (mcStatus === "confirmed") {
hasConfirmedRef.current = true
abortController.abort()
clearInterval(dbPollInterval)
setIsConfirmed(true)
const result: TxConfirmationResult =
mode === "receipt"
? { source: "db" }
: { source: "db", status: { success: true, hash } }
onConfirmedRef.current(result)
return
}
if (mcStatus === "failed") {
hasConfirmedRef.current = true
abortController.abort()
clearInterval(dbPollInterval)
const e = new Error("Transaction was dropped by the network.")
setError(e)
onErrorRef.current?.(e)
return
}
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))
setError(e)
onErrorRef.current?.(e)
}
}
}
poll()
return () => {
abortController.abort()
if (abortRef.current === abortController) abortRef.current = null
processingHashRef.current = null
}
}, [hash, mode])
const isConfirming = !!hash && !isConfirmed && !error
return { isConfirming, error, reset }
}