@sip-protocol/react 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -14
- package/dist/index.d.mts +1224 -6
- package/dist/index.d.ts +1224 -6
- package/dist/index.js +5783 -10
- package/dist/index.mjs +5777 -9
- package/package.json +9 -8
- package/src/components/ethereum/index.ts +55 -0
- package/src/components/ethereum/privacy-toggle.tsx +822 -0
- package/src/components/ethereum/stealth-address-display.tsx +1050 -0
- package/src/components/ethereum/transaction-history.tsx +1187 -0
- package/src/components/ethereum/transaction-tracker.tsx +302 -0
- package/src/components/ethereum/viewing-key-manager.tsx +228 -0
- package/src/components/index.ts +107 -0
- package/src/components/privacy-toggle.tsx +548 -0
- package/src/components/stealth-address-display.tsx +770 -0
- package/src/components/transaction-history.tsx +651 -0
- package/src/components/transaction-tracker.tsx +1079 -0
- package/src/components/viewing-key-manager.tsx +1576 -0
- package/src/hooks/index.ts +61 -0
- package/src/hooks/use-privacy-advisor.ts +371 -0
- package/src/hooks/use-private-swap.ts +5 -5
- package/src/hooks/use-proof-composition.ts +654 -0
- package/src/hooks/use-scan-payments.ts +504 -0
- package/src/hooks/use-stealth-address.ts +23 -7
- package/src/hooks/use-stealth-transfer.ts +284 -0
- package/src/hooks/use-transaction-history.ts +435 -0
- package/src/index.ts +75 -0
|
@@ -0,0 +1,1079 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Transaction status types
|
|
5
|
+
*/
|
|
6
|
+
export type TransactionStatus =
|
|
7
|
+
| 'pending'
|
|
8
|
+
| 'processing'
|
|
9
|
+
| 'confirmed'
|
|
10
|
+
| 'finalized'
|
|
11
|
+
| 'failed'
|
|
12
|
+
| 'cancelled'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Privacy-specific verification status
|
|
16
|
+
*/
|
|
17
|
+
export type PrivacyVerificationStatus = 'pending' | 'verified' | 'failed' | 'not_applicable'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Transaction action types
|
|
21
|
+
*/
|
|
22
|
+
export type TransactionActionType =
|
|
23
|
+
| 'transfer'
|
|
24
|
+
| 'stealth_transfer'
|
|
25
|
+
| 'function_call'
|
|
26
|
+
| 'create_account'
|
|
27
|
+
| 'stake'
|
|
28
|
+
| 'unstake'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Transaction action
|
|
32
|
+
*/
|
|
33
|
+
export interface TransactionAction {
|
|
34
|
+
type: TransactionActionType
|
|
35
|
+
receiver: string
|
|
36
|
+
amount?: string
|
|
37
|
+
methodName?: string
|
|
38
|
+
args?: Record<string, unknown>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Privacy verification details
|
|
43
|
+
*/
|
|
44
|
+
export interface PrivacyVerification {
|
|
45
|
+
stealthAddressResolved: PrivacyVerificationStatus
|
|
46
|
+
commitmentVerified: PrivacyVerificationStatus
|
|
47
|
+
viewingKeyGenerated: PrivacyVerificationStatus
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Transaction details
|
|
52
|
+
*/
|
|
53
|
+
export interface PrivacyTransaction {
|
|
54
|
+
/** Transaction hash */
|
|
55
|
+
hash: string
|
|
56
|
+
/** Transaction status */
|
|
57
|
+
status: TransactionStatus
|
|
58
|
+
/** Block number (null if pending) */
|
|
59
|
+
blockHeight?: number
|
|
60
|
+
/** Number of confirmations */
|
|
61
|
+
confirmations: number
|
|
62
|
+
/** Required confirmations for finality */
|
|
63
|
+
requiredConfirmations: number
|
|
64
|
+
/** Timestamp of transaction (ms) */
|
|
65
|
+
timestamp: number
|
|
66
|
+
/** Sender account */
|
|
67
|
+
sender: string
|
|
68
|
+
/** Receiver account or stealth address */
|
|
69
|
+
receiver: string
|
|
70
|
+
/** Whether receiver is a stealth address */
|
|
71
|
+
isStealthReceiver: boolean
|
|
72
|
+
/** Amount transferred (in native token units) */
|
|
73
|
+
amount?: string
|
|
74
|
+
/** Gas used */
|
|
75
|
+
gasUsed?: string
|
|
76
|
+
/** Transaction fee */
|
|
77
|
+
fee?: string
|
|
78
|
+
/** Transaction actions */
|
|
79
|
+
actions: TransactionAction[]
|
|
80
|
+
/** Privacy verification status */
|
|
81
|
+
privacyVerification?: PrivacyVerification
|
|
82
|
+
/** Error message if failed */
|
|
83
|
+
errorMessage?: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* TransactionTracker component props
|
|
88
|
+
*/
|
|
89
|
+
export interface TransactionTrackerProps {
|
|
90
|
+
/** Transaction data */
|
|
91
|
+
transaction: PrivacyTransaction
|
|
92
|
+
/** Callback to refresh transaction status */
|
|
93
|
+
onRefresh?: () => void
|
|
94
|
+
/** Callback to retry failed transaction */
|
|
95
|
+
onRetry?: () => void
|
|
96
|
+
/** Callback to cancel pending transaction */
|
|
97
|
+
onCancel?: () => void
|
|
98
|
+
/** Whether to show expanded details by default */
|
|
99
|
+
defaultExpanded?: boolean
|
|
100
|
+
/** Polling interval in ms (0 to disable) */
|
|
101
|
+
pollingInterval?: number
|
|
102
|
+
/** Network name for display */
|
|
103
|
+
networkName?: string
|
|
104
|
+
/** Explorer URL template (use {hash} placeholder) */
|
|
105
|
+
explorerUrlTemplate?: string
|
|
106
|
+
/** Custom class name */
|
|
107
|
+
className?: string
|
|
108
|
+
/** Size variant */
|
|
109
|
+
size?: 'sm' | 'md' | 'lg'
|
|
110
|
+
/** Whether to show privacy verification status */
|
|
111
|
+
showPrivacyStatus?: boolean
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* CSS styles for the component
|
|
116
|
+
*/
|
|
117
|
+
const styles = `
|
|
118
|
+
.sip-tx-tracker {
|
|
119
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
120
|
+
border-radius: 12px;
|
|
121
|
+
overflow: hidden;
|
|
122
|
+
background: #ffffff;
|
|
123
|
+
border: 1px solid #e5e7eb;
|
|
124
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.sip-tx-tracker[data-size="sm"] {
|
|
128
|
+
border-radius: 8px;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.sip-tx-tracker[data-size="lg"] {
|
|
132
|
+
border-radius: 16px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Header */
|
|
136
|
+
.sip-tx-header {
|
|
137
|
+
display: flex;
|
|
138
|
+
align-items: center;
|
|
139
|
+
justify-content: space-between;
|
|
140
|
+
padding: 16px;
|
|
141
|
+
background: #f9fafb;
|
|
142
|
+
border-bottom: 1px solid #e5e7eb;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.sip-tx-tracker[data-size="sm"] .sip-tx-header {
|
|
146
|
+
padding: 12px;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.sip-tx-tracker[data-size="lg"] .sip-tx-header {
|
|
150
|
+
padding: 20px;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.sip-tx-header-left {
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
gap: 12px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.sip-tx-status-icon {
|
|
160
|
+
display: flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
justify-content: center;
|
|
163
|
+
width: 40px;
|
|
164
|
+
height: 40px;
|
|
165
|
+
border-radius: 50%;
|
|
166
|
+
flex-shrink: 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.sip-tx-tracker[data-size="sm"] .sip-tx-status-icon {
|
|
170
|
+
width: 32px;
|
|
171
|
+
height: 32px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.sip-tx-tracker[data-size="lg"] .sip-tx-status-icon {
|
|
175
|
+
width: 48px;
|
|
176
|
+
height: 48px;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.sip-tx-status-icon svg {
|
|
180
|
+
width: 20px;
|
|
181
|
+
height: 20px;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.sip-tx-tracker[data-size="sm"] .sip-tx-status-icon svg {
|
|
185
|
+
width: 16px;
|
|
186
|
+
height: 16px;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.sip-tx-tracker[data-size="lg"] .sip-tx-status-icon svg {
|
|
190
|
+
width: 24px;
|
|
191
|
+
height: 24px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.sip-tx-status-icon[data-status="pending"] {
|
|
195
|
+
background: #fef3c7;
|
|
196
|
+
color: #d97706;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.sip-tx-status-icon[data-status="processing"] {
|
|
200
|
+
background: #dbeafe;
|
|
201
|
+
color: #2563eb;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.sip-tx-status-icon[data-status="confirmed"],
|
|
205
|
+
.sip-tx-status-icon[data-status="finalized"] {
|
|
206
|
+
background: #d1fae5;
|
|
207
|
+
color: #059669;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.sip-tx-status-icon[data-status="failed"],
|
|
211
|
+
.sip-tx-status-icon[data-status="cancelled"] {
|
|
212
|
+
background: #fee2e2;
|
|
213
|
+
color: #dc2626;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.sip-tx-status-info {
|
|
217
|
+
display: flex;
|
|
218
|
+
flex-direction: column;
|
|
219
|
+
gap: 2px;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.sip-tx-status-title {
|
|
223
|
+
font-size: 14px;
|
|
224
|
+
font-weight: 600;
|
|
225
|
+
color: #111827;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.sip-tx-tracker[data-size="sm"] .sip-tx-status-title {
|
|
229
|
+
font-size: 13px;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.sip-tx-tracker[data-size="lg"] .sip-tx-status-title {
|
|
233
|
+
font-size: 16px;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.sip-tx-status-subtitle {
|
|
237
|
+
font-size: 12px;
|
|
238
|
+
color: #6b7280;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.sip-tx-header-actions {
|
|
242
|
+
display: flex;
|
|
243
|
+
align-items: center;
|
|
244
|
+
gap: 8px;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.sip-tx-action-btn {
|
|
248
|
+
display: flex;
|
|
249
|
+
align-items: center;
|
|
250
|
+
justify-content: center;
|
|
251
|
+
padding: 8px 12px;
|
|
252
|
+
border: none;
|
|
253
|
+
border-radius: 6px;
|
|
254
|
+
font-size: 13px;
|
|
255
|
+
font-weight: 500;
|
|
256
|
+
cursor: pointer;
|
|
257
|
+
transition: all 0.2s;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.sip-tx-action-btn-primary {
|
|
261
|
+
background: #3b82f6;
|
|
262
|
+
color: white;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.sip-tx-action-btn-primary:hover {
|
|
266
|
+
background: #2563eb;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.sip-tx-action-btn-secondary {
|
|
270
|
+
background: #f3f4f6;
|
|
271
|
+
color: #374151;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.sip-tx-action-btn-secondary:hover {
|
|
275
|
+
background: #e5e7eb;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.sip-tx-action-btn-danger {
|
|
279
|
+
background: #fee2e2;
|
|
280
|
+
color: #dc2626;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.sip-tx-action-btn-danger:hover {
|
|
284
|
+
background: #fecaca;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/* Progress bar */
|
|
288
|
+
.sip-tx-progress {
|
|
289
|
+
padding: 0 16px 16px;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.sip-tx-tracker[data-size="sm"] .sip-tx-progress {
|
|
293
|
+
padding: 0 12px 12px;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.sip-tx-tracker[data-size="lg"] .sip-tx-progress {
|
|
297
|
+
padding: 0 20px 20px;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.sip-tx-progress-bar {
|
|
301
|
+
height: 6px;
|
|
302
|
+
background: #e5e7eb;
|
|
303
|
+
border-radius: 3px;
|
|
304
|
+
overflow: hidden;
|
|
305
|
+
margin-bottom: 8px;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.sip-tx-progress-fill {
|
|
309
|
+
height: 100%;
|
|
310
|
+
border-radius: 3px;
|
|
311
|
+
transition: width 0.3s ease;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.sip-tx-progress-fill[data-status="pending"] {
|
|
315
|
+
background: #fbbf24;
|
|
316
|
+
animation: sip-tx-pulse 1.5s infinite;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.sip-tx-progress-fill[data-status="processing"] {
|
|
320
|
+
background: #3b82f6;
|
|
321
|
+
animation: sip-tx-pulse 1.5s infinite;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.sip-tx-progress-fill[data-status="confirmed"] {
|
|
325
|
+
background: #10b981;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.sip-tx-progress-fill[data-status="finalized"] {
|
|
329
|
+
background: #059669;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.sip-tx-progress-fill[data-status="failed"],
|
|
333
|
+
.sip-tx-progress-fill[data-status="cancelled"] {
|
|
334
|
+
background: #ef4444;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
@keyframes sip-tx-pulse {
|
|
338
|
+
0%, 100% { opacity: 1; }
|
|
339
|
+
50% { opacity: 0.7; }
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.sip-tx-progress-info {
|
|
343
|
+
display: flex;
|
|
344
|
+
justify-content: space-between;
|
|
345
|
+
font-size: 11px;
|
|
346
|
+
color: #6b7280;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/* Privacy status */
|
|
350
|
+
.sip-tx-privacy {
|
|
351
|
+
padding: 12px 16px;
|
|
352
|
+
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 100%);
|
|
353
|
+
border-bottom: 1px solid #4338ca;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.sip-tx-tracker[data-size="sm"] .sip-tx-privacy {
|
|
357
|
+
padding: 10px 12px;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.sip-tx-tracker[data-size="lg"] .sip-tx-privacy {
|
|
361
|
+
padding: 16px 20px;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.sip-tx-privacy-title {
|
|
365
|
+
display: flex;
|
|
366
|
+
align-items: center;
|
|
367
|
+
gap: 8px;
|
|
368
|
+
font-size: 12px;
|
|
369
|
+
font-weight: 600;
|
|
370
|
+
color: #c7d2fe;
|
|
371
|
+
text-transform: uppercase;
|
|
372
|
+
letter-spacing: 0.05em;
|
|
373
|
+
margin-bottom: 10px;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.sip-tx-privacy-title svg {
|
|
377
|
+
width: 14px;
|
|
378
|
+
height: 14px;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.sip-tx-privacy-items {
|
|
382
|
+
display: flex;
|
|
383
|
+
flex-wrap: wrap;
|
|
384
|
+
gap: 8px;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.sip-tx-privacy-item {
|
|
388
|
+
display: flex;
|
|
389
|
+
align-items: center;
|
|
390
|
+
gap: 6px;
|
|
391
|
+
padding: 6px 10px;
|
|
392
|
+
background: rgba(255, 255, 255, 0.1);
|
|
393
|
+
border-radius: 6px;
|
|
394
|
+
font-size: 12px;
|
|
395
|
+
color: #e0e7ff;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.sip-tx-privacy-item svg {
|
|
399
|
+
width: 14px;
|
|
400
|
+
height: 14px;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.sip-tx-privacy-item[data-status="verified"] svg {
|
|
404
|
+
color: #34d399;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.sip-tx-privacy-item[data-status="pending"] svg {
|
|
408
|
+
color: #fbbf24;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.sip-tx-privacy-item[data-status="failed"] svg {
|
|
412
|
+
color: #f87171;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/* Details section */
|
|
416
|
+
.sip-tx-details-toggle {
|
|
417
|
+
display: flex;
|
|
418
|
+
align-items: center;
|
|
419
|
+
justify-content: center;
|
|
420
|
+
gap: 8px;
|
|
421
|
+
width: 100%;
|
|
422
|
+
padding: 12px;
|
|
423
|
+
border: none;
|
|
424
|
+
background: transparent;
|
|
425
|
+
color: #6b7280;
|
|
426
|
+
font-size: 13px;
|
|
427
|
+
font-weight: 500;
|
|
428
|
+
cursor: pointer;
|
|
429
|
+
transition: all 0.2s;
|
|
430
|
+
border-top: 1px solid #e5e7eb;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.sip-tx-details-toggle:hover {
|
|
434
|
+
background: #f9fafb;
|
|
435
|
+
color: #374151;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.sip-tx-details-toggle svg {
|
|
439
|
+
width: 16px;
|
|
440
|
+
height: 16px;
|
|
441
|
+
transition: transform 0.2s;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.sip-tx-details-toggle[aria-expanded="true"] svg {
|
|
445
|
+
transform: rotate(180deg);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
.sip-tx-details {
|
|
449
|
+
padding: 16px;
|
|
450
|
+
background: #f9fafb;
|
|
451
|
+
border-top: 1px solid #e5e7eb;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.sip-tx-tracker[data-size="sm"] .sip-tx-details {
|
|
455
|
+
padding: 12px;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.sip-tx-tracker[data-size="lg"] .sip-tx-details {
|
|
459
|
+
padding: 20px;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.sip-tx-detail-row {
|
|
463
|
+
display: flex;
|
|
464
|
+
justify-content: space-between;
|
|
465
|
+
align-items: flex-start;
|
|
466
|
+
padding: 8px 0;
|
|
467
|
+
border-bottom: 1px solid #e5e7eb;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.sip-tx-detail-row:last-child {
|
|
471
|
+
border-bottom: none;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.sip-tx-detail-label {
|
|
475
|
+
font-size: 12px;
|
|
476
|
+
color: #6b7280;
|
|
477
|
+
font-weight: 500;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.sip-tx-detail-value {
|
|
481
|
+
font-size: 12px;
|
|
482
|
+
color: #111827;
|
|
483
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
484
|
+
text-align: right;
|
|
485
|
+
max-width: 60%;
|
|
486
|
+
word-break: break-all;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.sip-tx-detail-value a {
|
|
490
|
+
color: #3b82f6;
|
|
491
|
+
text-decoration: none;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.sip-tx-detail-value a:hover {
|
|
495
|
+
text-decoration: underline;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/* Error message */
|
|
499
|
+
.sip-tx-error {
|
|
500
|
+
padding: 12px 16px;
|
|
501
|
+
background: #fee2e2;
|
|
502
|
+
border-top: 1px solid #fecaca;
|
|
503
|
+
font-size: 13px;
|
|
504
|
+
color: #dc2626;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/* ETA */
|
|
508
|
+
.sip-tx-eta {
|
|
509
|
+
display: flex;
|
|
510
|
+
align-items: center;
|
|
511
|
+
gap: 6px;
|
|
512
|
+
padding: 12px 16px;
|
|
513
|
+
background: #eff6ff;
|
|
514
|
+
border-top: 1px solid #bfdbfe;
|
|
515
|
+
font-size: 12px;
|
|
516
|
+
color: #1d4ed8;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.sip-tx-eta svg {
|
|
520
|
+
width: 14px;
|
|
521
|
+
height: 14px;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/* Dark mode */
|
|
525
|
+
@media (prefers-color-scheme: dark) {
|
|
526
|
+
.sip-tx-tracker {
|
|
527
|
+
background: #1f2937;
|
|
528
|
+
border-color: #374151;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.sip-tx-header {
|
|
532
|
+
background: #111827;
|
|
533
|
+
border-color: #374151;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.sip-tx-status-title {
|
|
537
|
+
color: #f9fafb;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.sip-tx-status-subtitle {
|
|
541
|
+
color: #9ca3af;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.sip-tx-progress-bar {
|
|
545
|
+
background: #374151;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.sip-tx-progress-info {
|
|
549
|
+
color: #9ca3af;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.sip-tx-details-toggle {
|
|
553
|
+
border-color: #374151;
|
|
554
|
+
color: #9ca3af;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.sip-tx-details-toggle:hover {
|
|
558
|
+
background: #111827;
|
|
559
|
+
color: #e5e7eb;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
.sip-tx-details {
|
|
563
|
+
background: #111827;
|
|
564
|
+
border-color: #374151;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.sip-tx-detail-row {
|
|
568
|
+
border-color: #374151;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.sip-tx-detail-label {
|
|
572
|
+
color: #9ca3af;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.sip-tx-detail-value {
|
|
576
|
+
color: #f9fafb;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.sip-tx-action-btn-secondary {
|
|
580
|
+
background: #374151;
|
|
581
|
+
color: #e5e7eb;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.sip-tx-action-btn-secondary:hover {
|
|
585
|
+
background: #4b5563;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
`
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Status icons
|
|
592
|
+
*/
|
|
593
|
+
const StatusIcons: Record<TransactionStatus, React.ReactNode> = {
|
|
594
|
+
pending: (
|
|
595
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
596
|
+
<circle cx="12" cy="12" r="10" />
|
|
597
|
+
<path d="M12 6v6l4 2" />
|
|
598
|
+
</svg>
|
|
599
|
+
),
|
|
600
|
+
processing: (
|
|
601
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
602
|
+
<path d="M21 12a9 9 0 11-6.219-8.56" />
|
|
603
|
+
</svg>
|
|
604
|
+
),
|
|
605
|
+
confirmed: (
|
|
606
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
607
|
+
<polyline points="20 6 9 17 4 12" />
|
|
608
|
+
</svg>
|
|
609
|
+
),
|
|
610
|
+
finalized: (
|
|
611
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
612
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
613
|
+
<polyline points="9 12 11 14 15 10" />
|
|
614
|
+
</svg>
|
|
615
|
+
),
|
|
616
|
+
failed: (
|
|
617
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
618
|
+
<circle cx="12" cy="12" r="10" />
|
|
619
|
+
<line x1="15" y1="9" x2="9" y2="15" />
|
|
620
|
+
<line x1="9" y1="9" x2="15" y2="15" />
|
|
621
|
+
</svg>
|
|
622
|
+
),
|
|
623
|
+
cancelled: (
|
|
624
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
625
|
+
<circle cx="12" cy="12" r="10" />
|
|
626
|
+
<line x1="8" y1="12" x2="16" y2="12" />
|
|
627
|
+
</svg>
|
|
628
|
+
),
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Privacy status icons
|
|
633
|
+
*/
|
|
634
|
+
const PrivacyStatusIcons: Record<PrivacyVerificationStatus, React.ReactNode> = {
|
|
635
|
+
verified: (
|
|
636
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
637
|
+
<polyline points="20 6 9 17 4 12" />
|
|
638
|
+
</svg>
|
|
639
|
+
),
|
|
640
|
+
pending: (
|
|
641
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
642
|
+
<circle cx="12" cy="12" r="10" />
|
|
643
|
+
<path d="M12 6v6l4 2" />
|
|
644
|
+
</svg>
|
|
645
|
+
),
|
|
646
|
+
failed: (
|
|
647
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
648
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
649
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
650
|
+
</svg>
|
|
651
|
+
),
|
|
652
|
+
not_applicable: (
|
|
653
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
654
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
655
|
+
</svg>
|
|
656
|
+
),
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Chevron icon
|
|
661
|
+
*/
|
|
662
|
+
const ChevronIcon = () => (
|
|
663
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
664
|
+
<polyline points="6 9 12 15 18 9" />
|
|
665
|
+
</svg>
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Shield icon
|
|
670
|
+
*/
|
|
671
|
+
const ShieldIcon = () => (
|
|
672
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
673
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
674
|
+
</svg>
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Clock icon
|
|
679
|
+
*/
|
|
680
|
+
const ClockIcon = () => (
|
|
681
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
682
|
+
<circle cx="12" cy="12" r="10" />
|
|
683
|
+
<polyline points="12 6 12 12 16 14" />
|
|
684
|
+
</svg>
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Status titles
|
|
689
|
+
*/
|
|
690
|
+
const STATUS_TITLES: Record<TransactionStatus, string> = {
|
|
691
|
+
pending: 'Transaction Pending',
|
|
692
|
+
processing: 'Processing Transaction',
|
|
693
|
+
confirmed: 'Transaction Confirmed',
|
|
694
|
+
finalized: 'Transaction Finalized',
|
|
695
|
+
failed: 'Transaction Failed',
|
|
696
|
+
cancelled: 'Transaction Cancelled',
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* TransactionTracker - Component for tracking NEAR privacy transactions
|
|
701
|
+
*
|
|
702
|
+
* @example Basic usage
|
|
703
|
+
* ```tsx
|
|
704
|
+
* import { TransactionTracker } from '@sip-protocol/react'
|
|
705
|
+
*
|
|
706
|
+
* function TransactionView({ txHash }) {
|
|
707
|
+
* const [tx, setTx] = useState(null)
|
|
708
|
+
*
|
|
709
|
+
* return (
|
|
710
|
+
* <TransactionTracker
|
|
711
|
+
* transaction={tx}
|
|
712
|
+
* onRefresh={() => fetchTransaction(txHash)}
|
|
713
|
+
* />
|
|
714
|
+
* )
|
|
715
|
+
* }
|
|
716
|
+
* ```
|
|
717
|
+
*/
|
|
718
|
+
export function TransactionTracker({
|
|
719
|
+
transaction,
|
|
720
|
+
onRefresh,
|
|
721
|
+
onRetry,
|
|
722
|
+
onCancel,
|
|
723
|
+
defaultExpanded = false,
|
|
724
|
+
pollingInterval = 0,
|
|
725
|
+
networkName = 'NEAR',
|
|
726
|
+
explorerUrlTemplate = 'https://nearblocks.io/txns/{hash}',
|
|
727
|
+
className = '',
|
|
728
|
+
size = 'md',
|
|
729
|
+
showPrivacyStatus = true,
|
|
730
|
+
}: TransactionTrackerProps) {
|
|
731
|
+
const [isExpanded, setIsExpanded] = useState(defaultExpanded)
|
|
732
|
+
|
|
733
|
+
// Calculate progress percentage
|
|
734
|
+
const progressPercentage = useMemo(() => {
|
|
735
|
+
const { status, confirmations, requiredConfirmations } = transaction
|
|
736
|
+
|
|
737
|
+
if (status === 'failed' || status === 'cancelled') return 100
|
|
738
|
+
if (status === 'finalized') return 100
|
|
739
|
+
if (status === 'pending') return 10
|
|
740
|
+
|
|
741
|
+
const progress = Math.min(confirmations / requiredConfirmations, 1) * 100
|
|
742
|
+
return Math.max(progress, 20)
|
|
743
|
+
}, [transaction])
|
|
744
|
+
|
|
745
|
+
// Calculate ETA
|
|
746
|
+
const estimatedTimeLeft = useMemo(() => {
|
|
747
|
+
const { status, confirmations, requiredConfirmations } = transaction
|
|
748
|
+
|
|
749
|
+
if (status === 'finalized' || status === 'failed' || status === 'cancelled') {
|
|
750
|
+
return null
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const remaining = requiredConfirmations - confirmations
|
|
754
|
+
// NEAR has ~1 second block time
|
|
755
|
+
const seconds = Math.max(remaining, 1)
|
|
756
|
+
|
|
757
|
+
if (seconds < 60) {
|
|
758
|
+
return `~${seconds}s to finality`
|
|
759
|
+
}
|
|
760
|
+
return `~${Math.ceil(seconds / 60)}m to finality`
|
|
761
|
+
}, [transaction])
|
|
762
|
+
|
|
763
|
+
// Format timestamp
|
|
764
|
+
const formattedTime = useMemo(() => {
|
|
765
|
+
const date = new Date(transaction.timestamp)
|
|
766
|
+
return date.toLocaleString()
|
|
767
|
+
}, [transaction.timestamp])
|
|
768
|
+
|
|
769
|
+
// Explorer URL
|
|
770
|
+
const explorerUrl = useMemo(() => {
|
|
771
|
+
return explorerUrlTemplate.replace('{hash}', transaction.hash)
|
|
772
|
+
}, [explorerUrlTemplate, transaction.hash])
|
|
773
|
+
|
|
774
|
+
// Truncate hash for display
|
|
775
|
+
const truncatedHash = useMemo(() => {
|
|
776
|
+
const hash = transaction.hash
|
|
777
|
+
return `${hash.slice(0, 8)}...${hash.slice(-8)}`
|
|
778
|
+
}, [transaction.hash])
|
|
779
|
+
|
|
780
|
+
// Polling effect
|
|
781
|
+
useEffect(() => {
|
|
782
|
+
if (
|
|
783
|
+
pollingInterval <= 0 ||
|
|
784
|
+
!onRefresh ||
|
|
785
|
+
transaction.status === 'finalized' ||
|
|
786
|
+
transaction.status === 'failed' ||
|
|
787
|
+
transaction.status === 'cancelled'
|
|
788
|
+
) {
|
|
789
|
+
return
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const interval = setInterval(onRefresh, pollingInterval)
|
|
793
|
+
return () => clearInterval(interval)
|
|
794
|
+
}, [pollingInterval, onRefresh, transaction.status])
|
|
795
|
+
|
|
796
|
+
// Toggle expanded state
|
|
797
|
+
const toggleExpanded = useCallback(() => {
|
|
798
|
+
setIsExpanded((prev) => !prev)
|
|
799
|
+
}, [])
|
|
800
|
+
|
|
801
|
+
// Can show retry button
|
|
802
|
+
const canRetry = transaction.status === 'failed' && onRetry
|
|
803
|
+
|
|
804
|
+
// Can show cancel button
|
|
805
|
+
const canCancel = transaction.status === 'pending' && onCancel
|
|
806
|
+
|
|
807
|
+
return (
|
|
808
|
+
<>
|
|
809
|
+
<style>{styles}</style>
|
|
810
|
+
|
|
811
|
+
<div
|
|
812
|
+
className={`sip-tx-tracker ${className}`}
|
|
813
|
+
data-size={size}
|
|
814
|
+
data-status={transaction.status}
|
|
815
|
+
>
|
|
816
|
+
{/* Header */}
|
|
817
|
+
<div className="sip-tx-header">
|
|
818
|
+
<div className="sip-tx-header-left">
|
|
819
|
+
<div
|
|
820
|
+
className="sip-tx-status-icon"
|
|
821
|
+
data-status={transaction.status}
|
|
822
|
+
data-testid="status-icon"
|
|
823
|
+
>
|
|
824
|
+
{StatusIcons[transaction.status]}
|
|
825
|
+
</div>
|
|
826
|
+
<div className="sip-tx-status-info">
|
|
827
|
+
<span className="sip-tx-status-title">{STATUS_TITLES[transaction.status]}</span>
|
|
828
|
+
<span className="sip-tx-status-subtitle">
|
|
829
|
+
{networkName} • {truncatedHash}
|
|
830
|
+
</span>
|
|
831
|
+
</div>
|
|
832
|
+
</div>
|
|
833
|
+
|
|
834
|
+
<div className="sip-tx-header-actions">
|
|
835
|
+
{onRefresh && transaction.status !== 'finalized' && (
|
|
836
|
+
<button
|
|
837
|
+
type="button"
|
|
838
|
+
className="sip-tx-action-btn sip-tx-action-btn-secondary"
|
|
839
|
+
onClick={onRefresh}
|
|
840
|
+
aria-label="Refresh status"
|
|
841
|
+
>
|
|
842
|
+
Refresh
|
|
843
|
+
</button>
|
|
844
|
+
)}
|
|
845
|
+
{canRetry && (
|
|
846
|
+
<button
|
|
847
|
+
type="button"
|
|
848
|
+
className="sip-tx-action-btn sip-tx-action-btn-primary"
|
|
849
|
+
onClick={onRetry}
|
|
850
|
+
aria-label="Retry transaction"
|
|
851
|
+
>
|
|
852
|
+
Retry
|
|
853
|
+
</button>
|
|
854
|
+
)}
|
|
855
|
+
{canCancel && (
|
|
856
|
+
<button
|
|
857
|
+
type="button"
|
|
858
|
+
className="sip-tx-action-btn sip-tx-action-btn-danger"
|
|
859
|
+
onClick={onCancel}
|
|
860
|
+
aria-label="Cancel transaction"
|
|
861
|
+
>
|
|
862
|
+
Cancel
|
|
863
|
+
</button>
|
|
864
|
+
)}
|
|
865
|
+
</div>
|
|
866
|
+
</div>
|
|
867
|
+
|
|
868
|
+
{/* Progress bar */}
|
|
869
|
+
{transaction.status !== 'cancelled' && (
|
|
870
|
+
<div className="sip-tx-progress">
|
|
871
|
+
<div className="sip-tx-progress-bar">
|
|
872
|
+
<div
|
|
873
|
+
className="sip-tx-progress-fill"
|
|
874
|
+
data-status={transaction.status}
|
|
875
|
+
style={{ width: `${progressPercentage}%` }}
|
|
876
|
+
role="progressbar"
|
|
877
|
+
aria-valuenow={progressPercentage}
|
|
878
|
+
aria-valuemin={0}
|
|
879
|
+
aria-valuemax={100}
|
|
880
|
+
data-testid="progress-bar"
|
|
881
|
+
/>
|
|
882
|
+
</div>
|
|
883
|
+
<div className="sip-tx-progress-info">
|
|
884
|
+
<span>
|
|
885
|
+
{transaction.confirmations}/{transaction.requiredConfirmations} confirmations
|
|
886
|
+
</span>
|
|
887
|
+
<span>{progressPercentage.toFixed(0)}%</span>
|
|
888
|
+
</div>
|
|
889
|
+
</div>
|
|
890
|
+
)}
|
|
891
|
+
|
|
892
|
+
{/* Privacy verification status */}
|
|
893
|
+
{showPrivacyStatus && transaction.privacyVerification && (
|
|
894
|
+
<div className="sip-tx-privacy" data-testid="privacy-status">
|
|
895
|
+
<div className="sip-tx-privacy-title">
|
|
896
|
+
<ShieldIcon />
|
|
897
|
+
Privacy Verification
|
|
898
|
+
</div>
|
|
899
|
+
<div className="sip-tx-privacy-items">
|
|
900
|
+
<div
|
|
901
|
+
className="sip-tx-privacy-item"
|
|
902
|
+
data-status={transaction.privacyVerification.stealthAddressResolved}
|
|
903
|
+
>
|
|
904
|
+
{PrivacyStatusIcons[transaction.privacyVerification.stealthAddressResolved] ??
|
|
905
|
+
PrivacyStatusIcons.pending}
|
|
906
|
+
<span>Stealth Address</span>
|
|
907
|
+
</div>
|
|
908
|
+
<div
|
|
909
|
+
className="sip-tx-privacy-item"
|
|
910
|
+
data-status={transaction.privacyVerification.commitmentVerified}
|
|
911
|
+
>
|
|
912
|
+
{PrivacyStatusIcons[transaction.privacyVerification.commitmentVerified] ??
|
|
913
|
+
PrivacyStatusIcons.pending}
|
|
914
|
+
<span>Commitment</span>
|
|
915
|
+
</div>
|
|
916
|
+
<div
|
|
917
|
+
className="sip-tx-privacy-item"
|
|
918
|
+
data-status={transaction.privacyVerification.viewingKeyGenerated}
|
|
919
|
+
>
|
|
920
|
+
{PrivacyStatusIcons[transaction.privacyVerification.viewingKeyGenerated] ??
|
|
921
|
+
PrivacyStatusIcons.pending}
|
|
922
|
+
<span>Viewing Key</span>
|
|
923
|
+
</div>
|
|
924
|
+
</div>
|
|
925
|
+
</div>
|
|
926
|
+
)}
|
|
927
|
+
|
|
928
|
+
{/* ETA */}
|
|
929
|
+
{estimatedTimeLeft && (
|
|
930
|
+
<div className="sip-tx-eta" data-testid="eta">
|
|
931
|
+
<ClockIcon />
|
|
932
|
+
{estimatedTimeLeft}
|
|
933
|
+
</div>
|
|
934
|
+
)}
|
|
935
|
+
|
|
936
|
+
{/* Error message */}
|
|
937
|
+
{transaction.errorMessage && (
|
|
938
|
+
<div className="sip-tx-error" data-testid="error-message">
|
|
939
|
+
{transaction.errorMessage}
|
|
940
|
+
</div>
|
|
941
|
+
)}
|
|
942
|
+
|
|
943
|
+
{/* Details toggle */}
|
|
944
|
+
<button
|
|
945
|
+
type="button"
|
|
946
|
+
className="sip-tx-details-toggle"
|
|
947
|
+
onClick={toggleExpanded}
|
|
948
|
+
aria-expanded={isExpanded}
|
|
949
|
+
aria-controls="tx-details"
|
|
950
|
+
>
|
|
951
|
+
<span>{isExpanded ? 'Hide Details' : 'Show Details'}</span>
|
|
952
|
+
<ChevronIcon />
|
|
953
|
+
</button>
|
|
954
|
+
|
|
955
|
+
{/* Details section */}
|
|
956
|
+
{isExpanded && (
|
|
957
|
+
<div id="tx-details" className="sip-tx-details" data-testid="details-section">
|
|
958
|
+
<div className="sip-tx-detail-row">
|
|
959
|
+
<span className="sip-tx-detail-label">Transaction Hash</span>
|
|
960
|
+
<span className="sip-tx-detail-value">
|
|
961
|
+
<a href={explorerUrl} target="_blank" rel="noopener noreferrer">
|
|
962
|
+
{truncatedHash}
|
|
963
|
+
</a>
|
|
964
|
+
</span>
|
|
965
|
+
</div>
|
|
966
|
+
|
|
967
|
+
<div className="sip-tx-detail-row">
|
|
968
|
+
<span className="sip-tx-detail-label">Timestamp</span>
|
|
969
|
+
<span className="sip-tx-detail-value">{formattedTime}</span>
|
|
970
|
+
</div>
|
|
971
|
+
|
|
972
|
+
<div className="sip-tx-detail-row">
|
|
973
|
+
<span className="sip-tx-detail-label">From</span>
|
|
974
|
+
<span className="sip-tx-detail-value">{transaction.sender}</span>
|
|
975
|
+
</div>
|
|
976
|
+
|
|
977
|
+
<div className="sip-tx-detail-row">
|
|
978
|
+
<span className="sip-tx-detail-label">
|
|
979
|
+
To {transaction.isStealthReceiver && '(Stealth)'}
|
|
980
|
+
</span>
|
|
981
|
+
<span className="sip-tx-detail-value">{transaction.receiver}</span>
|
|
982
|
+
</div>
|
|
983
|
+
|
|
984
|
+
{transaction.amount && (
|
|
985
|
+
<div className="sip-tx-detail-row">
|
|
986
|
+
<span className="sip-tx-detail-label">Amount</span>
|
|
987
|
+
<span className="sip-tx-detail-value">{transaction.amount}</span>
|
|
988
|
+
</div>
|
|
989
|
+
)}
|
|
990
|
+
|
|
991
|
+
{transaction.gasUsed && (
|
|
992
|
+
<div className="sip-tx-detail-row">
|
|
993
|
+
<span className="sip-tx-detail-label">Gas Used</span>
|
|
994
|
+
<span className="sip-tx-detail-value">{transaction.gasUsed}</span>
|
|
995
|
+
</div>
|
|
996
|
+
)}
|
|
997
|
+
|
|
998
|
+
{transaction.fee && (
|
|
999
|
+
<div className="sip-tx-detail-row">
|
|
1000
|
+
<span className="sip-tx-detail-label">Fee</span>
|
|
1001
|
+
<span className="sip-tx-detail-value">{transaction.fee}</span>
|
|
1002
|
+
</div>
|
|
1003
|
+
)}
|
|
1004
|
+
|
|
1005
|
+
{transaction.blockHeight && (
|
|
1006
|
+
<div className="sip-tx-detail-row">
|
|
1007
|
+
<span className="sip-tx-detail-label">Block</span>
|
|
1008
|
+
<span className="sip-tx-detail-value">#{transaction.blockHeight}</span>
|
|
1009
|
+
</div>
|
|
1010
|
+
)}
|
|
1011
|
+
</div>
|
|
1012
|
+
)}
|
|
1013
|
+
</div>
|
|
1014
|
+
</>
|
|
1015
|
+
)
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Hook to manage transaction tracking state
|
|
1020
|
+
*/
|
|
1021
|
+
export function useTransactionTracker(
|
|
1022
|
+
initialTransaction?: PrivacyTransaction,
|
|
1023
|
+
options: {
|
|
1024
|
+
pollingInterval?: number
|
|
1025
|
+
onStatusChange?: (status: TransactionStatus) => void
|
|
1026
|
+
} = {}
|
|
1027
|
+
) {
|
|
1028
|
+
const { pollingInterval = 0, onStatusChange } = options
|
|
1029
|
+
const [transaction, setTransaction] = useState<PrivacyTransaction | null>(
|
|
1030
|
+
initialTransaction ?? null
|
|
1031
|
+
)
|
|
1032
|
+
const [isPolling, setIsPolling] = useState(pollingInterval > 0)
|
|
1033
|
+
|
|
1034
|
+
// Update transaction
|
|
1035
|
+
const updateTransaction = useCallback(
|
|
1036
|
+
(updates: Partial<PrivacyTransaction>) => {
|
|
1037
|
+
setTransaction((prev) => {
|
|
1038
|
+
if (!prev) return prev
|
|
1039
|
+
|
|
1040
|
+
const newTx = { ...prev, ...updates }
|
|
1041
|
+
|
|
1042
|
+
if (updates.status && updates.status !== prev.status) {
|
|
1043
|
+
onStatusChange?.(updates.status)
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
return newTx
|
|
1047
|
+
})
|
|
1048
|
+
},
|
|
1049
|
+
[onStatusChange]
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
// Start polling
|
|
1053
|
+
const startPolling = useCallback(() => {
|
|
1054
|
+
setIsPolling(true)
|
|
1055
|
+
}, [])
|
|
1056
|
+
|
|
1057
|
+
// Stop polling
|
|
1058
|
+
const stopPolling = useCallback(() => {
|
|
1059
|
+
setIsPolling(false)
|
|
1060
|
+
}, [])
|
|
1061
|
+
|
|
1062
|
+
// Check if transaction is final
|
|
1063
|
+
const isFinal = useMemo(() => {
|
|
1064
|
+
if (!transaction) return false
|
|
1065
|
+
return ['finalized', 'failed', 'cancelled'].includes(transaction.status)
|
|
1066
|
+
}, [transaction])
|
|
1067
|
+
|
|
1068
|
+
return {
|
|
1069
|
+
transaction,
|
|
1070
|
+
setTransaction,
|
|
1071
|
+
updateTransaction,
|
|
1072
|
+
isPolling,
|
|
1073
|
+
startPolling,
|
|
1074
|
+
stopPolling,
|
|
1075
|
+
isFinal,
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
export default TransactionTracker
|