@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,1050 @@
|
|
|
1
|
+
import React, { useState, useCallback, useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* EthereumStealthAddressDisplay Component
|
|
5
|
+
*
|
|
6
|
+
* Displays EIP-5564 stealth addresses with Ethereum-specific features including
|
|
7
|
+
* network support for mainnet and L2s, Etherscan integration, and EIP-5564 validation.
|
|
8
|
+
*
|
|
9
|
+
* @module components/ethereum/stealth-address-display
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Ownership status for stealth addresses
|
|
14
|
+
*/
|
|
15
|
+
export type EthereumOwnershipStatus = 'yours' | 'others' | 'unknown'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Ethereum network IDs
|
|
19
|
+
*/
|
|
20
|
+
export type EthereumStealthNetworkId =
|
|
21
|
+
| 'mainnet'
|
|
22
|
+
| 'arbitrum'
|
|
23
|
+
| 'optimism'
|
|
24
|
+
| 'base'
|
|
25
|
+
| 'polygon'
|
|
26
|
+
| 'sepolia'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Network configuration for explorer links
|
|
30
|
+
*/
|
|
31
|
+
export interface EthereumStealthNetworkConfig {
|
|
32
|
+
name: string
|
|
33
|
+
chainId: number
|
|
34
|
+
explorerUrl: string
|
|
35
|
+
explorerName: string
|
|
36
|
+
isL2: boolean
|
|
37
|
+
color: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Default Ethereum network configurations
|
|
42
|
+
*/
|
|
43
|
+
export const ETHEREUM_STEALTH_NETWORKS: Record<
|
|
44
|
+
EthereumStealthNetworkId,
|
|
45
|
+
EthereumStealthNetworkConfig
|
|
46
|
+
> = {
|
|
47
|
+
mainnet: {
|
|
48
|
+
name: 'Ethereum',
|
|
49
|
+
chainId: 1,
|
|
50
|
+
explorerUrl: 'https://etherscan.io/address',
|
|
51
|
+
explorerName: 'Etherscan',
|
|
52
|
+
isL2: false,
|
|
53
|
+
color: '#627EEA',
|
|
54
|
+
},
|
|
55
|
+
arbitrum: {
|
|
56
|
+
name: 'Arbitrum',
|
|
57
|
+
chainId: 42161,
|
|
58
|
+
explorerUrl: 'https://arbiscan.io/address',
|
|
59
|
+
explorerName: 'Arbiscan',
|
|
60
|
+
isL2: true,
|
|
61
|
+
color: '#28A0F0',
|
|
62
|
+
},
|
|
63
|
+
optimism: {
|
|
64
|
+
name: 'Optimism',
|
|
65
|
+
chainId: 10,
|
|
66
|
+
explorerUrl: 'https://optimistic.etherscan.io/address',
|
|
67
|
+
explorerName: 'Optimism Explorer',
|
|
68
|
+
isL2: true,
|
|
69
|
+
color: '#FF0420',
|
|
70
|
+
},
|
|
71
|
+
base: {
|
|
72
|
+
name: 'Base',
|
|
73
|
+
chainId: 8453,
|
|
74
|
+
explorerUrl: 'https://basescan.org/address',
|
|
75
|
+
explorerName: 'BaseScan',
|
|
76
|
+
isL2: true,
|
|
77
|
+
color: '#0052FF',
|
|
78
|
+
},
|
|
79
|
+
polygon: {
|
|
80
|
+
name: 'Polygon',
|
|
81
|
+
chainId: 137,
|
|
82
|
+
explorerUrl: 'https://polygonscan.com/address',
|
|
83
|
+
explorerName: 'PolygonScan',
|
|
84
|
+
isL2: true,
|
|
85
|
+
color: '#8247E5',
|
|
86
|
+
},
|
|
87
|
+
sepolia: {
|
|
88
|
+
name: 'Sepolia',
|
|
89
|
+
chainId: 11155111,
|
|
90
|
+
explorerUrl: 'https://sepolia.etherscan.io/address',
|
|
91
|
+
explorerName: 'Sepolia Etherscan',
|
|
92
|
+
isL2: false,
|
|
93
|
+
color: '#CFB5F0',
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* EthereumStealthAddressDisplay component props
|
|
99
|
+
*/
|
|
100
|
+
export interface EthereumStealthAddressDisplayProps {
|
|
101
|
+
/** The stealth address to display (0x prefixed, 42 chars) */
|
|
102
|
+
address: string
|
|
103
|
+
/** Optional stealth meta-address for QR code (full sip: format) */
|
|
104
|
+
metaAddress?: string
|
|
105
|
+
/** Optional ephemeral public key (EIP-5564) */
|
|
106
|
+
ephemeralPublicKey?: string
|
|
107
|
+
/** View tag for the stealth address (EIP-5564) */
|
|
108
|
+
viewTag?: number
|
|
109
|
+
/** Ownership status of the address */
|
|
110
|
+
ownership?: EthereumOwnershipStatus
|
|
111
|
+
/** Whether the address is validated */
|
|
112
|
+
isValid?: boolean
|
|
113
|
+
/** Network for explorer links */
|
|
114
|
+
network?: EthereumStealthNetworkId
|
|
115
|
+
/** Custom network configuration */
|
|
116
|
+
networkConfig?: EthereumStealthNetworkConfig
|
|
117
|
+
/** Whether to show the QR code button */
|
|
118
|
+
showQrCode?: boolean
|
|
119
|
+
/** Whether to show the explorer link */
|
|
120
|
+
showExplorerLink?: boolean
|
|
121
|
+
/** Whether to show the copy button */
|
|
122
|
+
showCopyButton?: boolean
|
|
123
|
+
/** Whether to show the ownership badge */
|
|
124
|
+
showOwnership?: boolean
|
|
125
|
+
/** Whether to show the validation indicator */
|
|
126
|
+
showValidation?: boolean
|
|
127
|
+
/** Whether to show the network badge */
|
|
128
|
+
showNetworkBadge?: boolean
|
|
129
|
+
/** Whether to show the EIP-5564 badge */
|
|
130
|
+
showEipBadge?: boolean
|
|
131
|
+
/** Whether to show the view tag */
|
|
132
|
+
showViewTag?: boolean
|
|
133
|
+
/** Custom class name */
|
|
134
|
+
className?: string
|
|
135
|
+
/** Size variant */
|
|
136
|
+
size?: 'sm' | 'md' | 'lg'
|
|
137
|
+
/** Callback when address is copied */
|
|
138
|
+
onCopy?: (address: string) => void
|
|
139
|
+
/** Callback when QR code is shown */
|
|
140
|
+
onShowQr?: (address: string) => void
|
|
141
|
+
/** Callback when ephemeral key is copied */
|
|
142
|
+
onCopyEphemeralKey?: (key: string) => void
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Validates an Ethereum address format
|
|
147
|
+
*/
|
|
148
|
+
export function isValidEthereumAddress(address: string): boolean {
|
|
149
|
+
// Ethereum addresses are 42 chars with 0x prefix
|
|
150
|
+
const ethRegex = /^0x[0-9a-fA-F]{40}$/
|
|
151
|
+
return ethRegex.test(address)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Validates an EIP-5564 stealth address
|
|
156
|
+
* Stealth addresses are valid Ethereum addresses generated from EIP-5564
|
|
157
|
+
*/
|
|
158
|
+
export function isValidEthereumStealthAddress(address: string): boolean {
|
|
159
|
+
return isValidEthereumAddress(address)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Validates an ephemeral public key (compressed secp256k1)
|
|
164
|
+
*/
|
|
165
|
+
export function isValidEphemeralPublicKey(key: string): boolean {
|
|
166
|
+
// Compressed public key: 66 chars (02 or 03 prefix + 64 hex)
|
|
167
|
+
const compressedRegex = /^0x0[23][0-9a-fA-F]{64}$/
|
|
168
|
+
// Uncompressed public key: 130 chars (04 prefix + 128 hex)
|
|
169
|
+
const uncompressedRegex = /^0x04[0-9a-fA-F]{128}$/
|
|
170
|
+
return compressedRegex.test(key) || uncompressedRegex.test(key)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Truncates an address for display
|
|
175
|
+
*/
|
|
176
|
+
export function truncateEthereumAddress(
|
|
177
|
+
address: string,
|
|
178
|
+
startChars = 6,
|
|
179
|
+
endChars = 4
|
|
180
|
+
): string {
|
|
181
|
+
if (address.length <= startChars + endChars + 3) {
|
|
182
|
+
return address
|
|
183
|
+
}
|
|
184
|
+
return `${address.slice(0, startChars)}...${address.slice(-endChars)}`
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* CSS styles for the component
|
|
189
|
+
*/
|
|
190
|
+
const styles = `
|
|
191
|
+
.sip-eth-stealth-address {
|
|
192
|
+
display: inline-flex;
|
|
193
|
+
flex-direction: column;
|
|
194
|
+
gap: 8px;
|
|
195
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.sip-eth-stealth-container {
|
|
199
|
+
display: inline-flex;
|
|
200
|
+
align-items: center;
|
|
201
|
+
gap: 8px;
|
|
202
|
+
padding: 8px 12px;
|
|
203
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #0d1421 100%);
|
|
204
|
+
border: 1px solid #2d3748;
|
|
205
|
+
border-radius: 8px;
|
|
206
|
+
position: relative;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.sip-eth-stealth-container[data-size="sm"] {
|
|
210
|
+
padding: 4px 8px;
|
|
211
|
+
gap: 6px;
|
|
212
|
+
border-radius: 6px;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.sip-eth-stealth-container[data-size="lg"] {
|
|
216
|
+
padding: 12px 16px;
|
|
217
|
+
gap: 12px;
|
|
218
|
+
border-radius: 12px;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.sip-eth-stealth-icon {
|
|
222
|
+
display: flex;
|
|
223
|
+
align-items: center;
|
|
224
|
+
justify-content: center;
|
|
225
|
+
width: 24px;
|
|
226
|
+
height: 24px;
|
|
227
|
+
background: linear-gradient(135deg, #627EEA 0%, #8A92B2 100%);
|
|
228
|
+
border-radius: 6px;
|
|
229
|
+
color: white;
|
|
230
|
+
flex-shrink: 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.sip-eth-stealth-icon svg {
|
|
234
|
+
width: 14px;
|
|
235
|
+
height: 14px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.sip-eth-stealth-container[data-size="sm"] .sip-eth-stealth-icon {
|
|
239
|
+
width: 20px;
|
|
240
|
+
height: 20px;
|
|
241
|
+
border-radius: 4px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.sip-eth-stealth-container[data-size="sm"] .sip-eth-stealth-icon svg {
|
|
245
|
+
width: 12px;
|
|
246
|
+
height: 12px;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.sip-eth-stealth-container[data-size="lg"] .sip-eth-stealth-icon {
|
|
250
|
+
width: 32px;
|
|
251
|
+
height: 32px;
|
|
252
|
+
border-radius: 8px;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.sip-eth-stealth-container[data-size="lg"] .sip-eth-stealth-icon svg {
|
|
256
|
+
width: 18px;
|
|
257
|
+
height: 18px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.sip-eth-stealth-address-text {
|
|
261
|
+
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', monospace;
|
|
262
|
+
font-size: 13px;
|
|
263
|
+
color: #e2e8f0;
|
|
264
|
+
cursor: default;
|
|
265
|
+
position: relative;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.sip-eth-stealth-container[data-size="sm"] .sip-eth-stealth-address-text {
|
|
269
|
+
font-size: 11px;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.sip-eth-stealth-container[data-size="lg"] .sip-eth-stealth-address-text {
|
|
273
|
+
font-size: 15px;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.sip-eth-stealth-address-full {
|
|
277
|
+
position: absolute;
|
|
278
|
+
bottom: calc(100% + 8px);
|
|
279
|
+
left: 50%;
|
|
280
|
+
transform: translateX(-50%);
|
|
281
|
+
padding: 8px 12px;
|
|
282
|
+
background: #1f2937;
|
|
283
|
+
color: #e2e8f0;
|
|
284
|
+
font-size: 11px;
|
|
285
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
286
|
+
border-radius: 6px;
|
|
287
|
+
white-space: nowrap;
|
|
288
|
+
opacity: 0;
|
|
289
|
+
visibility: hidden;
|
|
290
|
+
transition: opacity 0.2s, visibility 0.2s;
|
|
291
|
+
z-index: 10;
|
|
292
|
+
pointer-events: none;
|
|
293
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.sip-eth-stealth-address-full::after {
|
|
297
|
+
content: '';
|
|
298
|
+
position: absolute;
|
|
299
|
+
top: 100%;
|
|
300
|
+
left: 50%;
|
|
301
|
+
transform: translateX(-50%);
|
|
302
|
+
border: 6px solid transparent;
|
|
303
|
+
border-top-color: #1f2937;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.sip-eth-stealth-address-text:hover .sip-eth-stealth-address-full {
|
|
307
|
+
opacity: 1;
|
|
308
|
+
visibility: visible;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.sip-eth-stealth-actions {
|
|
312
|
+
display: flex;
|
|
313
|
+
align-items: center;
|
|
314
|
+
gap: 4px;
|
|
315
|
+
margin-left: 4px;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.sip-eth-stealth-action-btn {
|
|
319
|
+
display: flex;
|
|
320
|
+
align-items: center;
|
|
321
|
+
justify-content: center;
|
|
322
|
+
width: 28px;
|
|
323
|
+
height: 28px;
|
|
324
|
+
padding: 0;
|
|
325
|
+
border: none;
|
|
326
|
+
background: transparent;
|
|
327
|
+
color: #94a3b8;
|
|
328
|
+
cursor: pointer;
|
|
329
|
+
border-radius: 4px;
|
|
330
|
+
transition: all 0.2s;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.sip-eth-stealth-action-btn:hover {
|
|
334
|
+
background: rgba(255, 255, 255, 0.1);
|
|
335
|
+
color: #e2e8f0;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.sip-eth-stealth-action-btn:active {
|
|
339
|
+
transform: scale(0.95);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.sip-eth-stealth-action-btn svg {
|
|
343
|
+
width: 16px;
|
|
344
|
+
height: 16px;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.sip-eth-stealth-container[data-size="sm"] .sip-eth-stealth-action-btn {
|
|
348
|
+
width: 24px;
|
|
349
|
+
height: 24px;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.sip-eth-stealth-container[data-size="sm"] .sip-eth-stealth-action-btn svg {
|
|
353
|
+
width: 14px;
|
|
354
|
+
height: 14px;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.sip-eth-stealth-container[data-size="lg"] .sip-eth-stealth-action-btn {
|
|
358
|
+
width: 32px;
|
|
359
|
+
height: 32px;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.sip-eth-stealth-container[data-size="lg"] .sip-eth-stealth-action-btn svg {
|
|
363
|
+
width: 18px;
|
|
364
|
+
height: 18px;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.sip-eth-stealth-badge {
|
|
368
|
+
display: inline-flex;
|
|
369
|
+
align-items: center;
|
|
370
|
+
gap: 4px;
|
|
371
|
+
padding: 2px 8px;
|
|
372
|
+
font-size: 10px;
|
|
373
|
+
font-weight: 600;
|
|
374
|
+
text-transform: uppercase;
|
|
375
|
+
letter-spacing: 0.05em;
|
|
376
|
+
border-radius: 4px;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.sip-eth-stealth-badge[data-ownership="yours"] {
|
|
380
|
+
background: rgba(34, 197, 94, 0.2);
|
|
381
|
+
color: #22c55e;
|
|
382
|
+
border: 1px solid rgba(34, 197, 94, 0.3);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.sip-eth-stealth-badge[data-ownership="others"] {
|
|
386
|
+
background: rgba(59, 130, 246, 0.2);
|
|
387
|
+
color: #3b82f6;
|
|
388
|
+
border: 1px solid rgba(59, 130, 246, 0.3);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.sip-eth-stealth-badge[data-ownership="unknown"] {
|
|
392
|
+
background: rgba(156, 163, 175, 0.2);
|
|
393
|
+
color: #9ca3af;
|
|
394
|
+
border: 1px solid rgba(156, 163, 175, 0.3);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.sip-eth-stealth-network-badge {
|
|
398
|
+
display: inline-flex;
|
|
399
|
+
align-items: center;
|
|
400
|
+
gap: 4px;
|
|
401
|
+
padding: 2px 8px;
|
|
402
|
+
font-size: 10px;
|
|
403
|
+
font-weight: 600;
|
|
404
|
+
border-radius: 4px;
|
|
405
|
+
background: rgba(98, 126, 234, 0.2);
|
|
406
|
+
color: #627EEA;
|
|
407
|
+
border: 1px solid rgba(98, 126, 234, 0.3);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.sip-eth-stealth-network-badge[data-l2="true"] {
|
|
411
|
+
background: rgba(40, 160, 240, 0.2);
|
|
412
|
+
color: #28A0F0;
|
|
413
|
+
border: 1px solid rgba(40, 160, 240, 0.3);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.sip-eth-stealth-eip-badge {
|
|
417
|
+
display: inline-flex;
|
|
418
|
+
align-items: center;
|
|
419
|
+
gap: 4px;
|
|
420
|
+
padding: 2px 8px;
|
|
421
|
+
font-size: 9px;
|
|
422
|
+
font-weight: 600;
|
|
423
|
+
border-radius: 4px;
|
|
424
|
+
background: rgba(139, 92, 246, 0.2);
|
|
425
|
+
color: #8b5cf6;
|
|
426
|
+
border: 1px solid rgba(139, 92, 246, 0.3);
|
|
427
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.sip-eth-stealth-view-tag {
|
|
431
|
+
display: inline-flex;
|
|
432
|
+
align-items: center;
|
|
433
|
+
gap: 4px;
|
|
434
|
+
padding: 2px 8px;
|
|
435
|
+
font-size: 10px;
|
|
436
|
+
font-weight: 500;
|
|
437
|
+
border-radius: 4px;
|
|
438
|
+
background: rgba(251, 191, 36, 0.2);
|
|
439
|
+
color: #fbbf24;
|
|
440
|
+
border: 1px solid rgba(251, 191, 36, 0.3);
|
|
441
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.sip-eth-stealth-validation {
|
|
445
|
+
display: flex;
|
|
446
|
+
align-items: center;
|
|
447
|
+
gap: 4px;
|
|
448
|
+
font-size: 11px;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.sip-eth-stealth-validation[data-valid="true"] {
|
|
452
|
+
color: #22c55e;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.sip-eth-stealth-validation[data-valid="false"] {
|
|
456
|
+
color: #ef4444;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.sip-eth-stealth-validation svg {
|
|
460
|
+
width: 14px;
|
|
461
|
+
height: 14px;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.sip-eth-stealth-meta {
|
|
465
|
+
display: flex;
|
|
466
|
+
flex-wrap: wrap;
|
|
467
|
+
gap: 6px;
|
|
468
|
+
align-items: center;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.sip-eth-stealth-ephemeral {
|
|
472
|
+
display: flex;
|
|
473
|
+
align-items: center;
|
|
474
|
+
gap: 6px;
|
|
475
|
+
padding: 4px 8px;
|
|
476
|
+
background: rgba(99, 102, 241, 0.1);
|
|
477
|
+
border: 1px solid rgba(99, 102, 241, 0.2);
|
|
478
|
+
border-radius: 6px;
|
|
479
|
+
font-size: 10px;
|
|
480
|
+
color: #a5b4fc;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.sip-eth-stealth-ephemeral-label {
|
|
484
|
+
font-weight: 600;
|
|
485
|
+
text-transform: uppercase;
|
|
486
|
+
letter-spacing: 0.05em;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.sip-eth-stealth-ephemeral-key {
|
|
490
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
491
|
+
color: #c4b5fd;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.sip-eth-stealth-qr-modal {
|
|
495
|
+
position: fixed;
|
|
496
|
+
top: 0;
|
|
497
|
+
left: 0;
|
|
498
|
+
right: 0;
|
|
499
|
+
bottom: 0;
|
|
500
|
+
background: rgba(0, 0, 0, 0.7);
|
|
501
|
+
display: flex;
|
|
502
|
+
align-items: center;
|
|
503
|
+
justify-content: center;
|
|
504
|
+
z-index: 1000;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
.sip-eth-stealth-qr-content {
|
|
508
|
+
background: white;
|
|
509
|
+
padding: 24px;
|
|
510
|
+
border-radius: 16px;
|
|
511
|
+
text-align: center;
|
|
512
|
+
max-width: 320px;
|
|
513
|
+
width: 90%;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.sip-eth-stealth-qr-title {
|
|
517
|
+
font-size: 16px;
|
|
518
|
+
font-weight: 600;
|
|
519
|
+
color: #111827;
|
|
520
|
+
margin-bottom: 16px;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.sip-eth-stealth-qr-code {
|
|
524
|
+
display: flex;
|
|
525
|
+
align-items: center;
|
|
526
|
+
justify-content: center;
|
|
527
|
+
width: 200px;
|
|
528
|
+
height: 200px;
|
|
529
|
+
margin: 0 auto 16px;
|
|
530
|
+
background: #f9fafb;
|
|
531
|
+
border-radius: 8px;
|
|
532
|
+
border: 1px solid #e5e7eb;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
.sip-eth-stealth-qr-address {
|
|
536
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
537
|
+
font-size: 11px;
|
|
538
|
+
color: #6b7280;
|
|
539
|
+
word-break: break-all;
|
|
540
|
+
margin-bottom: 16px;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.sip-eth-stealth-qr-close {
|
|
544
|
+
padding: 8px 24px;
|
|
545
|
+
background: #111827;
|
|
546
|
+
color: white;
|
|
547
|
+
border: none;
|
|
548
|
+
border-radius: 8px;
|
|
549
|
+
font-size: 14px;
|
|
550
|
+
font-weight: 500;
|
|
551
|
+
cursor: pointer;
|
|
552
|
+
transition: background 0.2s;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.sip-eth-stealth-qr-close:hover {
|
|
556
|
+
background: #374151;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.sip-eth-stealth-copy-success {
|
|
560
|
+
position: absolute;
|
|
561
|
+
top: -32px;
|
|
562
|
+
left: 50%;
|
|
563
|
+
transform: translateX(-50%);
|
|
564
|
+
padding: 4px 12px;
|
|
565
|
+
background: #22c55e;
|
|
566
|
+
color: white;
|
|
567
|
+
font-size: 12px;
|
|
568
|
+
font-weight: 500;
|
|
569
|
+
border-radius: 4px;
|
|
570
|
+
opacity: 0;
|
|
571
|
+
visibility: hidden;
|
|
572
|
+
transition: opacity 0.2s, visibility 0.2s;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.sip-eth-stealth-copy-success.visible {
|
|
576
|
+
opacity: 1;
|
|
577
|
+
visibility: visible;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/* Dark mode already active by default, light mode override */
|
|
581
|
+
@media (prefers-color-scheme: light) {
|
|
582
|
+
.sip-eth-stealth-container {
|
|
583
|
+
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
|
584
|
+
border-color: #e2e8f0;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.sip-eth-stealth-address-text {
|
|
588
|
+
color: #1e293b;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.sip-eth-stealth-action-btn {
|
|
592
|
+
color: #64748b;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.sip-eth-stealth-action-btn:hover {
|
|
596
|
+
background: rgba(0, 0, 0, 0.05);
|
|
597
|
+
color: #1e293b;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
`
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Ethereum diamond icon for stealth address indicator
|
|
604
|
+
*/
|
|
605
|
+
const EthereumIcon = () => (
|
|
606
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
607
|
+
<path d="M12 2L4 12l8 10 8-10L12 2z" />
|
|
608
|
+
<path d="M4 12l8 3.5L20 12" />
|
|
609
|
+
<path d="M12 2v13.5" />
|
|
610
|
+
</svg>
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Copy icon
|
|
615
|
+
*/
|
|
616
|
+
const CopyIcon = () => (
|
|
617
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
618
|
+
<rect x="9" y="9" width="13" height="13" rx="2" />
|
|
619
|
+
<path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1" />
|
|
620
|
+
</svg>
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* External link icon
|
|
625
|
+
*/
|
|
626
|
+
const ExternalLinkIcon = () => (
|
|
627
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
628
|
+
<path d="M18 13v6a2 2 0 01-2 2H5a2 2 0 01-2-2V8a2 2 0 012-2h6" />
|
|
629
|
+
<polyline points="15 3 21 3 21 9" />
|
|
630
|
+
<line x1="10" y1="14" x2="21" y2="3" />
|
|
631
|
+
</svg>
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* QR code icon
|
|
636
|
+
*/
|
|
637
|
+
const QrCodeIcon = () => (
|
|
638
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
639
|
+
<rect x="3" y="3" width="7" height="7" rx="1" />
|
|
640
|
+
<rect x="14" y="3" width="7" height="7" rx="1" />
|
|
641
|
+
<rect x="3" y="14" width="7" height="7" rx="1" />
|
|
642
|
+
<rect x="14" y="14" width="3" height="3" />
|
|
643
|
+
<rect x="18" y="14" width="3" height="3" />
|
|
644
|
+
<rect x="14" y="18" width="3" height="3" />
|
|
645
|
+
<rect x="18" y="18" width="3" height="3" />
|
|
646
|
+
</svg>
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Check icon
|
|
651
|
+
*/
|
|
652
|
+
const CheckIcon = () => (
|
|
653
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
654
|
+
<polyline points="20 6 9 17 4 12" />
|
|
655
|
+
</svg>
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* X icon
|
|
660
|
+
*/
|
|
661
|
+
const XIcon = () => (
|
|
662
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
663
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
664
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
665
|
+
</svg>
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* EthereumStealthAddressDisplay - Component for displaying EIP-5564 stealth addresses
|
|
670
|
+
*
|
|
671
|
+
* Displays stealth addresses with Ethereum-specific visual styling,
|
|
672
|
+
* including network support, Etherscan links, and EIP-5564 metadata.
|
|
673
|
+
*
|
|
674
|
+
* @example Basic usage
|
|
675
|
+
* ```tsx
|
|
676
|
+
* import { EthereumStealthAddressDisplay } from '@sip-protocol/react'
|
|
677
|
+
*
|
|
678
|
+
* function WalletView() {
|
|
679
|
+
* return (
|
|
680
|
+
* <EthereumStealthAddressDisplay
|
|
681
|
+
* address="0x742d35Cc6634C0532925a3b844Bc9e7595f2..."
|
|
682
|
+
* ownership="yours"
|
|
683
|
+
* />
|
|
684
|
+
* )
|
|
685
|
+
* }
|
|
686
|
+
* ```
|
|
687
|
+
*
|
|
688
|
+
* @example With EIP-5564 metadata
|
|
689
|
+
* ```tsx
|
|
690
|
+
* <EthereumStealthAddressDisplay
|
|
691
|
+
* address="0x742d35Cc6634C0532925a3b844Bc9e7595f2..."
|
|
692
|
+
* ephemeralPublicKey="0x02abc...123"
|
|
693
|
+
* viewTag={42}
|
|
694
|
+
* network="arbitrum"
|
|
695
|
+
* showEipBadge
|
|
696
|
+
* showViewTag
|
|
697
|
+
* />
|
|
698
|
+
* ```
|
|
699
|
+
*/
|
|
700
|
+
export function EthereumStealthAddressDisplay({
|
|
701
|
+
address,
|
|
702
|
+
metaAddress,
|
|
703
|
+
ephemeralPublicKey,
|
|
704
|
+
viewTag,
|
|
705
|
+
ownership = 'unknown',
|
|
706
|
+
isValid,
|
|
707
|
+
network = 'mainnet',
|
|
708
|
+
networkConfig,
|
|
709
|
+
showQrCode = true,
|
|
710
|
+
showExplorerLink = true,
|
|
711
|
+
showCopyButton = true,
|
|
712
|
+
showOwnership = true,
|
|
713
|
+
showValidation = true,
|
|
714
|
+
showNetworkBadge = true,
|
|
715
|
+
showEipBadge = true,
|
|
716
|
+
showViewTag = true,
|
|
717
|
+
className = '',
|
|
718
|
+
size = 'md',
|
|
719
|
+
onCopy,
|
|
720
|
+
onShowQr,
|
|
721
|
+
onCopyEphemeralKey,
|
|
722
|
+
}: EthereumStealthAddressDisplayProps) {
|
|
723
|
+
const [showCopySuccess, setShowCopySuccess] = useState(false)
|
|
724
|
+
const [showQrModal, setShowQrModal] = useState(false)
|
|
725
|
+
|
|
726
|
+
// Determine validation status
|
|
727
|
+
const validationStatus = useMemo(() => {
|
|
728
|
+
if (isValid !== undefined) return isValid
|
|
729
|
+
return isValidEthereumStealthAddress(address)
|
|
730
|
+
}, [address, isValid])
|
|
731
|
+
|
|
732
|
+
// Get network config
|
|
733
|
+
const config = useMemo(() => {
|
|
734
|
+
return networkConfig ?? ETHEREUM_STEALTH_NETWORKS[network]
|
|
735
|
+
}, [network, networkConfig])
|
|
736
|
+
|
|
737
|
+
// Truncate address for display based on size
|
|
738
|
+
const displayAddress = useMemo(() => {
|
|
739
|
+
const startChars = size === 'sm' ? 6 : size === 'lg' ? 10 : 8
|
|
740
|
+
const endChars = size === 'sm' ? 4 : size === 'lg' ? 6 : 4
|
|
741
|
+
return truncateEthereumAddress(address, startChars, endChars)
|
|
742
|
+
}, [address, size])
|
|
743
|
+
|
|
744
|
+
// Handle copy to clipboard
|
|
745
|
+
const handleCopy = useCallback(async () => {
|
|
746
|
+
try {
|
|
747
|
+
await navigator.clipboard.writeText(address)
|
|
748
|
+
setShowCopySuccess(true)
|
|
749
|
+
onCopy?.(address)
|
|
750
|
+
setTimeout(() => setShowCopySuccess(false), 2000)
|
|
751
|
+
} catch {
|
|
752
|
+
// Fallback for older browsers
|
|
753
|
+
const textArea = document.createElement('textarea')
|
|
754
|
+
textArea.value = address
|
|
755
|
+
document.body.appendChild(textArea)
|
|
756
|
+
textArea.select()
|
|
757
|
+
document.execCommand('copy')
|
|
758
|
+
document.body.removeChild(textArea)
|
|
759
|
+
setShowCopySuccess(true)
|
|
760
|
+
onCopy?.(address)
|
|
761
|
+
setTimeout(() => setShowCopySuccess(false), 2000)
|
|
762
|
+
}
|
|
763
|
+
}, [address, onCopy])
|
|
764
|
+
|
|
765
|
+
// Handle copy ephemeral key
|
|
766
|
+
const handleCopyEphemeralKey = useCallback(async () => {
|
|
767
|
+
if (!ephemeralPublicKey) return
|
|
768
|
+
try {
|
|
769
|
+
await navigator.clipboard.writeText(ephemeralPublicKey)
|
|
770
|
+
onCopyEphemeralKey?.(ephemeralPublicKey)
|
|
771
|
+
} catch {
|
|
772
|
+
const textArea = document.createElement('textarea')
|
|
773
|
+
textArea.value = ephemeralPublicKey
|
|
774
|
+
document.body.appendChild(textArea)
|
|
775
|
+
textArea.select()
|
|
776
|
+
document.execCommand('copy')
|
|
777
|
+
document.body.removeChild(textArea)
|
|
778
|
+
onCopyEphemeralKey?.(ephemeralPublicKey)
|
|
779
|
+
}
|
|
780
|
+
}, [ephemeralPublicKey, onCopyEphemeralKey])
|
|
781
|
+
|
|
782
|
+
// Handle show QR code
|
|
783
|
+
const handleShowQr = useCallback(() => {
|
|
784
|
+
setShowQrModal(true)
|
|
785
|
+
onShowQr?.(metaAddress ?? address)
|
|
786
|
+
}, [address, metaAddress, onShowQr])
|
|
787
|
+
|
|
788
|
+
// Handle close QR modal
|
|
789
|
+
const handleCloseQr = useCallback(() => {
|
|
790
|
+
setShowQrModal(false)
|
|
791
|
+
}, [])
|
|
792
|
+
|
|
793
|
+
// Get ownership label
|
|
794
|
+
const ownershipLabel = useMemo(() => {
|
|
795
|
+
switch (ownership) {
|
|
796
|
+
case 'yours':
|
|
797
|
+
return 'Your Address'
|
|
798
|
+
case 'others':
|
|
799
|
+
return "Recipient's Address"
|
|
800
|
+
case 'unknown':
|
|
801
|
+
default:
|
|
802
|
+
return 'Unknown'
|
|
803
|
+
}
|
|
804
|
+
}, [ownership])
|
|
805
|
+
|
|
806
|
+
// Explorer URL
|
|
807
|
+
const explorerUrl = useMemo(() => {
|
|
808
|
+
return `${config.explorerUrl}/${address}`
|
|
809
|
+
}, [config.explorerUrl, address])
|
|
810
|
+
|
|
811
|
+
// Truncate ephemeral key for display
|
|
812
|
+
const displayEphemeralKey = useMemo(() => {
|
|
813
|
+
if (!ephemeralPublicKey) return ''
|
|
814
|
+
return truncateEthereumAddress(ephemeralPublicKey, 8, 6)
|
|
815
|
+
}, [ephemeralPublicKey])
|
|
816
|
+
|
|
817
|
+
return (
|
|
818
|
+
<>
|
|
819
|
+
<style>{styles}</style>
|
|
820
|
+
|
|
821
|
+
<div
|
|
822
|
+
className={`sip-eth-stealth-address ${className}`}
|
|
823
|
+
data-testid="eth-stealth-address"
|
|
824
|
+
>
|
|
825
|
+
<div
|
|
826
|
+
className="sip-eth-stealth-container"
|
|
827
|
+
data-size={size}
|
|
828
|
+
data-ownership={ownership}
|
|
829
|
+
>
|
|
830
|
+
{/* Copy success message */}
|
|
831
|
+
<span
|
|
832
|
+
className={`sip-eth-stealth-copy-success ${showCopySuccess ? 'visible' : ''}`}
|
|
833
|
+
>
|
|
834
|
+
Copied!
|
|
835
|
+
</span>
|
|
836
|
+
|
|
837
|
+
{/* Ethereum icon badge */}
|
|
838
|
+
<div className="sip-eth-stealth-icon" title="EIP-5564 Stealth Address">
|
|
839
|
+
<EthereumIcon />
|
|
840
|
+
</div>
|
|
841
|
+
|
|
842
|
+
{/* Address text with full address tooltip */}
|
|
843
|
+
<span className="sip-eth-stealth-address-text" data-testid="address-text">
|
|
844
|
+
{displayAddress}
|
|
845
|
+
<span className="sip-eth-stealth-address-full">{address}</span>
|
|
846
|
+
</span>
|
|
847
|
+
|
|
848
|
+
{/* Action buttons */}
|
|
849
|
+
<div className="sip-eth-stealth-actions">
|
|
850
|
+
{showCopyButton && (
|
|
851
|
+
<button
|
|
852
|
+
type="button"
|
|
853
|
+
className="sip-eth-stealth-action-btn"
|
|
854
|
+
onClick={handleCopy}
|
|
855
|
+
title="Copy address"
|
|
856
|
+
aria-label="Copy address to clipboard"
|
|
857
|
+
>
|
|
858
|
+
<CopyIcon />
|
|
859
|
+
</button>
|
|
860
|
+
)}
|
|
861
|
+
|
|
862
|
+
{showExplorerLink && (
|
|
863
|
+
<a
|
|
864
|
+
href={explorerUrl}
|
|
865
|
+
target="_blank"
|
|
866
|
+
rel="noopener noreferrer"
|
|
867
|
+
className="sip-eth-stealth-action-btn"
|
|
868
|
+
title={`View on ${config.explorerName}`}
|
|
869
|
+
aria-label={`View on ${config.explorerName}`}
|
|
870
|
+
>
|
|
871
|
+
<ExternalLinkIcon />
|
|
872
|
+
</a>
|
|
873
|
+
)}
|
|
874
|
+
|
|
875
|
+
{showQrCode && (
|
|
876
|
+
<button
|
|
877
|
+
type="button"
|
|
878
|
+
className="sip-eth-stealth-action-btn"
|
|
879
|
+
onClick={handleShowQr}
|
|
880
|
+
title="Show QR code"
|
|
881
|
+
aria-label="Show QR code"
|
|
882
|
+
>
|
|
883
|
+
<QrCodeIcon />
|
|
884
|
+
</button>
|
|
885
|
+
)}
|
|
886
|
+
</div>
|
|
887
|
+
|
|
888
|
+
{/* Network badge */}
|
|
889
|
+
{showNetworkBadge && (
|
|
890
|
+
<span
|
|
891
|
+
className="sip-eth-stealth-network-badge"
|
|
892
|
+
data-l2={config.isL2.toString()}
|
|
893
|
+
style={{
|
|
894
|
+
borderColor: `${config.color}40`,
|
|
895
|
+
color: config.color,
|
|
896
|
+
background: `${config.color}20`,
|
|
897
|
+
}}
|
|
898
|
+
>
|
|
899
|
+
{config.name}
|
|
900
|
+
</span>
|
|
901
|
+
)}
|
|
902
|
+
|
|
903
|
+
{/* Ownership badge */}
|
|
904
|
+
{showOwnership && (
|
|
905
|
+
<span className="sip-eth-stealth-badge" data-ownership={ownership}>
|
|
906
|
+
{ownershipLabel}
|
|
907
|
+
</span>
|
|
908
|
+
)}
|
|
909
|
+
</div>
|
|
910
|
+
|
|
911
|
+
{/* Meta information row */}
|
|
912
|
+
<div className="sip-eth-stealth-meta">
|
|
913
|
+
{/* EIP-5564 badge */}
|
|
914
|
+
{showEipBadge && (
|
|
915
|
+
<span className="sip-eth-stealth-eip-badge">EIP-5564</span>
|
|
916
|
+
)}
|
|
917
|
+
|
|
918
|
+
{/* View tag */}
|
|
919
|
+
{showViewTag && viewTag !== undefined && (
|
|
920
|
+
<span className="sip-eth-stealth-view-tag">View Tag: {viewTag}</span>
|
|
921
|
+
)}
|
|
922
|
+
|
|
923
|
+
{/* Validation indicator */}
|
|
924
|
+
{showValidation && (
|
|
925
|
+
<div
|
|
926
|
+
className="sip-eth-stealth-validation"
|
|
927
|
+
data-valid={validationStatus}
|
|
928
|
+
aria-label={
|
|
929
|
+
validationStatus
|
|
930
|
+
? 'Valid EIP-5564 stealth address'
|
|
931
|
+
: 'Invalid address format'
|
|
932
|
+
}
|
|
933
|
+
>
|
|
934
|
+
{validationStatus ? <CheckIcon /> : <XIcon />}
|
|
935
|
+
<span>
|
|
936
|
+
{validationStatus ? 'Valid stealth address' : 'Invalid format'}
|
|
937
|
+
</span>
|
|
938
|
+
</div>
|
|
939
|
+
)}
|
|
940
|
+
</div>
|
|
941
|
+
|
|
942
|
+
{/* Ephemeral public key (if provided) */}
|
|
943
|
+
{ephemeralPublicKey && (
|
|
944
|
+
<div className="sip-eth-stealth-ephemeral">
|
|
945
|
+
<span className="sip-eth-stealth-ephemeral-label">Ephemeral Key:</span>
|
|
946
|
+
<span className="sip-eth-stealth-ephemeral-key">
|
|
947
|
+
{displayEphemeralKey}
|
|
948
|
+
</span>
|
|
949
|
+
<button
|
|
950
|
+
type="button"
|
|
951
|
+
className="sip-eth-stealth-action-btn"
|
|
952
|
+
onClick={handleCopyEphemeralKey}
|
|
953
|
+
title="Copy ephemeral key"
|
|
954
|
+
aria-label="Copy ephemeral public key"
|
|
955
|
+
style={{ width: 20, height: 20 }}
|
|
956
|
+
>
|
|
957
|
+
<CopyIcon />
|
|
958
|
+
</button>
|
|
959
|
+
</div>
|
|
960
|
+
)}
|
|
961
|
+
</div>
|
|
962
|
+
|
|
963
|
+
{/* QR Code Modal */}
|
|
964
|
+
{showQrModal && (
|
|
965
|
+
<div
|
|
966
|
+
className="sip-eth-stealth-qr-modal"
|
|
967
|
+
onClick={handleCloseQr}
|
|
968
|
+
role="dialog"
|
|
969
|
+
aria-modal="true"
|
|
970
|
+
aria-labelledby="eth-qr-modal-title"
|
|
971
|
+
>
|
|
972
|
+
<div
|
|
973
|
+
className="sip-eth-stealth-qr-content"
|
|
974
|
+
onClick={(e) => e.stopPropagation()}
|
|
975
|
+
>
|
|
976
|
+
<h3 id="eth-qr-modal-title" className="sip-eth-stealth-qr-title">
|
|
977
|
+
Scan to Receive ({config.name})
|
|
978
|
+
</h3>
|
|
979
|
+
<div className="sip-eth-stealth-qr-code" data-testid="qr-code-container">
|
|
980
|
+
{/* QR Code placeholder - can be enhanced with actual QR library */}
|
|
981
|
+
<QrCodeIcon />
|
|
982
|
+
</div>
|
|
983
|
+
<p className="sip-eth-stealth-qr-address">{metaAddress ?? address}</p>
|
|
984
|
+
<button
|
|
985
|
+
type="button"
|
|
986
|
+
className="sip-eth-stealth-qr-close"
|
|
987
|
+
onClick={handleCloseQr}
|
|
988
|
+
>
|
|
989
|
+
Close
|
|
990
|
+
</button>
|
|
991
|
+
</div>
|
|
992
|
+
</div>
|
|
993
|
+
)}
|
|
994
|
+
</>
|
|
995
|
+
)
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Hook to manage Ethereum stealth address display state
|
|
1000
|
+
*/
|
|
1001
|
+
export function useEthereumStealthAddressDisplay(
|
|
1002
|
+
address: string,
|
|
1003
|
+
options: {
|
|
1004
|
+
checkOwnership?: (address: string) => EthereumOwnershipStatus
|
|
1005
|
+
validateAddress?: (address: string) => boolean
|
|
1006
|
+
network?: EthereumStealthNetworkId
|
|
1007
|
+
} = {}
|
|
1008
|
+
) {
|
|
1009
|
+
const { checkOwnership, validateAddress, network = 'mainnet' } = options
|
|
1010
|
+
|
|
1011
|
+
const ownership = useMemo(() => {
|
|
1012
|
+
if (checkOwnership) {
|
|
1013
|
+
return checkOwnership(address)
|
|
1014
|
+
}
|
|
1015
|
+
return 'unknown' as EthereumOwnershipStatus
|
|
1016
|
+
}, [address, checkOwnership])
|
|
1017
|
+
|
|
1018
|
+
const isValid = useMemo(() => {
|
|
1019
|
+
if (validateAddress) {
|
|
1020
|
+
return validateAddress(address)
|
|
1021
|
+
}
|
|
1022
|
+
return isValidEthereumStealthAddress(address)
|
|
1023
|
+
}, [address, validateAddress])
|
|
1024
|
+
|
|
1025
|
+
const truncated = useMemo(() => truncateEthereumAddress(address), [address])
|
|
1026
|
+
|
|
1027
|
+
const networkConfig = useMemo(
|
|
1028
|
+
() => ETHEREUM_STEALTH_NETWORKS[network],
|
|
1029
|
+
[network]
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
const explorerUrl = useMemo(
|
|
1033
|
+
() => `${networkConfig.explorerUrl}/${address}`,
|
|
1034
|
+
[networkConfig.explorerUrl, address]
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
return {
|
|
1038
|
+
address,
|
|
1039
|
+
truncated,
|
|
1040
|
+
ownership,
|
|
1041
|
+
isValid,
|
|
1042
|
+
isStealth: isValidEthereumStealthAddress(address),
|
|
1043
|
+
network,
|
|
1044
|
+
networkConfig,
|
|
1045
|
+
explorerUrl,
|
|
1046
|
+
isL2: networkConfig.isL2,
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
export default EthereumStealthAddressDisplay
|