@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.
@@ -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