@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,1187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ethereum Privacy Transaction History
|
|
3
|
+
*
|
|
4
|
+
* Component for displaying history of Ethereum privacy transactions.
|
|
5
|
+
*
|
|
6
|
+
* @module components/ethereum/transaction-history
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useState, useCallback, useMemo, useEffect } from 'react'
|
|
10
|
+
import { ETHEREUM_NETWORKS, type EthereumNetwork } from './transaction-tracker'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Transaction direction
|
|
14
|
+
*/
|
|
15
|
+
export type TransactionDirection = 'sent' | 'received' | 'claimed'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Transaction type
|
|
19
|
+
*/
|
|
20
|
+
export type TransactionType = 'stealth_transfer' | 'standard_transfer' | 'claim'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Privacy transaction history item
|
|
24
|
+
*/
|
|
25
|
+
export interface PrivacyTransactionHistoryItem {
|
|
26
|
+
/** Transaction hash */
|
|
27
|
+
hash: string
|
|
28
|
+
/** Transaction direction */
|
|
29
|
+
direction: TransactionDirection
|
|
30
|
+
/** Transaction type */
|
|
31
|
+
type: TransactionType
|
|
32
|
+
/** Timestamp in milliseconds */
|
|
33
|
+
timestamp: number
|
|
34
|
+
/** Block number */
|
|
35
|
+
blockNumber?: number
|
|
36
|
+
/** From address */
|
|
37
|
+
from: string
|
|
38
|
+
/** To address (may be stealth address) */
|
|
39
|
+
to: string
|
|
40
|
+
/** Whether to address is stealth */
|
|
41
|
+
isStealthAddress: boolean
|
|
42
|
+
/** Amount in token units (string for precision) */
|
|
43
|
+
amount: string
|
|
44
|
+
/** Token symbol */
|
|
45
|
+
tokenSymbol: string
|
|
46
|
+
/** Token decimals */
|
|
47
|
+
tokenDecimals: number
|
|
48
|
+
/** USD value at time of transaction */
|
|
49
|
+
usdValue?: string
|
|
50
|
+
/** Current USD value */
|
|
51
|
+
currentUsdValue?: string
|
|
52
|
+
/** Gas used */
|
|
53
|
+
gasUsed?: string
|
|
54
|
+
/** Gas price in gwei */
|
|
55
|
+
gasPrice?: string
|
|
56
|
+
/** Transaction fee in ETH */
|
|
57
|
+
fee?: string
|
|
58
|
+
/** Status */
|
|
59
|
+
status: 'pending' | 'confirmed' | 'failed'
|
|
60
|
+
/** Ephemeral public key for stealth transactions */
|
|
61
|
+
ephemeralPublicKey?: string
|
|
62
|
+
/** View tag for efficient scanning */
|
|
63
|
+
viewTag?: number
|
|
64
|
+
/** Claim key (only for received/claimed) */
|
|
65
|
+
claimKey?: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Filter options for transaction history
|
|
70
|
+
*/
|
|
71
|
+
export interface TransactionHistoryFilter {
|
|
72
|
+
direction?: TransactionDirection | 'all'
|
|
73
|
+
type?: TransactionType | 'all'
|
|
74
|
+
status?: 'pending' | 'confirmed' | 'failed' | 'all'
|
|
75
|
+
tokenSymbol?: string
|
|
76
|
+
fromDate?: Date
|
|
77
|
+
toDate?: Date
|
|
78
|
+
minAmount?: string
|
|
79
|
+
maxAmount?: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Sort options for transaction history
|
|
84
|
+
*/
|
|
85
|
+
export interface TransactionHistorySort {
|
|
86
|
+
field: 'timestamp' | 'amount' | 'usdValue'
|
|
87
|
+
direction: 'asc' | 'desc'
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* TransactionHistory component props
|
|
92
|
+
*/
|
|
93
|
+
export interface TransactionHistoryProps {
|
|
94
|
+
/** List of transactions */
|
|
95
|
+
transactions: PrivacyTransactionHistoryItem[]
|
|
96
|
+
/** Loading state */
|
|
97
|
+
isLoading?: boolean
|
|
98
|
+
/** Error message */
|
|
99
|
+
error?: string | null
|
|
100
|
+
/** Filter options */
|
|
101
|
+
filter?: TransactionHistoryFilter
|
|
102
|
+
/** Sort options */
|
|
103
|
+
sort?: TransactionHistorySort
|
|
104
|
+
/** Callback when filter changes */
|
|
105
|
+
onFilterChange?: (filter: TransactionHistoryFilter) => void
|
|
106
|
+
/** Callback when sort changes */
|
|
107
|
+
onSortChange?: (sort: TransactionHistorySort) => void
|
|
108
|
+
/** Callback to load more transactions */
|
|
109
|
+
onLoadMore?: () => void
|
|
110
|
+
/** Whether more transactions are available */
|
|
111
|
+
hasMore?: boolean
|
|
112
|
+
/** Callback to export transactions */
|
|
113
|
+
onExport?: (format: 'csv' | 'json') => void
|
|
114
|
+
/** Callback when transaction is selected */
|
|
115
|
+
onTransactionSelect?: (tx: PrivacyTransactionHistoryItem) => void
|
|
116
|
+
/** Network configuration */
|
|
117
|
+
network?: string | EthereumNetwork
|
|
118
|
+
/** Items per page for pagination */
|
|
119
|
+
pageSize?: number
|
|
120
|
+
/** Custom class name */
|
|
121
|
+
className?: string
|
|
122
|
+
/** Size variant */
|
|
123
|
+
size?: 'sm' | 'md' | 'lg'
|
|
124
|
+
/** Show USD values */
|
|
125
|
+
showUsdValues?: boolean
|
|
126
|
+
/** Show filters */
|
|
127
|
+
showFilters?: boolean
|
|
128
|
+
/** Show export button */
|
|
129
|
+
showExport?: boolean
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* CSS styles for the component
|
|
134
|
+
*/
|
|
135
|
+
const styles = `
|
|
136
|
+
.sip-tx-history {
|
|
137
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
138
|
+
background: #ffffff;
|
|
139
|
+
border: 1px solid #e5e7eb;
|
|
140
|
+
border-radius: 12px;
|
|
141
|
+
overflow: hidden;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.sip-tx-history[data-size="sm"] {
|
|
145
|
+
border-radius: 8px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.sip-tx-history[data-size="lg"] {
|
|
149
|
+
border-radius: 16px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* Header */
|
|
153
|
+
.sip-tx-history-header {
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
justify-content: space-between;
|
|
157
|
+
padding: 16px 20px;
|
|
158
|
+
background: #f9fafb;
|
|
159
|
+
border-bottom: 1px solid #e5e7eb;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.sip-tx-history[data-size="sm"] .sip-tx-history-header {
|
|
163
|
+
padding: 12px 16px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.sip-tx-history-title {
|
|
167
|
+
display: flex;
|
|
168
|
+
align-items: center;
|
|
169
|
+
gap: 10px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.sip-tx-history-title svg {
|
|
173
|
+
width: 20px;
|
|
174
|
+
height: 20px;
|
|
175
|
+
color: #6b7280;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.sip-tx-history-title h2 {
|
|
179
|
+
font-size: 16px;
|
|
180
|
+
font-weight: 600;
|
|
181
|
+
color: #111827;
|
|
182
|
+
margin: 0;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.sip-tx-history-actions {
|
|
186
|
+
display: flex;
|
|
187
|
+
gap: 8px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* Filter bar */
|
|
191
|
+
.sip-tx-history-filters {
|
|
192
|
+
display: flex;
|
|
193
|
+
align-items: center;
|
|
194
|
+
gap: 12px;
|
|
195
|
+
padding: 12px 20px;
|
|
196
|
+
background: #f9fafb;
|
|
197
|
+
border-bottom: 1px solid #e5e7eb;
|
|
198
|
+
flex-wrap: wrap;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.sip-tx-history-filter-group {
|
|
202
|
+
display: flex;
|
|
203
|
+
align-items: center;
|
|
204
|
+
gap: 6px;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.sip-tx-history-filter-label {
|
|
208
|
+
font-size: 12px;
|
|
209
|
+
font-weight: 500;
|
|
210
|
+
color: #6b7280;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.sip-tx-history-select {
|
|
214
|
+
padding: 6px 10px;
|
|
215
|
+
border: 1px solid #d1d5db;
|
|
216
|
+
border-radius: 6px;
|
|
217
|
+
font-size: 13px;
|
|
218
|
+
color: #374151;
|
|
219
|
+
background: white;
|
|
220
|
+
cursor: pointer;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.sip-tx-history-select:focus {
|
|
224
|
+
outline: none;
|
|
225
|
+
border-color: #6366f1;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* Buttons */
|
|
229
|
+
.sip-tx-history-btn {
|
|
230
|
+
display: inline-flex;
|
|
231
|
+
align-items: center;
|
|
232
|
+
justify-content: center;
|
|
233
|
+
gap: 6px;
|
|
234
|
+
padding: 8px 14px;
|
|
235
|
+
border: none;
|
|
236
|
+
border-radius: 6px;
|
|
237
|
+
font-size: 13px;
|
|
238
|
+
font-weight: 500;
|
|
239
|
+
cursor: pointer;
|
|
240
|
+
transition: all 0.2s;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.sip-tx-history-btn svg {
|
|
244
|
+
width: 16px;
|
|
245
|
+
height: 16px;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.sip-tx-history-btn-secondary {
|
|
249
|
+
background: #f3f4f6;
|
|
250
|
+
color: #374151;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.sip-tx-history-btn-secondary:hover {
|
|
254
|
+
background: #e5e7eb;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.sip-tx-history-btn-primary {
|
|
258
|
+
background: #6366f1;
|
|
259
|
+
color: white;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.sip-tx-history-btn-primary:hover {
|
|
263
|
+
background: #4f46e5;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.sip-tx-history-btn:disabled {
|
|
267
|
+
opacity: 0.5;
|
|
268
|
+
cursor: not-allowed;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* List */
|
|
272
|
+
.sip-tx-history-list {
|
|
273
|
+
padding: 0;
|
|
274
|
+
margin: 0;
|
|
275
|
+
list-style: none;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.sip-tx-history-empty {
|
|
279
|
+
padding: 48px 20px;
|
|
280
|
+
text-align: center;
|
|
281
|
+
color: #6b7280;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.sip-tx-history-empty svg {
|
|
285
|
+
width: 48px;
|
|
286
|
+
height: 48px;
|
|
287
|
+
margin-bottom: 12px;
|
|
288
|
+
opacity: 0.4;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.sip-tx-history-empty p {
|
|
292
|
+
margin: 0;
|
|
293
|
+
font-size: 14px;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/* Item */
|
|
297
|
+
.sip-tx-history-item {
|
|
298
|
+
display: flex;
|
|
299
|
+
align-items: center;
|
|
300
|
+
justify-content: space-between;
|
|
301
|
+
padding: 16px 20px;
|
|
302
|
+
border-bottom: 1px solid #e5e7eb;
|
|
303
|
+
cursor: pointer;
|
|
304
|
+
transition: background 0.2s;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.sip-tx-history-item:last-child {
|
|
308
|
+
border-bottom: none;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.sip-tx-history-item:hover {
|
|
312
|
+
background: #f9fafb;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.sip-tx-history[data-size="sm"] .sip-tx-history-item {
|
|
316
|
+
padding: 12px 16px;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.sip-tx-history-item-left {
|
|
320
|
+
display: flex;
|
|
321
|
+
align-items: center;
|
|
322
|
+
gap: 12px;
|
|
323
|
+
flex: 1;
|
|
324
|
+
min-width: 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.sip-tx-history-item-icon {
|
|
328
|
+
display: flex;
|
|
329
|
+
align-items: center;
|
|
330
|
+
justify-content: center;
|
|
331
|
+
width: 40px;
|
|
332
|
+
height: 40px;
|
|
333
|
+
border-radius: 10px;
|
|
334
|
+
flex-shrink: 0;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.sip-tx-history-item-icon svg {
|
|
338
|
+
width: 20px;
|
|
339
|
+
height: 20px;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.sip-tx-history-item-icon[data-direction="sent"] {
|
|
343
|
+
background: #fee2e2;
|
|
344
|
+
color: #dc2626;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.sip-tx-history-item-icon[data-direction="received"] {
|
|
348
|
+
background: #d1fae5;
|
|
349
|
+
color: #059669;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.sip-tx-history-item-icon[data-direction="claimed"] {
|
|
353
|
+
background: #dbeafe;
|
|
354
|
+
color: #2563eb;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.sip-tx-history-item-details {
|
|
358
|
+
flex: 1;
|
|
359
|
+
min-width: 0;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.sip-tx-history-item-title {
|
|
363
|
+
display: flex;
|
|
364
|
+
align-items: center;
|
|
365
|
+
gap: 8px;
|
|
366
|
+
font-size: 14px;
|
|
367
|
+
font-weight: 600;
|
|
368
|
+
color: #111827;
|
|
369
|
+
margin-bottom: 2px;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.sip-tx-history-item-stealth-badge {
|
|
373
|
+
display: inline-flex;
|
|
374
|
+
align-items: center;
|
|
375
|
+
gap: 4px;
|
|
376
|
+
padding: 2px 6px;
|
|
377
|
+
font-size: 10px;
|
|
378
|
+
font-weight: 600;
|
|
379
|
+
text-transform: uppercase;
|
|
380
|
+
letter-spacing: 0.05em;
|
|
381
|
+
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
|
382
|
+
color: white;
|
|
383
|
+
border-radius: 4px;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.sip-tx-history-item-stealth-badge svg {
|
|
387
|
+
width: 10px;
|
|
388
|
+
height: 10px;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.sip-tx-history-item-address {
|
|
392
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
393
|
+
font-size: 12px;
|
|
394
|
+
color: #6b7280;
|
|
395
|
+
overflow: hidden;
|
|
396
|
+
text-overflow: ellipsis;
|
|
397
|
+
white-space: nowrap;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.sip-tx-history-item-meta {
|
|
401
|
+
display: flex;
|
|
402
|
+
align-items: center;
|
|
403
|
+
gap: 8px;
|
|
404
|
+
margin-top: 4px;
|
|
405
|
+
font-size: 11px;
|
|
406
|
+
color: #9ca3af;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.sip-tx-history-item-status {
|
|
410
|
+
display: inline-flex;
|
|
411
|
+
padding: 1px 6px;
|
|
412
|
+
border-radius: 3px;
|
|
413
|
+
font-weight: 500;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.sip-tx-history-item-status[data-status="pending"] {
|
|
417
|
+
background: #fef3c7;
|
|
418
|
+
color: #d97706;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.sip-tx-history-item-status[data-status="confirmed"] {
|
|
422
|
+
background: #d1fae5;
|
|
423
|
+
color: #059669;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.sip-tx-history-item-status[data-status="failed"] {
|
|
427
|
+
background: #fee2e2;
|
|
428
|
+
color: #dc2626;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.sip-tx-history-item-right {
|
|
432
|
+
display: flex;
|
|
433
|
+
flex-direction: column;
|
|
434
|
+
align-items: flex-end;
|
|
435
|
+
gap: 2px;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.sip-tx-history-item-amount {
|
|
439
|
+
font-size: 14px;
|
|
440
|
+
font-weight: 600;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.sip-tx-history-item-amount[data-direction="sent"] {
|
|
444
|
+
color: #dc2626;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.sip-tx-history-item-amount[data-direction="received"],
|
|
448
|
+
.sip-tx-history-item-amount[data-direction="claimed"] {
|
|
449
|
+
color: #059669;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
.sip-tx-history-item-usd {
|
|
453
|
+
font-size: 12px;
|
|
454
|
+
color: #6b7280;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/* Load more */
|
|
458
|
+
.sip-tx-history-load-more {
|
|
459
|
+
padding: 16px 20px;
|
|
460
|
+
text-align: center;
|
|
461
|
+
border-top: 1px solid #e5e7eb;
|
|
462
|
+
background: #f9fafb;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/* Loading */
|
|
466
|
+
.sip-tx-history-loading {
|
|
467
|
+
display: flex;
|
|
468
|
+
align-items: center;
|
|
469
|
+
justify-content: center;
|
|
470
|
+
gap: 8px;
|
|
471
|
+
padding: 32px 20px;
|
|
472
|
+
color: #6b7280;
|
|
473
|
+
font-size: 14px;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.sip-tx-history-loading-spinner {
|
|
477
|
+
width: 20px;
|
|
478
|
+
height: 20px;
|
|
479
|
+
border: 2px solid #e5e7eb;
|
|
480
|
+
border-top-color: #6366f1;
|
|
481
|
+
border-radius: 50%;
|
|
482
|
+
animation: sip-tx-history-spin 1s linear infinite;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
@keyframes sip-tx-history-spin {
|
|
486
|
+
to { transform: rotate(360deg); }
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/* Error */
|
|
490
|
+
.sip-tx-history-error {
|
|
491
|
+
display: flex;
|
|
492
|
+
align-items: center;
|
|
493
|
+
justify-content: center;
|
|
494
|
+
gap: 8px;
|
|
495
|
+
padding: 24px 20px;
|
|
496
|
+
background: #fee2e2;
|
|
497
|
+
color: #dc2626;
|
|
498
|
+
font-size: 14px;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.sip-tx-history-error svg {
|
|
502
|
+
width: 20px;
|
|
503
|
+
height: 20px;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/* Dark mode */
|
|
507
|
+
@media (prefers-color-scheme: dark) {
|
|
508
|
+
.sip-tx-history {
|
|
509
|
+
background: #1f2937;
|
|
510
|
+
border-color: #374151;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.sip-tx-history-header {
|
|
514
|
+
background: #111827;
|
|
515
|
+
border-color: #374151;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.sip-tx-history-title h2 {
|
|
519
|
+
color: #f9fafb;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.sip-tx-history-filters {
|
|
523
|
+
background: #111827;
|
|
524
|
+
border-color: #374151;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.sip-tx-history-select {
|
|
528
|
+
background: #374151;
|
|
529
|
+
border-color: #4b5563;
|
|
530
|
+
color: #e5e7eb;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.sip-tx-history-item:hover {
|
|
534
|
+
background: #111827;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.sip-tx-history-item-title {
|
|
538
|
+
color: #f9fafb;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.sip-tx-history-item-address {
|
|
542
|
+
color: #9ca3af;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.sip-tx-history-btn-secondary {
|
|
546
|
+
background: #374151;
|
|
547
|
+
color: #e5e7eb;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.sip-tx-history-btn-secondary:hover {
|
|
551
|
+
background: #4b5563;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.sip-tx-history-empty {
|
|
555
|
+
color: #9ca3af;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.sip-tx-history-load-more {
|
|
559
|
+
background: #111827;
|
|
560
|
+
border-color: #374151;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
`
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Icons
|
|
567
|
+
*/
|
|
568
|
+
const HistoryIcon = () => (
|
|
569
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
570
|
+
<circle cx="12" cy="12" r="10" />
|
|
571
|
+
<polyline points="12 6 12 12 16 14" />
|
|
572
|
+
</svg>
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
const ArrowUpIcon = () => (
|
|
576
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
577
|
+
<line x1="12" y1="19" x2="12" y2="5" />
|
|
578
|
+
<polyline points="5 12 12 5 19 12" />
|
|
579
|
+
</svg>
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
const ArrowDownIcon = () => (
|
|
583
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
584
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
585
|
+
<polyline points="19 12 12 19 5 12" />
|
|
586
|
+
</svg>
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
const CheckCircleIcon = () => (
|
|
590
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
591
|
+
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
|
|
592
|
+
<polyline points="22 4 12 14.01 9 11.01" />
|
|
593
|
+
</svg>
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
const ShieldIcon = () => (
|
|
597
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
598
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
599
|
+
</svg>
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
const DownloadIcon = () => (
|
|
603
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
604
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
605
|
+
<polyline points="7 10 12 15 17 10" />
|
|
606
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
607
|
+
</svg>
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
const AlertIcon = () => (
|
|
611
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
612
|
+
<circle cx="12" cy="12" r="10" />
|
|
613
|
+
<line x1="12" y1="8" x2="12" y2="12" />
|
|
614
|
+
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
615
|
+
</svg>
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Format address for display
|
|
620
|
+
*/
|
|
621
|
+
function formatAddress(address: string): string {
|
|
622
|
+
if (address.length <= 14) return address
|
|
623
|
+
return `${address.slice(0, 8)}...${address.slice(-6)}`
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Format timestamp
|
|
628
|
+
*/
|
|
629
|
+
function formatTimestamp(timestamp: number): string {
|
|
630
|
+
const date = new Date(timestamp)
|
|
631
|
+
const now = new Date()
|
|
632
|
+
const diffMs = now.getTime() - date.getTime()
|
|
633
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
|
634
|
+
|
|
635
|
+
if (diffDays === 0) {
|
|
636
|
+
return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
|
|
637
|
+
}
|
|
638
|
+
if (diffDays === 1) {
|
|
639
|
+
return 'Yesterday'
|
|
640
|
+
}
|
|
641
|
+
if (diffDays < 7) {
|
|
642
|
+
return `${diffDays} days ago`
|
|
643
|
+
}
|
|
644
|
+
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Format amount
|
|
649
|
+
*/
|
|
650
|
+
function formatAmount(amount: string, decimals: number, symbol: string): string {
|
|
651
|
+
const value = parseFloat(amount) / Math.pow(10, decimals)
|
|
652
|
+
const formatted = value.toLocaleString(undefined, {
|
|
653
|
+
minimumFractionDigits: 0,
|
|
654
|
+
maximumFractionDigits: 6,
|
|
655
|
+
})
|
|
656
|
+
return `${formatted} ${symbol}`
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Get direction label
|
|
661
|
+
*/
|
|
662
|
+
function getDirectionLabel(direction: TransactionDirection): string {
|
|
663
|
+
switch (direction) {
|
|
664
|
+
case 'sent':
|
|
665
|
+
return 'Sent'
|
|
666
|
+
case 'received':
|
|
667
|
+
return 'Received'
|
|
668
|
+
case 'claimed':
|
|
669
|
+
return 'Claimed'
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* TransactionHistory - Display privacy transaction history
|
|
675
|
+
*
|
|
676
|
+
* @example Basic usage
|
|
677
|
+
* ```tsx
|
|
678
|
+
* import { TransactionHistory } from '@sip-protocol/react'
|
|
679
|
+
*
|
|
680
|
+
* function HistoryView() {
|
|
681
|
+
* const [transactions, setTransactions] = useState([])
|
|
682
|
+
*
|
|
683
|
+
* return (
|
|
684
|
+
* <TransactionHistory
|
|
685
|
+
* transactions={transactions}
|
|
686
|
+
* onTransactionSelect={(tx) => console.log('Selected:', tx)}
|
|
687
|
+
* network="mainnet"
|
|
688
|
+
* showFilters
|
|
689
|
+
* showExport
|
|
690
|
+
* />
|
|
691
|
+
* )
|
|
692
|
+
* }
|
|
693
|
+
* ```
|
|
694
|
+
*/
|
|
695
|
+
export function TransactionHistory({
|
|
696
|
+
transactions,
|
|
697
|
+
isLoading = false,
|
|
698
|
+
error = null,
|
|
699
|
+
filter = { direction: 'all' },
|
|
700
|
+
sort = { field: 'timestamp', direction: 'desc' },
|
|
701
|
+
onFilterChange,
|
|
702
|
+
onSortChange,
|
|
703
|
+
onLoadMore,
|
|
704
|
+
hasMore = false,
|
|
705
|
+
onExport,
|
|
706
|
+
onTransactionSelect,
|
|
707
|
+
network = 'mainnet',
|
|
708
|
+
pageSize = 20,
|
|
709
|
+
className = '',
|
|
710
|
+
size = 'md',
|
|
711
|
+
showUsdValues = true,
|
|
712
|
+
showFilters = true,
|
|
713
|
+
showExport = true,
|
|
714
|
+
}: TransactionHistoryProps) {
|
|
715
|
+
const [currentPage, setCurrentPage] = useState(1)
|
|
716
|
+
|
|
717
|
+
// Resolve network config
|
|
718
|
+
const networkConfig = useMemo((): EthereumNetwork => {
|
|
719
|
+
if (typeof network === 'object') return network
|
|
720
|
+
return ETHEREUM_NETWORKS[network] ?? ETHEREUM_NETWORKS.mainnet
|
|
721
|
+
}, [network])
|
|
722
|
+
|
|
723
|
+
// Filter and sort transactions
|
|
724
|
+
const filteredTransactions = useMemo(() => {
|
|
725
|
+
let result = [...transactions]
|
|
726
|
+
|
|
727
|
+
// Apply filters
|
|
728
|
+
if (filter.direction && filter.direction !== 'all') {
|
|
729
|
+
result = result.filter((tx) => tx.direction === filter.direction)
|
|
730
|
+
}
|
|
731
|
+
if (filter.type && filter.type !== 'all') {
|
|
732
|
+
result = result.filter((tx) => tx.type === filter.type)
|
|
733
|
+
}
|
|
734
|
+
if (filter.status && filter.status !== 'all') {
|
|
735
|
+
result = result.filter((tx) => tx.status === filter.status)
|
|
736
|
+
}
|
|
737
|
+
if (filter.tokenSymbol) {
|
|
738
|
+
result = result.filter((tx) => tx.tokenSymbol === filter.tokenSymbol)
|
|
739
|
+
}
|
|
740
|
+
if (filter.fromDate) {
|
|
741
|
+
result = result.filter((tx) => tx.timestamp >= filter.fromDate!.getTime())
|
|
742
|
+
}
|
|
743
|
+
if (filter.toDate) {
|
|
744
|
+
result = result.filter((tx) => tx.timestamp <= filter.toDate!.getTime())
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Apply sort
|
|
748
|
+
result.sort((a, b) => {
|
|
749
|
+
let cmp = 0
|
|
750
|
+
switch (sort.field) {
|
|
751
|
+
case 'timestamp':
|
|
752
|
+
cmp = a.timestamp - b.timestamp
|
|
753
|
+
break
|
|
754
|
+
case 'amount':
|
|
755
|
+
cmp = parseFloat(a.amount) - parseFloat(b.amount)
|
|
756
|
+
break
|
|
757
|
+
case 'usdValue':
|
|
758
|
+
cmp = parseFloat(a.usdValue || '0') - parseFloat(b.usdValue || '0')
|
|
759
|
+
break
|
|
760
|
+
}
|
|
761
|
+
return sort.direction === 'asc' ? cmp : -cmp
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
return result
|
|
765
|
+
}, [transactions, filter, sort])
|
|
766
|
+
|
|
767
|
+
// Paginated transactions
|
|
768
|
+
const paginatedTransactions = useMemo(() => {
|
|
769
|
+
return filteredTransactions.slice(0, currentPage * pageSize)
|
|
770
|
+
}, [filteredTransactions, currentPage, pageSize])
|
|
771
|
+
|
|
772
|
+
// Handle filter change
|
|
773
|
+
const handleFilterChange = useCallback(
|
|
774
|
+
(key: keyof TransactionHistoryFilter, value: string) => {
|
|
775
|
+
const newFilter = { ...filter, [key]: value }
|
|
776
|
+
onFilterChange?.(newFilter)
|
|
777
|
+
setCurrentPage(1)
|
|
778
|
+
},
|
|
779
|
+
[filter, onFilterChange]
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
// Handle load more
|
|
783
|
+
const handleLoadMore = useCallback(() => {
|
|
784
|
+
if (paginatedTransactions.length < filteredTransactions.length) {
|
|
785
|
+
setCurrentPage((p) => p + 1)
|
|
786
|
+
} else {
|
|
787
|
+
onLoadMore?.()
|
|
788
|
+
}
|
|
789
|
+
}, [paginatedTransactions, filteredTransactions, onLoadMore])
|
|
790
|
+
|
|
791
|
+
// Can load more
|
|
792
|
+
const canLoadMore = useMemo(() => {
|
|
793
|
+
return paginatedTransactions.length < filteredTransactions.length || hasMore
|
|
794
|
+
}, [paginatedTransactions, filteredTransactions, hasMore])
|
|
795
|
+
|
|
796
|
+
// Get explorer URL for transaction
|
|
797
|
+
const getExplorerUrl = useCallback(
|
|
798
|
+
(hash: string) => `${networkConfig.explorerUrl}/tx/${hash}`,
|
|
799
|
+
[networkConfig]
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
return (
|
|
803
|
+
<>
|
|
804
|
+
<style>{styles}</style>
|
|
805
|
+
|
|
806
|
+
<div className={`sip-tx-history ${className}`} data-size={size}>
|
|
807
|
+
{/* Header */}
|
|
808
|
+
<div className="sip-tx-history-header">
|
|
809
|
+
<div className="sip-tx-history-title">
|
|
810
|
+
<HistoryIcon />
|
|
811
|
+
<h2>Transaction History</h2>
|
|
812
|
+
</div>
|
|
813
|
+
<div className="sip-tx-history-actions">
|
|
814
|
+
{showExport && onExport && (
|
|
815
|
+
<button
|
|
816
|
+
type="button"
|
|
817
|
+
className="sip-tx-history-btn sip-tx-history-btn-secondary"
|
|
818
|
+
onClick={() => onExport('csv')}
|
|
819
|
+
disabled={transactions.length === 0}
|
|
820
|
+
aria-label="Export as CSV"
|
|
821
|
+
>
|
|
822
|
+
<DownloadIcon />
|
|
823
|
+
Export
|
|
824
|
+
</button>
|
|
825
|
+
)}
|
|
826
|
+
</div>
|
|
827
|
+
</div>
|
|
828
|
+
|
|
829
|
+
{/* Filters */}
|
|
830
|
+
{showFilters && (
|
|
831
|
+
<div className="sip-tx-history-filters">
|
|
832
|
+
<div className="sip-tx-history-filter-group">
|
|
833
|
+
<span className="sip-tx-history-filter-label">Direction:</span>
|
|
834
|
+
<select
|
|
835
|
+
className="sip-tx-history-select"
|
|
836
|
+
value={filter.direction || 'all'}
|
|
837
|
+
onChange={(e) => handleFilterChange('direction', e.target.value)}
|
|
838
|
+
>
|
|
839
|
+
<option value="all">All</option>
|
|
840
|
+
<option value="sent">Sent</option>
|
|
841
|
+
<option value="received">Received</option>
|
|
842
|
+
<option value="claimed">Claimed</option>
|
|
843
|
+
</select>
|
|
844
|
+
</div>
|
|
845
|
+
|
|
846
|
+
<div className="sip-tx-history-filter-group">
|
|
847
|
+
<span className="sip-tx-history-filter-label">Status:</span>
|
|
848
|
+
<select
|
|
849
|
+
className="sip-tx-history-select"
|
|
850
|
+
value={filter.status || 'all'}
|
|
851
|
+
onChange={(e) => handleFilterChange('status', e.target.value)}
|
|
852
|
+
>
|
|
853
|
+
<option value="all">All</option>
|
|
854
|
+
<option value="pending">Pending</option>
|
|
855
|
+
<option value="confirmed">Confirmed</option>
|
|
856
|
+
<option value="failed">Failed</option>
|
|
857
|
+
</select>
|
|
858
|
+
</div>
|
|
859
|
+
|
|
860
|
+
<div className="sip-tx-history-filter-group">
|
|
861
|
+
<span className="sip-tx-history-filter-label">Sort:</span>
|
|
862
|
+
<select
|
|
863
|
+
className="sip-tx-history-select"
|
|
864
|
+
value={`${sort.field}-${sort.direction}`}
|
|
865
|
+
onChange={(e) => {
|
|
866
|
+
const [field, direction] = e.target.value.split('-') as [
|
|
867
|
+
TransactionHistorySort['field'],
|
|
868
|
+
TransactionHistorySort['direction']
|
|
869
|
+
]
|
|
870
|
+
onSortChange?.({ field, direction })
|
|
871
|
+
}}
|
|
872
|
+
>
|
|
873
|
+
<option value="timestamp-desc">Newest First</option>
|
|
874
|
+
<option value="timestamp-asc">Oldest First</option>
|
|
875
|
+
<option value="amount-desc">Amount (High to Low)</option>
|
|
876
|
+
<option value="amount-asc">Amount (Low to High)</option>
|
|
877
|
+
</select>
|
|
878
|
+
</div>
|
|
879
|
+
</div>
|
|
880
|
+
)}
|
|
881
|
+
|
|
882
|
+
{/* Error */}
|
|
883
|
+
{error && (
|
|
884
|
+
<div className="sip-tx-history-error" data-testid="error">
|
|
885
|
+
<AlertIcon />
|
|
886
|
+
{error}
|
|
887
|
+
</div>
|
|
888
|
+
)}
|
|
889
|
+
|
|
890
|
+
{/* Loading */}
|
|
891
|
+
{isLoading && transactions.length === 0 && (
|
|
892
|
+
<div className="sip-tx-history-loading" data-testid="loading">
|
|
893
|
+
<div className="sip-tx-history-loading-spinner" />
|
|
894
|
+
Loading transactions...
|
|
895
|
+
</div>
|
|
896
|
+
)}
|
|
897
|
+
|
|
898
|
+
{/* Empty state */}
|
|
899
|
+
{!isLoading && transactions.length === 0 && !error && (
|
|
900
|
+
<div className="sip-tx-history-empty" data-testid="empty">
|
|
901
|
+
<HistoryIcon />
|
|
902
|
+
<p>No transactions yet</p>
|
|
903
|
+
</div>
|
|
904
|
+
)}
|
|
905
|
+
|
|
906
|
+
{/* Transaction list */}
|
|
907
|
+
{paginatedTransactions.length > 0 && (
|
|
908
|
+
<ul className="sip-tx-history-list" data-testid="transaction-list">
|
|
909
|
+
{paginatedTransactions.map((tx) => (
|
|
910
|
+
<li
|
|
911
|
+
key={tx.hash}
|
|
912
|
+
className="sip-tx-history-item"
|
|
913
|
+
onClick={() => onTransactionSelect?.(tx)}
|
|
914
|
+
data-testid={`tx-item-${tx.hash.slice(0, 8)}`}
|
|
915
|
+
>
|
|
916
|
+
<div className="sip-tx-history-item-left">
|
|
917
|
+
<div
|
|
918
|
+
className="sip-tx-history-item-icon"
|
|
919
|
+
data-direction={tx.direction}
|
|
920
|
+
>
|
|
921
|
+
{tx.direction === 'sent' ? (
|
|
922
|
+
<ArrowUpIcon />
|
|
923
|
+
) : tx.direction === 'claimed' ? (
|
|
924
|
+
<CheckCircleIcon />
|
|
925
|
+
) : (
|
|
926
|
+
<ArrowDownIcon />
|
|
927
|
+
)}
|
|
928
|
+
</div>
|
|
929
|
+
<div className="sip-tx-history-item-details">
|
|
930
|
+
<div className="sip-tx-history-item-title">
|
|
931
|
+
{getDirectionLabel(tx.direction)}
|
|
932
|
+
{tx.isStealthAddress && (
|
|
933
|
+
<span className="sip-tx-history-item-stealth-badge">
|
|
934
|
+
<ShieldIcon />
|
|
935
|
+
Stealth
|
|
936
|
+
</span>
|
|
937
|
+
)}
|
|
938
|
+
</div>
|
|
939
|
+
<div className="sip-tx-history-item-address">
|
|
940
|
+
{tx.direction === 'sent' ? 'To: ' : 'From: '}
|
|
941
|
+
<a
|
|
942
|
+
href={`${networkConfig.explorerUrl}/address/${tx.direction === 'sent' ? tx.to : tx.from}`}
|
|
943
|
+
target="_blank"
|
|
944
|
+
rel="noopener noreferrer"
|
|
945
|
+
onClick={(e) => e.stopPropagation()}
|
|
946
|
+
style={{ color: 'inherit', textDecoration: 'none' }}
|
|
947
|
+
>
|
|
948
|
+
{formatAddress(tx.direction === 'sent' ? tx.to : tx.from)}
|
|
949
|
+
</a>
|
|
950
|
+
</div>
|
|
951
|
+
<div className="sip-tx-history-item-meta">
|
|
952
|
+
<span className="sip-tx-history-item-status" data-status={tx.status}>
|
|
953
|
+
{tx.status}
|
|
954
|
+
</span>
|
|
955
|
+
<span>{formatTimestamp(tx.timestamp)}</span>
|
|
956
|
+
<a
|
|
957
|
+
href={getExplorerUrl(tx.hash)}
|
|
958
|
+
target="_blank"
|
|
959
|
+
rel="noopener noreferrer"
|
|
960
|
+
onClick={(e) => e.stopPropagation()}
|
|
961
|
+
style={{ color: '#6366f1', textDecoration: 'none' }}
|
|
962
|
+
>
|
|
963
|
+
View on Explorer
|
|
964
|
+
</a>
|
|
965
|
+
</div>
|
|
966
|
+
</div>
|
|
967
|
+
</div>
|
|
968
|
+
<div className="sip-tx-history-item-right">
|
|
969
|
+
<span
|
|
970
|
+
className="sip-tx-history-item-amount"
|
|
971
|
+
data-direction={tx.direction}
|
|
972
|
+
>
|
|
973
|
+
{tx.direction === 'sent' ? '-' : '+'}
|
|
974
|
+
{formatAmount(tx.amount, tx.tokenDecimals, tx.tokenSymbol)}
|
|
975
|
+
</span>
|
|
976
|
+
{showUsdValues && tx.usdValue && (
|
|
977
|
+
<span className="sip-tx-history-item-usd">
|
|
978
|
+
${parseFloat(tx.usdValue).toLocaleString(undefined, {
|
|
979
|
+
minimumFractionDigits: 2,
|
|
980
|
+
maximumFractionDigits: 2,
|
|
981
|
+
})}
|
|
982
|
+
</span>
|
|
983
|
+
)}
|
|
984
|
+
</div>
|
|
985
|
+
</li>
|
|
986
|
+
))}
|
|
987
|
+
</ul>
|
|
988
|
+
)}
|
|
989
|
+
|
|
990
|
+
{/* Load more */}
|
|
991
|
+
{canLoadMore && !isLoading && (
|
|
992
|
+
<div className="sip-tx-history-load-more">
|
|
993
|
+
<button
|
|
994
|
+
type="button"
|
|
995
|
+
className="sip-tx-history-btn sip-tx-history-btn-secondary"
|
|
996
|
+
onClick={handleLoadMore}
|
|
997
|
+
>
|
|
998
|
+
Load More
|
|
999
|
+
</button>
|
|
1000
|
+
</div>
|
|
1001
|
+
)}
|
|
1002
|
+
</div>
|
|
1003
|
+
</>
|
|
1004
|
+
)
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Hook for managing transaction history
|
|
1009
|
+
*
|
|
1010
|
+
* @example
|
|
1011
|
+
* ```tsx
|
|
1012
|
+
* const {
|
|
1013
|
+
* transactions,
|
|
1014
|
+
* isLoading,
|
|
1015
|
+
* filter,
|
|
1016
|
+
* setFilter,
|
|
1017
|
+
* loadMore,
|
|
1018
|
+
* refresh,
|
|
1019
|
+
* exportHistory,
|
|
1020
|
+
* } = useTransactionHistory({
|
|
1021
|
+
* viewingPrivateKey,
|
|
1022
|
+
* spendingPublicKey,
|
|
1023
|
+
* network: 'mainnet',
|
|
1024
|
+
* })
|
|
1025
|
+
* ```
|
|
1026
|
+
*/
|
|
1027
|
+
export function useTransactionHistory(options: {
|
|
1028
|
+
initialTransactions?: PrivacyTransactionHistoryItem[]
|
|
1029
|
+
fetchTransactions?: () => Promise<PrivacyTransactionHistoryItem[]>
|
|
1030
|
+
pageSize?: number
|
|
1031
|
+
} = {}) {
|
|
1032
|
+
const {
|
|
1033
|
+
initialTransactions = [],
|
|
1034
|
+
fetchTransactions,
|
|
1035
|
+
pageSize = 20,
|
|
1036
|
+
} = options
|
|
1037
|
+
|
|
1038
|
+
const [transactions, setTransactions] = useState<PrivacyTransactionHistoryItem[]>(
|
|
1039
|
+
initialTransactions
|
|
1040
|
+
)
|
|
1041
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
1042
|
+
const [error, setError] = useState<string | null>(null)
|
|
1043
|
+
const [filter, setFilter] = useState<TransactionHistoryFilter>({ direction: 'all' })
|
|
1044
|
+
const [sort, setSort] = useState<TransactionHistorySort>({
|
|
1045
|
+
field: 'timestamp',
|
|
1046
|
+
direction: 'desc',
|
|
1047
|
+
})
|
|
1048
|
+
const [hasMore, setHasMore] = useState(true)
|
|
1049
|
+
|
|
1050
|
+
// Load transactions
|
|
1051
|
+
const loadTransactions = useCallback(async () => {
|
|
1052
|
+
if (!fetchTransactions) return
|
|
1053
|
+
|
|
1054
|
+
setIsLoading(true)
|
|
1055
|
+
setError(null)
|
|
1056
|
+
|
|
1057
|
+
try {
|
|
1058
|
+
const result = await fetchTransactions()
|
|
1059
|
+
setTransactions(result)
|
|
1060
|
+
setHasMore(result.length >= pageSize)
|
|
1061
|
+
} catch (err) {
|
|
1062
|
+
setError(err instanceof Error ? err.message : 'Failed to load transactions')
|
|
1063
|
+
} finally {
|
|
1064
|
+
setIsLoading(false)
|
|
1065
|
+
}
|
|
1066
|
+
}, [fetchTransactions, pageSize])
|
|
1067
|
+
|
|
1068
|
+
// Load on mount
|
|
1069
|
+
useEffect(() => {
|
|
1070
|
+
if (fetchTransactions && initialTransactions.length === 0) {
|
|
1071
|
+
loadTransactions()
|
|
1072
|
+
}
|
|
1073
|
+
}, [fetchTransactions, initialTransactions.length, loadTransactions])
|
|
1074
|
+
|
|
1075
|
+
// Add transaction
|
|
1076
|
+
const addTransaction = useCallback((tx: PrivacyTransactionHistoryItem) => {
|
|
1077
|
+
setTransactions((prev) => [tx, ...prev])
|
|
1078
|
+
}, [])
|
|
1079
|
+
|
|
1080
|
+
// Update transaction
|
|
1081
|
+
const updateTransaction = useCallback(
|
|
1082
|
+
(hash: string, updates: Partial<PrivacyTransactionHistoryItem>) => {
|
|
1083
|
+
setTransactions((prev) =>
|
|
1084
|
+
prev.map((tx) => (tx.hash === hash ? { ...tx, ...updates } : tx))
|
|
1085
|
+
)
|
|
1086
|
+
},
|
|
1087
|
+
[]
|
|
1088
|
+
)
|
|
1089
|
+
|
|
1090
|
+
// Export history
|
|
1091
|
+
const exportHistory = useCallback(
|
|
1092
|
+
(format: 'csv' | 'json') => {
|
|
1093
|
+
if (format === 'json') {
|
|
1094
|
+
const blob = new Blob([JSON.stringify(transactions, null, 2)], {
|
|
1095
|
+
type: 'application/json',
|
|
1096
|
+
})
|
|
1097
|
+
const url = URL.createObjectURL(blob)
|
|
1098
|
+
const a = document.createElement('a')
|
|
1099
|
+
a.href = url
|
|
1100
|
+
a.download = `sip-transactions-${Date.now()}.json`
|
|
1101
|
+
document.body.appendChild(a)
|
|
1102
|
+
a.click()
|
|
1103
|
+
document.body.removeChild(a)
|
|
1104
|
+
URL.revokeObjectURL(url)
|
|
1105
|
+
} else {
|
|
1106
|
+
// CSV export
|
|
1107
|
+
const headers = [
|
|
1108
|
+
'Hash',
|
|
1109
|
+
'Direction',
|
|
1110
|
+
'Type',
|
|
1111
|
+
'Timestamp',
|
|
1112
|
+
'From',
|
|
1113
|
+
'To',
|
|
1114
|
+
'Amount',
|
|
1115
|
+
'Token',
|
|
1116
|
+
'USD Value',
|
|
1117
|
+
'Status',
|
|
1118
|
+
'Stealth',
|
|
1119
|
+
]
|
|
1120
|
+
const rows = transactions.map((tx) => [
|
|
1121
|
+
tx.hash,
|
|
1122
|
+
tx.direction,
|
|
1123
|
+
tx.type,
|
|
1124
|
+
new Date(tx.timestamp).toISOString(),
|
|
1125
|
+
tx.from,
|
|
1126
|
+
tx.to,
|
|
1127
|
+
(parseFloat(tx.amount) / Math.pow(10, tx.tokenDecimals)).toString(),
|
|
1128
|
+
tx.tokenSymbol,
|
|
1129
|
+
tx.usdValue || '',
|
|
1130
|
+
tx.status,
|
|
1131
|
+
tx.isStealthAddress ? 'Yes' : 'No',
|
|
1132
|
+
])
|
|
1133
|
+
|
|
1134
|
+
const csv = [headers, ...rows].map((row) => row.join(',')).join('\n')
|
|
1135
|
+
const blob = new Blob([csv], { type: 'text/csv' })
|
|
1136
|
+
const url = URL.createObjectURL(blob)
|
|
1137
|
+
const a = document.createElement('a')
|
|
1138
|
+
a.href = url
|
|
1139
|
+
a.download = `sip-transactions-${Date.now()}.csv`
|
|
1140
|
+
document.body.appendChild(a)
|
|
1141
|
+
a.click()
|
|
1142
|
+
document.body.removeChild(a)
|
|
1143
|
+
URL.revokeObjectURL(url)
|
|
1144
|
+
}
|
|
1145
|
+
},
|
|
1146
|
+
[transactions]
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
// Get summary statistics
|
|
1150
|
+
const summary = useMemo(() => {
|
|
1151
|
+
const sent = transactions.filter((tx) => tx.direction === 'sent')
|
|
1152
|
+
const received = transactions.filter((tx) => tx.direction === 'received')
|
|
1153
|
+
const claimed = transactions.filter((tx) => tx.direction === 'claimed')
|
|
1154
|
+
const stealth = transactions.filter((tx) => tx.isStealthAddress)
|
|
1155
|
+
|
|
1156
|
+
return {
|
|
1157
|
+
total: transactions.length,
|
|
1158
|
+
sent: sent.length,
|
|
1159
|
+
received: received.length,
|
|
1160
|
+
claimed: claimed.length,
|
|
1161
|
+
stealthCount: stealth.length,
|
|
1162
|
+
stealthPercentage: transactions.length > 0
|
|
1163
|
+
? Math.round((stealth.length / transactions.length) * 100)
|
|
1164
|
+
: 0,
|
|
1165
|
+
}
|
|
1166
|
+
}, [transactions])
|
|
1167
|
+
|
|
1168
|
+
return {
|
|
1169
|
+
transactions,
|
|
1170
|
+
setTransactions,
|
|
1171
|
+
isLoading,
|
|
1172
|
+
error,
|
|
1173
|
+
filter,
|
|
1174
|
+
setFilter,
|
|
1175
|
+
sort,
|
|
1176
|
+
setSort,
|
|
1177
|
+
hasMore,
|
|
1178
|
+
setHasMore,
|
|
1179
|
+
loadTransactions,
|
|
1180
|
+
addTransaction,
|
|
1181
|
+
updateTransaction,
|
|
1182
|
+
exportHistory,
|
|
1183
|
+
summary,
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
export default TransactionHistory
|