@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,651 @@
|
|
|
1
|
+
import React, { useState, useCallback, useMemo } from 'react'
|
|
2
|
+
import type { HexString } from '@sip-protocol/types'
|
|
3
|
+
import { useTransactionHistory } from '../hooks/use-transaction-history'
|
|
4
|
+
import type { TransactionSummary } from '../hooks/use-transaction-history'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Transaction type for NEAR privacy operations
|
|
8
|
+
*/
|
|
9
|
+
type NEARTransactionType = 'send' | 'receive' | 'contract_call'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Export format options
|
|
13
|
+
*/
|
|
14
|
+
type NEARExportFormat = 'csv' | 'json'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Historical transaction structure
|
|
18
|
+
*/
|
|
19
|
+
interface NEARHistoricalTransaction {
|
|
20
|
+
hash: string
|
|
21
|
+
timestamp: number
|
|
22
|
+
blockHeight: number
|
|
23
|
+
type: NEARTransactionType
|
|
24
|
+
stealthAddress: string
|
|
25
|
+
stealthPublicKey: HexString
|
|
26
|
+
ephemeralPublicKey: HexString
|
|
27
|
+
viewTag: number
|
|
28
|
+
amount: string
|
|
29
|
+
amountFormatted: string
|
|
30
|
+
token: string
|
|
31
|
+
tokenContract: string | null
|
|
32
|
+
decimals: number
|
|
33
|
+
privacyLevel: 'transparent' | 'shielded' | 'compliant'
|
|
34
|
+
amountRevealed: boolean
|
|
35
|
+
sender?: string
|
|
36
|
+
receiver?: string
|
|
37
|
+
fee?: string
|
|
38
|
+
explorerUrl: string
|
|
39
|
+
recipientLabel?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Transaction history view component props
|
|
44
|
+
*/
|
|
45
|
+
export interface TransactionHistoryViewProps {
|
|
46
|
+
/** NEAR RPC URL */
|
|
47
|
+
rpcUrl: string
|
|
48
|
+
/** Viewing private key (hex) */
|
|
49
|
+
viewingPrivateKey: HexString
|
|
50
|
+
/** Spending private key (hex) */
|
|
51
|
+
spendingPrivateKey: HexString
|
|
52
|
+
/** Network type */
|
|
53
|
+
network?: 'mainnet' | 'testnet'
|
|
54
|
+
/** Number of transactions per page */
|
|
55
|
+
pageSize?: number
|
|
56
|
+
/** Auto-refresh interval in milliseconds (0 = disabled) */
|
|
57
|
+
refreshInterval?: number
|
|
58
|
+
/** Whether to show filter controls */
|
|
59
|
+
showFilters?: boolean
|
|
60
|
+
/** Whether to show export button */
|
|
61
|
+
showExport?: boolean
|
|
62
|
+
/** Whether to show summary statistics */
|
|
63
|
+
showSummary?: boolean
|
|
64
|
+
/** Whether to show search */
|
|
65
|
+
showSearch?: boolean
|
|
66
|
+
/** Callback when transaction is clicked */
|
|
67
|
+
onTransactionClick?: (tx: NEARHistoricalTransaction) => void
|
|
68
|
+
/** Callback when export is triggered */
|
|
69
|
+
onExport?: (format: NEARExportFormat, data: string) => void
|
|
70
|
+
/** Custom class name */
|
|
71
|
+
className?: string
|
|
72
|
+
/** Theme */
|
|
73
|
+
theme?: 'light' | 'dark'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Default styles for the component
|
|
78
|
+
*/
|
|
79
|
+
const styles = {
|
|
80
|
+
container: {
|
|
81
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
82
|
+
borderRadius: '8px',
|
|
83
|
+
overflow: 'hidden',
|
|
84
|
+
},
|
|
85
|
+
header: {
|
|
86
|
+
padding: '16px',
|
|
87
|
+
borderBottom: '1px solid',
|
|
88
|
+
display: 'flex',
|
|
89
|
+
justifyContent: 'space-between',
|
|
90
|
+
alignItems: 'center',
|
|
91
|
+
},
|
|
92
|
+
title: {
|
|
93
|
+
fontSize: '18px',
|
|
94
|
+
fontWeight: 600,
|
|
95
|
+
margin: 0,
|
|
96
|
+
},
|
|
97
|
+
controls: {
|
|
98
|
+
display: 'flex',
|
|
99
|
+
gap: '8px',
|
|
100
|
+
alignItems: 'center',
|
|
101
|
+
},
|
|
102
|
+
filterBar: {
|
|
103
|
+
padding: '12px 16px',
|
|
104
|
+
borderBottom: '1px solid',
|
|
105
|
+
display: 'flex',
|
|
106
|
+
gap: '12px',
|
|
107
|
+
flexWrap: 'wrap' as const,
|
|
108
|
+
alignItems: 'center',
|
|
109
|
+
},
|
|
110
|
+
searchInput: {
|
|
111
|
+
padding: '8px 12px',
|
|
112
|
+
borderRadius: '6px',
|
|
113
|
+
border: '1px solid',
|
|
114
|
+
fontSize: '14px',
|
|
115
|
+
minWidth: '200px',
|
|
116
|
+
},
|
|
117
|
+
filterSelect: {
|
|
118
|
+
padding: '8px 12px',
|
|
119
|
+
borderRadius: '6px',
|
|
120
|
+
border: '1px solid',
|
|
121
|
+
fontSize: '14px',
|
|
122
|
+
cursor: 'pointer',
|
|
123
|
+
},
|
|
124
|
+
button: {
|
|
125
|
+
padding: '8px 16px',
|
|
126
|
+
borderRadius: '6px',
|
|
127
|
+
border: 'none',
|
|
128
|
+
fontSize: '14px',
|
|
129
|
+
fontWeight: 500,
|
|
130
|
+
cursor: 'pointer',
|
|
131
|
+
display: 'flex',
|
|
132
|
+
alignItems: 'center',
|
|
133
|
+
gap: '6px',
|
|
134
|
+
},
|
|
135
|
+
primaryButton: {
|
|
136
|
+
backgroundColor: '#3b82f6',
|
|
137
|
+
color: 'white',
|
|
138
|
+
},
|
|
139
|
+
secondaryButton: {
|
|
140
|
+
backgroundColor: 'transparent',
|
|
141
|
+
border: '1px solid',
|
|
142
|
+
},
|
|
143
|
+
list: {
|
|
144
|
+
maxHeight: '500px',
|
|
145
|
+
overflowY: 'auto' as const,
|
|
146
|
+
},
|
|
147
|
+
listItem: {
|
|
148
|
+
padding: '16px',
|
|
149
|
+
borderBottom: '1px solid',
|
|
150
|
+
cursor: 'pointer',
|
|
151
|
+
transition: 'background-color 0.15s',
|
|
152
|
+
},
|
|
153
|
+
listItemHover: {
|
|
154
|
+
backgroundColor: 'rgba(0,0,0,0.02)',
|
|
155
|
+
},
|
|
156
|
+
txRow: {
|
|
157
|
+
display: 'flex',
|
|
158
|
+
justifyContent: 'space-between',
|
|
159
|
+
alignItems: 'flex-start',
|
|
160
|
+
},
|
|
161
|
+
txLeft: {
|
|
162
|
+
display: 'flex',
|
|
163
|
+
flexDirection: 'column' as const,
|
|
164
|
+
gap: '4px',
|
|
165
|
+
},
|
|
166
|
+
txRight: {
|
|
167
|
+
display: 'flex',
|
|
168
|
+
flexDirection: 'column' as const,
|
|
169
|
+
alignItems: 'flex-end' as const,
|
|
170
|
+
gap: '4px',
|
|
171
|
+
},
|
|
172
|
+
txType: {
|
|
173
|
+
fontSize: '12px',
|
|
174
|
+
fontWeight: 500,
|
|
175
|
+
padding: '2px 8px',
|
|
176
|
+
borderRadius: '4px',
|
|
177
|
+
textTransform: 'uppercase' as const,
|
|
178
|
+
},
|
|
179
|
+
txTypeReceive: {
|
|
180
|
+
backgroundColor: '#dcfce7',
|
|
181
|
+
color: '#166534',
|
|
182
|
+
},
|
|
183
|
+
txTypeSend: {
|
|
184
|
+
backgroundColor: '#fee2e2',
|
|
185
|
+
color: '#991b1b',
|
|
186
|
+
},
|
|
187
|
+
txTypeCall: {
|
|
188
|
+
backgroundColor: '#e0e7ff',
|
|
189
|
+
color: '#3730a3',
|
|
190
|
+
},
|
|
191
|
+
txAmount: {
|
|
192
|
+
fontSize: '16px',
|
|
193
|
+
fontWeight: 600,
|
|
194
|
+
},
|
|
195
|
+
txToken: {
|
|
196
|
+
fontSize: '14px',
|
|
197
|
+
opacity: 0.7,
|
|
198
|
+
},
|
|
199
|
+
txHash: {
|
|
200
|
+
fontSize: '12px',
|
|
201
|
+
fontFamily: 'monospace',
|
|
202
|
+
opacity: 0.6,
|
|
203
|
+
},
|
|
204
|
+
txTime: {
|
|
205
|
+
fontSize: '12px',
|
|
206
|
+
opacity: 0.6,
|
|
207
|
+
},
|
|
208
|
+
privacyBadge: {
|
|
209
|
+
fontSize: '10px',
|
|
210
|
+
padding: '2px 6px',
|
|
211
|
+
borderRadius: '3px',
|
|
212
|
+
textTransform: 'uppercase' as const,
|
|
213
|
+
},
|
|
214
|
+
privacyShielded: {
|
|
215
|
+
backgroundColor: '#fef3c7',
|
|
216
|
+
color: '#92400e',
|
|
217
|
+
},
|
|
218
|
+
privacyCompliant: {
|
|
219
|
+
backgroundColor: '#dbeafe',
|
|
220
|
+
color: '#1e40af',
|
|
221
|
+
},
|
|
222
|
+
summary: {
|
|
223
|
+
padding: '16px',
|
|
224
|
+
borderBottom: '1px solid',
|
|
225
|
+
display: 'grid',
|
|
226
|
+
gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
|
|
227
|
+
gap: '16px',
|
|
228
|
+
},
|
|
229
|
+
summaryItem: {
|
|
230
|
+
display: 'flex',
|
|
231
|
+
flexDirection: 'column' as const,
|
|
232
|
+
gap: '4px',
|
|
233
|
+
},
|
|
234
|
+
summaryLabel: {
|
|
235
|
+
fontSize: '12px',
|
|
236
|
+
opacity: 0.6,
|
|
237
|
+
textTransform: 'uppercase' as const,
|
|
238
|
+
},
|
|
239
|
+
summaryValue: {
|
|
240
|
+
fontSize: '20px',
|
|
241
|
+
fontWeight: 600,
|
|
242
|
+
},
|
|
243
|
+
loading: {
|
|
244
|
+
padding: '40px',
|
|
245
|
+
textAlign: 'center' as const,
|
|
246
|
+
opacity: 0.6,
|
|
247
|
+
},
|
|
248
|
+
error: {
|
|
249
|
+
padding: '16px',
|
|
250
|
+
backgroundColor: '#fee2e2',
|
|
251
|
+
color: '#991b1b',
|
|
252
|
+
borderRadius: '6px',
|
|
253
|
+
margin: '16px',
|
|
254
|
+
},
|
|
255
|
+
empty: {
|
|
256
|
+
padding: '40px',
|
|
257
|
+
textAlign: 'center' as const,
|
|
258
|
+
opacity: 0.6,
|
|
259
|
+
},
|
|
260
|
+
footer: {
|
|
261
|
+
padding: '12px 16px',
|
|
262
|
+
borderTop: '1px solid',
|
|
263
|
+
display: 'flex',
|
|
264
|
+
justifyContent: 'space-between',
|
|
265
|
+
alignItems: 'center',
|
|
266
|
+
},
|
|
267
|
+
pagination: {
|
|
268
|
+
fontSize: '14px',
|
|
269
|
+
opacity: 0.7,
|
|
270
|
+
},
|
|
271
|
+
light: {
|
|
272
|
+
backgroundColor: '#ffffff',
|
|
273
|
+
color: '#1f2937',
|
|
274
|
+
borderColor: '#e5e7eb',
|
|
275
|
+
},
|
|
276
|
+
dark: {
|
|
277
|
+
backgroundColor: '#1f2937',
|
|
278
|
+
color: '#f9fafb',
|
|
279
|
+
borderColor: '#374151',
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Format timestamp to readable date
|
|
285
|
+
*/
|
|
286
|
+
function formatDate(timestamp: number): string {
|
|
287
|
+
return new Date(timestamp).toLocaleDateString(undefined, {
|
|
288
|
+
year: 'numeric',
|
|
289
|
+
month: 'short',
|
|
290
|
+
day: 'numeric',
|
|
291
|
+
hour: '2-digit',
|
|
292
|
+
minute: '2-digit',
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Truncate hash for display
|
|
298
|
+
*/
|
|
299
|
+
function truncateHash(hash: string): string {
|
|
300
|
+
if (hash.length <= 16) return hash
|
|
301
|
+
return `${hash.slice(0, 8)}...${hash.slice(-6)}`
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Transaction list item component
|
|
306
|
+
*/
|
|
307
|
+
interface TransactionItemProps {
|
|
308
|
+
transaction: NEARHistoricalTransaction
|
|
309
|
+
onClick?: (tx: NEARHistoricalTransaction) => void
|
|
310
|
+
theme: 'light' | 'dark'
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function TransactionItem({ transaction, onClick, theme }: TransactionItemProps) {
|
|
314
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
315
|
+
const themeStyles = theme === 'dark' ? styles.dark : styles.light
|
|
316
|
+
|
|
317
|
+
const typeStyleMap: Record<NEARTransactionType, React.CSSProperties> = {
|
|
318
|
+
receive: styles.txTypeReceive,
|
|
319
|
+
send: styles.txTypeSend,
|
|
320
|
+
contract_call: styles.txTypeCall,
|
|
321
|
+
}
|
|
322
|
+
const typeStyle = typeStyleMap[transaction.type]
|
|
323
|
+
|
|
324
|
+
const privacyStyle = transaction.privacyLevel === 'compliant'
|
|
325
|
+
? styles.privacyCompliant
|
|
326
|
+
: styles.privacyShielded
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<div
|
|
330
|
+
style={{
|
|
331
|
+
...styles.listItem,
|
|
332
|
+
borderColor: themeStyles.borderColor,
|
|
333
|
+
...(isHovered ? styles.listItemHover : {}),
|
|
334
|
+
}}
|
|
335
|
+
onClick={() => onClick?.(transaction)}
|
|
336
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
337
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
338
|
+
>
|
|
339
|
+
<div style={styles.txRow}>
|
|
340
|
+
<div style={styles.txLeft}>
|
|
341
|
+
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
|
342
|
+
<span style={{ ...styles.txType, ...typeStyle }}>
|
|
343
|
+
{transaction.type}
|
|
344
|
+
</span>
|
|
345
|
+
<span style={{ ...styles.privacyBadge, ...privacyStyle }}>
|
|
346
|
+
{transaction.privacyLevel}
|
|
347
|
+
</span>
|
|
348
|
+
</div>
|
|
349
|
+
<span style={styles.txHash} title={transaction.hash}>
|
|
350
|
+
{truncateHash(transaction.hash)}
|
|
351
|
+
</span>
|
|
352
|
+
<span style={styles.txTime}>
|
|
353
|
+
{formatDate(transaction.timestamp)}
|
|
354
|
+
</span>
|
|
355
|
+
</div>
|
|
356
|
+
<div style={styles.txRight}>
|
|
357
|
+
<span style={styles.txAmount}>
|
|
358
|
+
{transaction.type === 'send' ? '-' : '+'}{transaction.amountFormatted}
|
|
359
|
+
</span>
|
|
360
|
+
<span style={styles.txToken}>{transaction.token}</span>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Summary statistics component
|
|
369
|
+
*/
|
|
370
|
+
interface SummaryViewProps {
|
|
371
|
+
summary: TransactionSummary
|
|
372
|
+
theme: 'light' | 'dark'
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function SummaryView({ summary, theme }: SummaryViewProps) {
|
|
376
|
+
const themeStyles = theme === 'dark' ? styles.dark : styles.light
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
<div style={{ ...styles.summary, borderColor: themeStyles.borderColor }}>
|
|
380
|
+
<div style={styles.summaryItem}>
|
|
381
|
+
<span style={styles.summaryLabel}>Transactions</span>
|
|
382
|
+
<span style={styles.summaryValue}>{summary.transactionCount}</span>
|
|
383
|
+
</div>
|
|
384
|
+
<div style={styles.summaryItem}>
|
|
385
|
+
<span style={styles.summaryLabel}>Total Received</span>
|
|
386
|
+
<span style={styles.summaryValue}>
|
|
387
|
+
{Object.entries(summary.totalReceived).map(([token, amount]) => (
|
|
388
|
+
<span key={token}>{amount.toString()} {token}</span>
|
|
389
|
+
))}
|
|
390
|
+
{Object.keys(summary.totalReceived).length === 0 && '0'}
|
|
391
|
+
</span>
|
|
392
|
+
</div>
|
|
393
|
+
<div style={styles.summaryItem}>
|
|
394
|
+
<span style={styles.summaryLabel}>Addresses</span>
|
|
395
|
+
<span style={styles.summaryValue}>{summary.uniqueAddresses}</span>
|
|
396
|
+
</div>
|
|
397
|
+
{summary.dateRange && (
|
|
398
|
+
<div style={styles.summaryItem}>
|
|
399
|
+
<span style={styles.summaryLabel}>Date Range</span>
|
|
400
|
+
<span style={{ fontSize: '14px' }}>
|
|
401
|
+
{formatDate(summary.dateRange.from).split(',')[0]} - {formatDate(summary.dateRange.to).split(',')[0]}
|
|
402
|
+
</span>
|
|
403
|
+
</div>
|
|
404
|
+
)}
|
|
405
|
+
</div>
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* TransactionHistoryView - Display NEAR privacy transaction history
|
|
411
|
+
*
|
|
412
|
+
* A comprehensive component for viewing, filtering, and exporting
|
|
413
|
+
* NEAR privacy transaction history.
|
|
414
|
+
*
|
|
415
|
+
* @example Basic usage
|
|
416
|
+
* ```tsx
|
|
417
|
+
* <TransactionHistoryView
|
|
418
|
+
* rpcUrl="https://rpc.mainnet.near.org"
|
|
419
|
+
* viewingPrivateKey="0x..."
|
|
420
|
+
* spendingPrivateKey="0x..."
|
|
421
|
+
* />
|
|
422
|
+
* ```
|
|
423
|
+
*
|
|
424
|
+
* @example With callbacks
|
|
425
|
+
* ```tsx
|
|
426
|
+
* <TransactionHistoryView
|
|
427
|
+
* rpcUrl="https://rpc.mainnet.near.org"
|
|
428
|
+
* viewingPrivateKey="0x..."
|
|
429
|
+
* spendingPrivateKey="0x..."
|
|
430
|
+
* onTransactionClick={(tx) => openInExplorer(tx.explorerUrl)}
|
|
431
|
+
* onExport={(format, data) => downloadFile(`transactions.${format}`, data)}
|
|
432
|
+
* showFilters
|
|
433
|
+
* showExport
|
|
434
|
+
* showSummary
|
|
435
|
+
* />
|
|
436
|
+
* ```
|
|
437
|
+
*/
|
|
438
|
+
export function TransactionHistoryView({
|
|
439
|
+
rpcUrl,
|
|
440
|
+
viewingPrivateKey,
|
|
441
|
+
spendingPrivateKey,
|
|
442
|
+
network = 'mainnet',
|
|
443
|
+
pageSize = 20,
|
|
444
|
+
refreshInterval = 0,
|
|
445
|
+
showFilters = true,
|
|
446
|
+
showExport = true,
|
|
447
|
+
showSummary = true,
|
|
448
|
+
showSearch = true,
|
|
449
|
+
onTransactionClick,
|
|
450
|
+
onExport,
|
|
451
|
+
className,
|
|
452
|
+
theme = 'light',
|
|
453
|
+
}: TransactionHistoryViewProps) {
|
|
454
|
+
const {
|
|
455
|
+
status,
|
|
456
|
+
isLoading,
|
|
457
|
+
isRefreshing,
|
|
458
|
+
error,
|
|
459
|
+
transactions,
|
|
460
|
+
hasMore,
|
|
461
|
+
totalCount,
|
|
462
|
+
lastRefreshedAt,
|
|
463
|
+
summary,
|
|
464
|
+
filters,
|
|
465
|
+
refresh,
|
|
466
|
+
loadMore,
|
|
467
|
+
setFilters,
|
|
468
|
+
clearFilters,
|
|
469
|
+
exportData,
|
|
470
|
+
search,
|
|
471
|
+
clearError,
|
|
472
|
+
} = useTransactionHistory({
|
|
473
|
+
rpcUrl,
|
|
474
|
+
viewingPrivateKey,
|
|
475
|
+
spendingPrivateKey,
|
|
476
|
+
network,
|
|
477
|
+
pageSize,
|
|
478
|
+
refreshInterval,
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
482
|
+
const [exportFormat, setExportFormat] = useState<NEARExportFormat>('csv')
|
|
483
|
+
|
|
484
|
+
const themeStyles = theme === 'dark' ? styles.dark : styles.light
|
|
485
|
+
|
|
486
|
+
const handleSearch = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
487
|
+
const query = e.target.value
|
|
488
|
+
setSearchQuery(query)
|
|
489
|
+
// Debounce search
|
|
490
|
+
const timeoutId = setTimeout(() => {
|
|
491
|
+
search(query)
|
|
492
|
+
}, 300)
|
|
493
|
+
return () => clearTimeout(timeoutId)
|
|
494
|
+
}, [search])
|
|
495
|
+
|
|
496
|
+
const handleTypeFilter = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
497
|
+
const value = e.target.value
|
|
498
|
+
const typeFilter: NEARTransactionType[] | undefined = value ? [value as NEARTransactionType] : undefined
|
|
499
|
+
setFilters({ ...filters, typeFilter })
|
|
500
|
+
}, [filters, setFilters])
|
|
501
|
+
|
|
502
|
+
const handleExport = useCallback(() => {
|
|
503
|
+
const data = exportData(exportFormat, { prettyPrint: exportFormat === 'json' })
|
|
504
|
+
onExport?.(exportFormat, data)
|
|
505
|
+
}, [exportData, exportFormat, onExport])
|
|
506
|
+
|
|
507
|
+
const containerStyle = useMemo(() => ({
|
|
508
|
+
...styles.container,
|
|
509
|
+
...themeStyles,
|
|
510
|
+
border: `1px solid ${themeStyles.borderColor}`,
|
|
511
|
+
}), [themeStyles])
|
|
512
|
+
|
|
513
|
+
return (
|
|
514
|
+
<div style={containerStyle} className={className}>
|
|
515
|
+
{/* Header */}
|
|
516
|
+
<div style={{ ...styles.header, borderColor: themeStyles.borderColor }}>
|
|
517
|
+
<h2 style={styles.title}>Transaction History</h2>
|
|
518
|
+
<div style={styles.controls}>
|
|
519
|
+
{lastRefreshedAt && (
|
|
520
|
+
<span style={{ fontSize: '12px', opacity: 0.6 }}>
|
|
521
|
+
Last updated: {formatDate(lastRefreshedAt.getTime())}
|
|
522
|
+
</span>
|
|
523
|
+
)}
|
|
524
|
+
<button
|
|
525
|
+
style={{ ...styles.button, ...styles.secondaryButton, borderColor: themeStyles.borderColor }}
|
|
526
|
+
onClick={refresh}
|
|
527
|
+
disabled={isLoading || isRefreshing}
|
|
528
|
+
>
|
|
529
|
+
{isRefreshing ? 'Refreshing...' : 'Refresh'}
|
|
530
|
+
</button>
|
|
531
|
+
</div>
|
|
532
|
+
</div>
|
|
533
|
+
|
|
534
|
+
{/* Filter Bar */}
|
|
535
|
+
{showFilters && (
|
|
536
|
+
<div style={{ ...styles.filterBar, borderColor: themeStyles.borderColor }}>
|
|
537
|
+
{showSearch && (
|
|
538
|
+
<input
|
|
539
|
+
type="text"
|
|
540
|
+
placeholder="Search by hash or address..."
|
|
541
|
+
value={searchQuery}
|
|
542
|
+
onChange={handleSearch}
|
|
543
|
+
style={{ ...styles.searchInput, borderColor: themeStyles.borderColor, backgroundColor: themeStyles.backgroundColor }}
|
|
544
|
+
/>
|
|
545
|
+
)}
|
|
546
|
+
<select
|
|
547
|
+
value={filters.typeFilter?.[0] || ''}
|
|
548
|
+
onChange={handleTypeFilter}
|
|
549
|
+
style={{ ...styles.filterSelect, borderColor: themeStyles.borderColor, backgroundColor: themeStyles.backgroundColor }}
|
|
550
|
+
>
|
|
551
|
+
<option value="">All Types</option>
|
|
552
|
+
<option value="receive">Receive</option>
|
|
553
|
+
<option value="send">Send</option>
|
|
554
|
+
<option value="contract_call">Contract Call</option>
|
|
555
|
+
</select>
|
|
556
|
+
{(filters.typeFilter || filters.searchQuery) && (
|
|
557
|
+
<button
|
|
558
|
+
style={{ ...styles.button, ...styles.secondaryButton, borderColor: themeStyles.borderColor }}
|
|
559
|
+
onClick={() => { clearFilters(); setSearchQuery('') }}
|
|
560
|
+
>
|
|
561
|
+
Clear Filters
|
|
562
|
+
</button>
|
|
563
|
+
)}
|
|
564
|
+
{showExport && transactions.length > 0 && (
|
|
565
|
+
<>
|
|
566
|
+
<select
|
|
567
|
+
value={exportFormat}
|
|
568
|
+
onChange={(e) => setExportFormat(e.target.value as NEARExportFormat)}
|
|
569
|
+
style={{ ...styles.filterSelect, borderColor: themeStyles.borderColor, backgroundColor: themeStyles.backgroundColor }}
|
|
570
|
+
>
|
|
571
|
+
<option value="csv">CSV</option>
|
|
572
|
+
<option value="json">JSON</option>
|
|
573
|
+
</select>
|
|
574
|
+
<button
|
|
575
|
+
style={{ ...styles.button, ...styles.primaryButton }}
|
|
576
|
+
onClick={handleExport}
|
|
577
|
+
>
|
|
578
|
+
Export
|
|
579
|
+
</button>
|
|
580
|
+
</>
|
|
581
|
+
)}
|
|
582
|
+
</div>
|
|
583
|
+
)}
|
|
584
|
+
|
|
585
|
+
{/* Summary */}
|
|
586
|
+
{showSummary && summary && summary.transactionCount > 0 && (
|
|
587
|
+
<SummaryView summary={summary} theme={theme} />
|
|
588
|
+
)}
|
|
589
|
+
|
|
590
|
+
{/* Error */}
|
|
591
|
+
{error && (
|
|
592
|
+
<div style={styles.error}>
|
|
593
|
+
<strong>Error:</strong> {error.message}
|
|
594
|
+
<button
|
|
595
|
+
style={{ marginLeft: '8px', textDecoration: 'underline', cursor: 'pointer', border: 'none', background: 'none', color: 'inherit' }}
|
|
596
|
+
onClick={clearError}
|
|
597
|
+
>
|
|
598
|
+
Dismiss
|
|
599
|
+
</button>
|
|
600
|
+
</div>
|
|
601
|
+
)}
|
|
602
|
+
|
|
603
|
+
{/* Loading */}
|
|
604
|
+
{isLoading && status !== 'refreshing' && (
|
|
605
|
+
<div style={styles.loading}>Loading transactions...</div>
|
|
606
|
+
)}
|
|
607
|
+
|
|
608
|
+
{/* Empty State */}
|
|
609
|
+
{!isLoading && transactions.length === 0 && !error && (
|
|
610
|
+
<div style={styles.empty}>
|
|
611
|
+
No transactions found.
|
|
612
|
+
{filters.searchQuery || filters.typeFilter ? ' Try adjusting your filters.' : ''}
|
|
613
|
+
</div>
|
|
614
|
+
)}
|
|
615
|
+
|
|
616
|
+
{/* Transaction List */}
|
|
617
|
+
{transactions.length > 0 && (
|
|
618
|
+
<div style={styles.list}>
|
|
619
|
+
{transactions.map(tx => (
|
|
620
|
+
<TransactionItem
|
|
621
|
+
key={tx.hash}
|
|
622
|
+
transaction={tx}
|
|
623
|
+
onClick={onTransactionClick}
|
|
624
|
+
theme={theme}
|
|
625
|
+
/>
|
|
626
|
+
))}
|
|
627
|
+
</div>
|
|
628
|
+
)}
|
|
629
|
+
|
|
630
|
+
{/* Footer */}
|
|
631
|
+
{transactions.length > 0 && (
|
|
632
|
+
<div style={{ ...styles.footer, borderColor: themeStyles.borderColor }}>
|
|
633
|
+
<span style={styles.pagination}>
|
|
634
|
+
Showing {transactions.length} of {totalCount} transactions
|
|
635
|
+
</span>
|
|
636
|
+
{hasMore && (
|
|
637
|
+
<button
|
|
638
|
+
style={{ ...styles.button, ...styles.primaryButton }}
|
|
639
|
+
onClick={loadMore}
|
|
640
|
+
disabled={isLoading}
|
|
641
|
+
>
|
|
642
|
+
{isLoading ? 'Loading...' : 'Load More'}
|
|
643
|
+
</button>
|
|
644
|
+
)}
|
|
645
|
+
</div>
|
|
646
|
+
)}
|
|
647
|
+
</div>
|
|
648
|
+
)
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export default TransactionHistoryView
|