@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,770 @@
|
|
|
1
|
+
import React, { useState, useCallback, useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ownership status for stealth addresses
|
|
5
|
+
*/
|
|
6
|
+
export type OwnershipStatus = 'yours' | 'others' | 'unknown'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Network configuration for explorer links
|
|
10
|
+
*/
|
|
11
|
+
export interface NetworkConfig {
|
|
12
|
+
name: string
|
|
13
|
+
explorerUrl: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Default NEAR network configurations
|
|
18
|
+
*/
|
|
19
|
+
export const NEAR_NETWORKS: Record<string, NetworkConfig> = {
|
|
20
|
+
mainnet: {
|
|
21
|
+
name: 'NEAR Mainnet',
|
|
22
|
+
explorerUrl: 'https://nearblocks.io/address',
|
|
23
|
+
},
|
|
24
|
+
testnet: {
|
|
25
|
+
name: 'NEAR Testnet',
|
|
26
|
+
explorerUrl: 'https://testnet.nearblocks.io/address',
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* StealthAddressDisplay component props
|
|
32
|
+
*/
|
|
33
|
+
export interface StealthAddressDisplayProps {
|
|
34
|
+
/** The stealth address to display (64-char hex or implicit account) */
|
|
35
|
+
address: string
|
|
36
|
+
/** Optional stealth meta-address for QR code (full sip: format) */
|
|
37
|
+
metaAddress?: string
|
|
38
|
+
/** Ownership status of the address */
|
|
39
|
+
ownership?: OwnershipStatus
|
|
40
|
+
/** Whether the address is validated */
|
|
41
|
+
isValid?: boolean
|
|
42
|
+
/** Network for explorer links */
|
|
43
|
+
network?: 'mainnet' | 'testnet'
|
|
44
|
+
/** Custom network configuration */
|
|
45
|
+
networkConfig?: NetworkConfig
|
|
46
|
+
/** Whether to show the QR code button */
|
|
47
|
+
showQrCode?: boolean
|
|
48
|
+
/** Whether to show the explorer link */
|
|
49
|
+
showExplorerLink?: boolean
|
|
50
|
+
/** Whether to show the copy button */
|
|
51
|
+
showCopyButton?: boolean
|
|
52
|
+
/** Whether to show the ownership badge */
|
|
53
|
+
showOwnership?: boolean
|
|
54
|
+
/** Whether to show the validation indicator */
|
|
55
|
+
showValidation?: boolean
|
|
56
|
+
/** Custom class name */
|
|
57
|
+
className?: string
|
|
58
|
+
/** Size variant */
|
|
59
|
+
size?: 'sm' | 'md' | 'lg'
|
|
60
|
+
/** Callback when address is copied */
|
|
61
|
+
onCopy?: (address: string) => void
|
|
62
|
+
/** Callback when QR code is shown */
|
|
63
|
+
onShowQr?: (address: string) => void
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Validates a NEAR stealth address format
|
|
68
|
+
*/
|
|
69
|
+
export function isValidStealthAddress(address: string): boolean {
|
|
70
|
+
// Stealth addresses are 64-char hex strings (implicit accounts)
|
|
71
|
+
const hexRegex = /^[0-9a-fA-F]{64}$/
|
|
72
|
+
return hexRegex.test(address)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Truncates an address for display
|
|
77
|
+
*/
|
|
78
|
+
export function truncateAddress(address: string, startChars = 8, endChars = 8): string {
|
|
79
|
+
if (address.length <= startChars + endChars + 3) {
|
|
80
|
+
return address
|
|
81
|
+
}
|
|
82
|
+
return `${address.slice(0, startChars)}...${address.slice(-endChars)}`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* CSS styles for the component
|
|
87
|
+
*/
|
|
88
|
+
const styles = `
|
|
89
|
+
.sip-stealth-address {
|
|
90
|
+
display: inline-flex;
|
|
91
|
+
flex-direction: column;
|
|
92
|
+
gap: 8px;
|
|
93
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.sip-stealth-address-container {
|
|
97
|
+
display: inline-flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
gap: 8px;
|
|
100
|
+
padding: 8px 12px;
|
|
101
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
102
|
+
border: 1px solid #3a3a5c;
|
|
103
|
+
border-radius: 8px;
|
|
104
|
+
position: relative;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.sip-stealth-address-container[data-size="sm"] {
|
|
108
|
+
padding: 4px 8px;
|
|
109
|
+
gap: 6px;
|
|
110
|
+
border-radius: 6px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.sip-stealth-address-container[data-size="lg"] {
|
|
114
|
+
padding: 12px 16px;
|
|
115
|
+
gap: 12px;
|
|
116
|
+
border-radius: 12px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.sip-stealth-icon {
|
|
120
|
+
display: flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
justify-content: center;
|
|
123
|
+
width: 24px;
|
|
124
|
+
height: 24px;
|
|
125
|
+
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
|
126
|
+
border-radius: 6px;
|
|
127
|
+
color: white;
|
|
128
|
+
flex-shrink: 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.sip-stealth-icon svg {
|
|
132
|
+
width: 14px;
|
|
133
|
+
height: 14px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.sip-stealth-address-container[data-size="sm"] .sip-stealth-icon {
|
|
137
|
+
width: 20px;
|
|
138
|
+
height: 20px;
|
|
139
|
+
border-radius: 4px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.sip-stealth-address-container[data-size="sm"] .sip-stealth-icon svg {
|
|
143
|
+
width: 12px;
|
|
144
|
+
height: 12px;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.sip-stealth-address-container[data-size="lg"] .sip-stealth-icon {
|
|
148
|
+
width: 32px;
|
|
149
|
+
height: 32px;
|
|
150
|
+
border-radius: 8px;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.sip-stealth-address-container[data-size="lg"] .sip-stealth-icon svg {
|
|
154
|
+
width: 18px;
|
|
155
|
+
height: 18px;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.sip-stealth-address-text {
|
|
159
|
+
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', monospace;
|
|
160
|
+
font-size: 13px;
|
|
161
|
+
color: #e2e8f0;
|
|
162
|
+
cursor: default;
|
|
163
|
+
position: relative;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.sip-stealth-address-container[data-size="sm"] .sip-stealth-address-text {
|
|
167
|
+
font-size: 11px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.sip-stealth-address-container[data-size="lg"] .sip-stealth-address-text {
|
|
171
|
+
font-size: 15px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.sip-stealth-address-full {
|
|
175
|
+
position: absolute;
|
|
176
|
+
bottom: calc(100% + 8px);
|
|
177
|
+
left: 50%;
|
|
178
|
+
transform: translateX(-50%);
|
|
179
|
+
padding: 8px 12px;
|
|
180
|
+
background: #1f2937;
|
|
181
|
+
color: #e2e8f0;
|
|
182
|
+
font-size: 11px;
|
|
183
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
184
|
+
border-radius: 6px;
|
|
185
|
+
white-space: nowrap;
|
|
186
|
+
opacity: 0;
|
|
187
|
+
visibility: hidden;
|
|
188
|
+
transition: opacity 0.2s, visibility 0.2s;
|
|
189
|
+
z-index: 10;
|
|
190
|
+
pointer-events: none;
|
|
191
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.sip-stealth-address-full::after {
|
|
195
|
+
content: '';
|
|
196
|
+
position: absolute;
|
|
197
|
+
top: 100%;
|
|
198
|
+
left: 50%;
|
|
199
|
+
transform: translateX(-50%);
|
|
200
|
+
border: 6px solid transparent;
|
|
201
|
+
border-top-color: #1f2937;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.sip-stealth-address-text:hover .sip-stealth-address-full {
|
|
205
|
+
opacity: 1;
|
|
206
|
+
visibility: visible;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.sip-stealth-actions {
|
|
210
|
+
display: flex;
|
|
211
|
+
align-items: center;
|
|
212
|
+
gap: 4px;
|
|
213
|
+
margin-left: 4px;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.sip-stealth-action-btn {
|
|
217
|
+
display: flex;
|
|
218
|
+
align-items: center;
|
|
219
|
+
justify-content: center;
|
|
220
|
+
width: 28px;
|
|
221
|
+
height: 28px;
|
|
222
|
+
padding: 0;
|
|
223
|
+
border: none;
|
|
224
|
+
background: transparent;
|
|
225
|
+
color: #94a3b8;
|
|
226
|
+
cursor: pointer;
|
|
227
|
+
border-radius: 4px;
|
|
228
|
+
transition: all 0.2s;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.sip-stealth-action-btn:hover {
|
|
232
|
+
background: rgba(255, 255, 255, 0.1);
|
|
233
|
+
color: #e2e8f0;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.sip-stealth-action-btn:active {
|
|
237
|
+
transform: scale(0.95);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.sip-stealth-action-btn svg {
|
|
241
|
+
width: 16px;
|
|
242
|
+
height: 16px;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.sip-stealth-address-container[data-size="sm"] .sip-stealth-action-btn {
|
|
246
|
+
width: 24px;
|
|
247
|
+
height: 24px;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.sip-stealth-address-container[data-size="sm"] .sip-stealth-action-btn svg {
|
|
251
|
+
width: 14px;
|
|
252
|
+
height: 14px;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.sip-stealth-address-container[data-size="lg"] .sip-stealth-action-btn {
|
|
256
|
+
width: 32px;
|
|
257
|
+
height: 32px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.sip-stealth-address-container[data-size="lg"] .sip-stealth-action-btn svg {
|
|
261
|
+
width: 18px;
|
|
262
|
+
height: 18px;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.sip-stealth-badge {
|
|
266
|
+
display: inline-flex;
|
|
267
|
+
align-items: center;
|
|
268
|
+
gap: 4px;
|
|
269
|
+
padding: 2px 8px;
|
|
270
|
+
font-size: 10px;
|
|
271
|
+
font-weight: 600;
|
|
272
|
+
text-transform: uppercase;
|
|
273
|
+
letter-spacing: 0.05em;
|
|
274
|
+
border-radius: 4px;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.sip-stealth-badge[data-ownership="yours"] {
|
|
278
|
+
background: rgba(34, 197, 94, 0.2);
|
|
279
|
+
color: #22c55e;
|
|
280
|
+
border: 1px solid rgba(34, 197, 94, 0.3);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.sip-stealth-badge[data-ownership="others"] {
|
|
284
|
+
background: rgba(59, 130, 246, 0.2);
|
|
285
|
+
color: #3b82f6;
|
|
286
|
+
border: 1px solid rgba(59, 130, 246, 0.3);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.sip-stealth-badge[data-ownership="unknown"] {
|
|
290
|
+
background: rgba(156, 163, 175, 0.2);
|
|
291
|
+
color: #9ca3af;
|
|
292
|
+
border: 1px solid rgba(156, 163, 175, 0.3);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.sip-stealth-validation {
|
|
296
|
+
display: flex;
|
|
297
|
+
align-items: center;
|
|
298
|
+
gap: 4px;
|
|
299
|
+
font-size: 11px;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.sip-stealth-validation[data-valid="true"] {
|
|
303
|
+
color: #22c55e;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.sip-stealth-validation[data-valid="false"] {
|
|
307
|
+
color: #ef4444;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.sip-stealth-validation svg {
|
|
311
|
+
width: 14px;
|
|
312
|
+
height: 14px;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.sip-stealth-qr-modal {
|
|
316
|
+
position: fixed;
|
|
317
|
+
top: 0;
|
|
318
|
+
left: 0;
|
|
319
|
+
right: 0;
|
|
320
|
+
bottom: 0;
|
|
321
|
+
background: rgba(0, 0, 0, 0.7);
|
|
322
|
+
display: flex;
|
|
323
|
+
align-items: center;
|
|
324
|
+
justify-content: center;
|
|
325
|
+
z-index: 1000;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.sip-stealth-qr-content {
|
|
329
|
+
background: white;
|
|
330
|
+
padding: 24px;
|
|
331
|
+
border-radius: 16px;
|
|
332
|
+
text-align: center;
|
|
333
|
+
max-width: 320px;
|
|
334
|
+
width: 90%;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.sip-stealth-qr-title {
|
|
338
|
+
font-size: 16px;
|
|
339
|
+
font-weight: 600;
|
|
340
|
+
color: #111827;
|
|
341
|
+
margin-bottom: 16px;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.sip-stealth-qr-code {
|
|
345
|
+
display: flex;
|
|
346
|
+
align-items: center;
|
|
347
|
+
justify-content: center;
|
|
348
|
+
width: 200px;
|
|
349
|
+
height: 200px;
|
|
350
|
+
margin: 0 auto 16px;
|
|
351
|
+
background: #f9fafb;
|
|
352
|
+
border-radius: 8px;
|
|
353
|
+
border: 1px solid #e5e7eb;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.sip-stealth-qr-address {
|
|
357
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
358
|
+
font-size: 11px;
|
|
359
|
+
color: #6b7280;
|
|
360
|
+
word-break: break-all;
|
|
361
|
+
margin-bottom: 16px;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.sip-stealth-qr-close {
|
|
365
|
+
padding: 8px 24px;
|
|
366
|
+
background: #111827;
|
|
367
|
+
color: white;
|
|
368
|
+
border: none;
|
|
369
|
+
border-radius: 8px;
|
|
370
|
+
font-size: 14px;
|
|
371
|
+
font-weight: 500;
|
|
372
|
+
cursor: pointer;
|
|
373
|
+
transition: background 0.2s;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.sip-stealth-qr-close:hover {
|
|
377
|
+
background: #374151;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.sip-stealth-copy-success {
|
|
381
|
+
position: absolute;
|
|
382
|
+
top: -32px;
|
|
383
|
+
left: 50%;
|
|
384
|
+
transform: translateX(-50%);
|
|
385
|
+
padding: 4px 12px;
|
|
386
|
+
background: #22c55e;
|
|
387
|
+
color: white;
|
|
388
|
+
font-size: 12px;
|
|
389
|
+
font-weight: 500;
|
|
390
|
+
border-radius: 4px;
|
|
391
|
+
opacity: 0;
|
|
392
|
+
visibility: hidden;
|
|
393
|
+
transition: opacity 0.2s, visibility 0.2s;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.sip-stealth-copy-success.visible {
|
|
397
|
+
opacity: 1;
|
|
398
|
+
visibility: visible;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/* Dark mode already active by default, light mode override */
|
|
402
|
+
@media (prefers-color-scheme: light) {
|
|
403
|
+
.sip-stealth-address-container {
|
|
404
|
+
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
|
405
|
+
border-color: #e2e8f0;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.sip-stealth-address-text {
|
|
409
|
+
color: #1e293b;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.sip-stealth-action-btn {
|
|
413
|
+
color: #64748b;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.sip-stealth-action-btn:hover {
|
|
417
|
+
background: rgba(0, 0, 0, 0.05);
|
|
418
|
+
color: #1e293b;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
`
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Shield icon for stealth address indicator
|
|
425
|
+
*/
|
|
426
|
+
const ShieldIcon = () => (
|
|
427
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
428
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
429
|
+
<path d="M9 12l2 2 4-4" />
|
|
430
|
+
</svg>
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Copy icon
|
|
435
|
+
*/
|
|
436
|
+
const CopyIcon = () => (
|
|
437
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
438
|
+
<rect x="9" y="9" width="13" height="13" rx="2" />
|
|
439
|
+
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
|
|
440
|
+
</svg>
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* External link icon
|
|
445
|
+
*/
|
|
446
|
+
const ExternalLinkIcon = () => (
|
|
447
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
448
|
+
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" />
|
|
449
|
+
<polyline points="15 3 21 3 21 9" />
|
|
450
|
+
<line x1="10" y1="14" x2="21" y2="3" />
|
|
451
|
+
</svg>
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* QR code icon
|
|
456
|
+
*/
|
|
457
|
+
const QrCodeIcon = () => (
|
|
458
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
459
|
+
<rect x="3" y="3" width="7" height="7" rx="1" />
|
|
460
|
+
<rect x="14" y="3" width="7" height="7" rx="1" />
|
|
461
|
+
<rect x="3" y="14" width="7" height="7" rx="1" />
|
|
462
|
+
<rect x="14" y="14" width="3" height="3" />
|
|
463
|
+
<rect x="18" y="14" width="3" height="3" />
|
|
464
|
+
<rect x="14" y="18" width="3" height="3" />
|
|
465
|
+
<rect x="18" y="18" width="3" height="3" />
|
|
466
|
+
</svg>
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Check icon
|
|
471
|
+
*/
|
|
472
|
+
const CheckIcon = () => (
|
|
473
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
474
|
+
<polyline points="20 6 9 17 4 12" />
|
|
475
|
+
</svg>
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* X icon
|
|
480
|
+
*/
|
|
481
|
+
const XIcon = () => (
|
|
482
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
483
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
484
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
485
|
+
</svg>
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* StealthAddressDisplay - Component for displaying NEAR stealth addresses
|
|
490
|
+
*
|
|
491
|
+
* Displays stealth addresses with visual distinction from regular NEAR addresses,
|
|
492
|
+
* including copy functionality, explorer links, and QR code generation.
|
|
493
|
+
*
|
|
494
|
+
* @example Basic usage
|
|
495
|
+
* ```tsx
|
|
496
|
+
* import { StealthAddressDisplay } from '@sip-protocol/react'
|
|
497
|
+
*
|
|
498
|
+
* function WalletView() {
|
|
499
|
+
* return (
|
|
500
|
+
* <StealthAddressDisplay
|
|
501
|
+
* address="a1b2c3d4e5f6..."
|
|
502
|
+
* ownership="yours"
|
|
503
|
+
* />
|
|
504
|
+
* )
|
|
505
|
+
* }
|
|
506
|
+
* ```
|
|
507
|
+
*
|
|
508
|
+
* @example With meta-address for QR
|
|
509
|
+
* ```tsx
|
|
510
|
+
* <StealthAddressDisplay
|
|
511
|
+
* address="a1b2c3d4e5f6..."
|
|
512
|
+
* metaAddress="sip:near:0x02abc...123:0x03def...456"
|
|
513
|
+
* showQrCode
|
|
514
|
+
* showExplorerLink
|
|
515
|
+
* network="mainnet"
|
|
516
|
+
* />
|
|
517
|
+
* ```
|
|
518
|
+
*/
|
|
519
|
+
export function StealthAddressDisplay({
|
|
520
|
+
address,
|
|
521
|
+
metaAddress,
|
|
522
|
+
ownership = 'unknown',
|
|
523
|
+
isValid,
|
|
524
|
+
network = 'mainnet',
|
|
525
|
+
networkConfig,
|
|
526
|
+
showQrCode = true,
|
|
527
|
+
showExplorerLink = true,
|
|
528
|
+
showCopyButton = true,
|
|
529
|
+
showOwnership = true,
|
|
530
|
+
showValidation = true,
|
|
531
|
+
className = '',
|
|
532
|
+
size = 'md',
|
|
533
|
+
onCopy,
|
|
534
|
+
onShowQr,
|
|
535
|
+
}: StealthAddressDisplayProps) {
|
|
536
|
+
const [showCopySuccess, setShowCopySuccess] = useState(false)
|
|
537
|
+
const [showQrModal, setShowQrModal] = useState(false)
|
|
538
|
+
|
|
539
|
+
// Determine validation status
|
|
540
|
+
const validationStatus = useMemo(() => {
|
|
541
|
+
if (isValid !== undefined) return isValid
|
|
542
|
+
return isValidStealthAddress(address)
|
|
543
|
+
}, [address, isValid])
|
|
544
|
+
|
|
545
|
+
// Get network config
|
|
546
|
+
const config = useMemo(() => {
|
|
547
|
+
return networkConfig ?? NEAR_NETWORKS[network]
|
|
548
|
+
}, [network, networkConfig])
|
|
549
|
+
|
|
550
|
+
// Truncate address for display
|
|
551
|
+
const displayAddress = useMemo(() => {
|
|
552
|
+
const chars = size === 'sm' ? 6 : size === 'lg' ? 10 : 8
|
|
553
|
+
return truncateAddress(address, chars, chars)
|
|
554
|
+
}, [address, size])
|
|
555
|
+
|
|
556
|
+
// Handle copy to clipboard
|
|
557
|
+
const handleCopy = useCallback(async () => {
|
|
558
|
+
try {
|
|
559
|
+
await navigator.clipboard.writeText(address)
|
|
560
|
+
setShowCopySuccess(true)
|
|
561
|
+
onCopy?.(address)
|
|
562
|
+
setTimeout(() => setShowCopySuccess(false), 2000)
|
|
563
|
+
} catch {
|
|
564
|
+
// Fallback for older browsers
|
|
565
|
+
const textArea = document.createElement('textarea')
|
|
566
|
+
textArea.value = address
|
|
567
|
+
document.body.appendChild(textArea)
|
|
568
|
+
textArea.select()
|
|
569
|
+
document.execCommand('copy')
|
|
570
|
+
document.body.removeChild(textArea)
|
|
571
|
+
setShowCopySuccess(true)
|
|
572
|
+
onCopy?.(address)
|
|
573
|
+
setTimeout(() => setShowCopySuccess(false), 2000)
|
|
574
|
+
}
|
|
575
|
+
}, [address, onCopy])
|
|
576
|
+
|
|
577
|
+
// Handle show QR code
|
|
578
|
+
const handleShowQr = useCallback(() => {
|
|
579
|
+
setShowQrModal(true)
|
|
580
|
+
onShowQr?.(metaAddress ?? address)
|
|
581
|
+
}, [address, metaAddress, onShowQr])
|
|
582
|
+
|
|
583
|
+
// Handle close QR modal
|
|
584
|
+
const handleCloseQr = useCallback(() => {
|
|
585
|
+
setShowQrModal(false)
|
|
586
|
+
}, [])
|
|
587
|
+
|
|
588
|
+
// Get ownership label
|
|
589
|
+
const ownershipLabel = useMemo(() => {
|
|
590
|
+
switch (ownership) {
|
|
591
|
+
case 'yours':
|
|
592
|
+
return 'Your Address'
|
|
593
|
+
case 'others':
|
|
594
|
+
return "Someone's Address"
|
|
595
|
+
case 'unknown':
|
|
596
|
+
default:
|
|
597
|
+
return 'Unknown'
|
|
598
|
+
}
|
|
599
|
+
}, [ownership])
|
|
600
|
+
|
|
601
|
+
// Explorer URL
|
|
602
|
+
const explorerUrl = useMemo(() => {
|
|
603
|
+
return `${config.explorerUrl}/${address}`
|
|
604
|
+
}, [config.explorerUrl, address])
|
|
605
|
+
|
|
606
|
+
return (
|
|
607
|
+
<>
|
|
608
|
+
<style>{styles}</style>
|
|
609
|
+
|
|
610
|
+
<div className={`sip-stealth-address ${className}`}>
|
|
611
|
+
<div
|
|
612
|
+
className="sip-stealth-address-container"
|
|
613
|
+
data-size={size}
|
|
614
|
+
data-ownership={ownership}
|
|
615
|
+
>
|
|
616
|
+
{/* Copy success message */}
|
|
617
|
+
<span className={`sip-stealth-copy-success ${showCopySuccess ? 'visible' : ''}`}>
|
|
618
|
+
Copied!
|
|
619
|
+
</span>
|
|
620
|
+
|
|
621
|
+
{/* Stealth icon badge */}
|
|
622
|
+
<div className="sip-stealth-icon" title="Stealth Address">
|
|
623
|
+
<ShieldIcon />
|
|
624
|
+
</div>
|
|
625
|
+
|
|
626
|
+
{/* Address text with full address tooltip */}
|
|
627
|
+
<span className="sip-stealth-address-text" data-testid="address-text">
|
|
628
|
+
{displayAddress}
|
|
629
|
+
<span className="sip-stealth-address-full">{address}</span>
|
|
630
|
+
</span>
|
|
631
|
+
|
|
632
|
+
{/* Action buttons */}
|
|
633
|
+
<div className="sip-stealth-actions">
|
|
634
|
+
{showCopyButton && (
|
|
635
|
+
<button
|
|
636
|
+
type="button"
|
|
637
|
+
className="sip-stealth-action-btn"
|
|
638
|
+
onClick={handleCopy}
|
|
639
|
+
title="Copy address"
|
|
640
|
+
aria-label="Copy address to clipboard"
|
|
641
|
+
>
|
|
642
|
+
<CopyIcon />
|
|
643
|
+
</button>
|
|
644
|
+
)}
|
|
645
|
+
|
|
646
|
+
{showExplorerLink && (
|
|
647
|
+
<a
|
|
648
|
+
href={explorerUrl}
|
|
649
|
+
target="_blank"
|
|
650
|
+
rel="noopener noreferrer"
|
|
651
|
+
className="sip-stealth-action-btn"
|
|
652
|
+
title={`View on ${config.name}`}
|
|
653
|
+
aria-label={`View on ${config.name}`}
|
|
654
|
+
>
|
|
655
|
+
<ExternalLinkIcon />
|
|
656
|
+
</a>
|
|
657
|
+
)}
|
|
658
|
+
|
|
659
|
+
{showQrCode && (
|
|
660
|
+
<button
|
|
661
|
+
type="button"
|
|
662
|
+
className="sip-stealth-action-btn"
|
|
663
|
+
onClick={handleShowQr}
|
|
664
|
+
title="Show QR code"
|
|
665
|
+
aria-label="Show QR code"
|
|
666
|
+
>
|
|
667
|
+
<QrCodeIcon />
|
|
668
|
+
</button>
|
|
669
|
+
)}
|
|
670
|
+
</div>
|
|
671
|
+
|
|
672
|
+
{/* Ownership badge */}
|
|
673
|
+
{showOwnership && (
|
|
674
|
+
<span
|
|
675
|
+
className="sip-stealth-badge"
|
|
676
|
+
data-ownership={ownership}
|
|
677
|
+
>
|
|
678
|
+
{ownershipLabel}
|
|
679
|
+
</span>
|
|
680
|
+
)}
|
|
681
|
+
</div>
|
|
682
|
+
|
|
683
|
+
{/* Validation indicator */}
|
|
684
|
+
{showValidation && (
|
|
685
|
+
<div
|
|
686
|
+
className="sip-stealth-validation"
|
|
687
|
+
data-valid={validationStatus}
|
|
688
|
+
aria-label={validationStatus ? 'Valid stealth address' : 'Invalid stealth address'}
|
|
689
|
+
>
|
|
690
|
+
{validationStatus ? <CheckIcon /> : <XIcon />}
|
|
691
|
+
<span>{validationStatus ? 'Valid stealth address' : 'Invalid format'}</span>
|
|
692
|
+
</div>
|
|
693
|
+
)}
|
|
694
|
+
</div>
|
|
695
|
+
|
|
696
|
+
{/* QR Code Modal */}
|
|
697
|
+
{showQrModal && (
|
|
698
|
+
<div
|
|
699
|
+
className="sip-stealth-qr-modal"
|
|
700
|
+
onClick={handleCloseQr}
|
|
701
|
+
role="dialog"
|
|
702
|
+
aria-modal="true"
|
|
703
|
+
aria-labelledby="qr-modal-title"
|
|
704
|
+
>
|
|
705
|
+
<div
|
|
706
|
+
className="sip-stealth-qr-content"
|
|
707
|
+
onClick={(e) => e.stopPropagation()}
|
|
708
|
+
>
|
|
709
|
+
<h3 id="qr-modal-title" className="sip-stealth-qr-title">
|
|
710
|
+
Scan to Receive
|
|
711
|
+
</h3>
|
|
712
|
+
<div className="sip-stealth-qr-code" data-testid="qr-code-container">
|
|
713
|
+
{/* QR Code placeholder - can be enhanced with actual QR library */}
|
|
714
|
+
<QrCodeIcon />
|
|
715
|
+
</div>
|
|
716
|
+
<p className="sip-stealth-qr-address">
|
|
717
|
+
{metaAddress ?? address}
|
|
718
|
+
</p>
|
|
719
|
+
<button
|
|
720
|
+
type="button"
|
|
721
|
+
className="sip-stealth-qr-close"
|
|
722
|
+
onClick={handleCloseQr}
|
|
723
|
+
>
|
|
724
|
+
Close
|
|
725
|
+
</button>
|
|
726
|
+
</div>
|
|
727
|
+
</div>
|
|
728
|
+
)}
|
|
729
|
+
</>
|
|
730
|
+
)
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Hook to manage stealth address display state
|
|
735
|
+
*/
|
|
736
|
+
export function useStealthAddressDisplay(
|
|
737
|
+
address: string,
|
|
738
|
+
options: {
|
|
739
|
+
checkOwnership?: (address: string) => OwnershipStatus
|
|
740
|
+
validateAddress?: (address: string) => boolean
|
|
741
|
+
} = {}
|
|
742
|
+
) {
|
|
743
|
+
const { checkOwnership, validateAddress } = options
|
|
744
|
+
|
|
745
|
+
const ownership = useMemo(() => {
|
|
746
|
+
if (checkOwnership) {
|
|
747
|
+
return checkOwnership(address)
|
|
748
|
+
}
|
|
749
|
+
return 'unknown' as OwnershipStatus
|
|
750
|
+
}, [address, checkOwnership])
|
|
751
|
+
|
|
752
|
+
const isValid = useMemo(() => {
|
|
753
|
+
if (validateAddress) {
|
|
754
|
+
return validateAddress(address)
|
|
755
|
+
}
|
|
756
|
+
return isValidStealthAddress(address)
|
|
757
|
+
}, [address, validateAddress])
|
|
758
|
+
|
|
759
|
+
const truncated = useMemo(() => truncateAddress(address), [address])
|
|
760
|
+
|
|
761
|
+
return {
|
|
762
|
+
address,
|
|
763
|
+
truncated,
|
|
764
|
+
ownership,
|
|
765
|
+
isValid,
|
|
766
|
+
isStealth: isValidStealthAddress(address),
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export default StealthAddressDisplay
|