@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,822 @@
1
+ import React, { useState, useCallback, useId, useMemo } from 'react'
2
+
3
+ /**
4
+ * Privacy level options for Ethereum
5
+ */
6
+ export type EthereumPrivacyLevel = 'public' | 'stealth' | 'compliant'
7
+
8
+ /**
9
+ * Supported Ethereum networks
10
+ */
11
+ export type EthereumNetworkId =
12
+ | 'mainnet'
13
+ | 'arbitrum'
14
+ | 'optimism'
15
+ | 'base'
16
+ | 'polygon'
17
+ | 'sepolia'
18
+
19
+ /**
20
+ * Gas estimate for Ethereum privacy operations
21
+ */
22
+ export interface EthereumGasEstimate {
23
+ /** Gas units required */
24
+ gasUnits: bigint
25
+ /** Gas price in gwei */
26
+ gasPriceGwei: number
27
+ /** Total cost in ETH */
28
+ costEth: string
29
+ /** Approximate cost in USD */
30
+ costUsd?: string
31
+ /** L1 data cost (for L2s) */
32
+ l1DataCost?: string
33
+ }
34
+
35
+ /**
36
+ * Network-specific gas configuration
37
+ */
38
+ export interface NetworkGasConfig {
39
+ network: EthereumNetworkId
40
+ displayName: string
41
+ nativeSymbol: string
42
+ gasMultiplier: number
43
+ isL2: boolean
44
+ }
45
+
46
+ /**
47
+ * Privacy level metadata for Ethereum
48
+ */
49
+ export interface EthereumPrivacyLevelInfo {
50
+ level: EthereumPrivacyLevel
51
+ label: string
52
+ description: string
53
+ eipReference?: string
54
+ icon: React.ReactNode
55
+ gasEstimate?: EthereumGasEstimate
56
+ }
57
+
58
+ /**
59
+ * EthereumPrivacyToggle component props
60
+ */
61
+ export interface EthereumPrivacyToggleProps {
62
+ /** Current privacy level (controlled mode) */
63
+ value?: EthereumPrivacyLevel
64
+ /** Default privacy level (uncontrolled mode) */
65
+ defaultValue?: EthereumPrivacyLevel
66
+ /** Callback when privacy level changes */
67
+ onChange?: (level: EthereumPrivacyLevel) => void
68
+ /** Whether the toggle is disabled */
69
+ disabled?: boolean
70
+ /** Network for gas estimates */
71
+ network?: EthereumNetworkId
72
+ /** Custom gas estimates */
73
+ gasEstimates?: Partial<Record<EthereumPrivacyLevel, EthereumGasEstimate>>
74
+ /** Show gas/fee estimates */
75
+ showGasEstimate?: boolean
76
+ /** Show EIP references */
77
+ showEipReferences?: boolean
78
+ /** Show tooltips */
79
+ showTooltips?: boolean
80
+ /** Custom class name */
81
+ className?: string
82
+ /** Size variant */
83
+ size?: 'sm' | 'md' | 'lg'
84
+ /** Compact mode (icon only) */
85
+ compact?: boolean
86
+ /** Show L2 savings badge */
87
+ showL2Savings?: boolean
88
+ /** Aria label for the toggle group */
89
+ 'aria-label'?: string
90
+ }
91
+
92
+ /**
93
+ * Network configuration defaults
94
+ */
95
+ const NETWORK_CONFIGS: Record<EthereumNetworkId, NetworkGasConfig> = {
96
+ mainnet: {
97
+ network: 'mainnet',
98
+ displayName: 'Ethereum',
99
+ nativeSymbol: 'ETH',
100
+ gasMultiplier: 1,
101
+ isL2: false,
102
+ },
103
+ arbitrum: {
104
+ network: 'arbitrum',
105
+ displayName: 'Arbitrum',
106
+ nativeSymbol: 'ETH',
107
+ gasMultiplier: 0.01,
108
+ isL2: true,
109
+ },
110
+ optimism: {
111
+ network: 'optimism',
112
+ displayName: 'Optimism',
113
+ nativeSymbol: 'ETH',
114
+ gasMultiplier: 0.01,
115
+ isL2: true,
116
+ },
117
+ base: {
118
+ network: 'base',
119
+ displayName: 'Base',
120
+ nativeSymbol: 'ETH',
121
+ gasMultiplier: 0.005,
122
+ isL2: true,
123
+ },
124
+ polygon: {
125
+ network: 'polygon',
126
+ displayName: 'Polygon',
127
+ nativeSymbol: 'MATIC',
128
+ gasMultiplier: 0.02,
129
+ isL2: true,
130
+ },
131
+ sepolia: {
132
+ network: 'sepolia',
133
+ displayName: 'Sepolia',
134
+ nativeSymbol: 'ETH',
135
+ gasMultiplier: 0,
136
+ isL2: false,
137
+ },
138
+ }
139
+
140
+ /**
141
+ * Default gas estimates for privacy operations (mainnet)
142
+ */
143
+ const DEFAULT_GAS_ESTIMATES: Record<EthereumPrivacyLevel, { gasUnits: bigint; description: string }> = {
144
+ public: {
145
+ gasUnits: 21000n,
146
+ description: 'Standard ETH transfer',
147
+ },
148
+ stealth: {
149
+ gasUnits: 85000n,
150
+ description: 'Stealth address transfer (EIP-5564)',
151
+ },
152
+ compliant: {
153
+ gasUnits: 120000n,
154
+ description: 'Stealth + viewing key announcement',
155
+ },
156
+ }
157
+
158
+ /**
159
+ * Default privacy level descriptions for Ethereum
160
+ */
161
+ const DEFAULT_LEVEL_INFO: Record<EthereumPrivacyLevel, Omit<EthereumPrivacyLevelInfo, 'level' | 'gasEstimate'>> = {
162
+ public: {
163
+ label: 'Public',
164
+ description: 'Standard transaction. Sender, recipient, and amount visible on-chain.',
165
+ eipReference: undefined,
166
+ icon: (
167
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="sip-eth-privacy-icon">
168
+ <circle cx="12" cy="12" r="10" />
169
+ <path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
170
+ </svg>
171
+ ),
172
+ },
173
+ stealth: {
174
+ label: 'Stealth',
175
+ description: 'Full privacy using EIP-5564 stealth addresses. Recipient hidden, only they can discover.',
176
+ eipReference: 'EIP-5564',
177
+ icon: (
178
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="sip-eth-privacy-icon">
179
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
180
+ <path d="M12 11v4M12 7h.01" />
181
+ </svg>
182
+ ),
183
+ },
184
+ compliant: {
185
+ label: 'Compliant',
186
+ description: 'Privacy with audit trail. Stealth addresses + viewing keys for authorized disclosure.',
187
+ eipReference: 'EIP-5564 + EIP-6538',
188
+ icon: (
189
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="sip-eth-privacy-icon">
190
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
191
+ <path d="M9 12l2 2 4-4" />
192
+ </svg>
193
+ ),
194
+ },
195
+ }
196
+
197
+ /**
198
+ * CSS styles for the component
199
+ */
200
+ const styles = `
201
+ .sip-eth-privacy-toggle {
202
+ display: inline-flex;
203
+ flex-direction: column;
204
+ gap: 8px;
205
+ font-family: system-ui, -apple-system, sans-serif;
206
+ }
207
+
208
+ .sip-eth-privacy-header {
209
+ display: flex;
210
+ align-items: center;
211
+ justify-content: space-between;
212
+ gap: 8px;
213
+ font-size: 12px;
214
+ color: #6b7280;
215
+ }
216
+
217
+ .sip-eth-privacy-network-badge {
218
+ display: inline-flex;
219
+ align-items: center;
220
+ gap: 4px;
221
+ padding: 2px 8px;
222
+ background: #e5e7eb;
223
+ border-radius: 4px;
224
+ font-size: 11px;
225
+ font-weight: 500;
226
+ }
227
+
228
+ .sip-eth-privacy-network-badge[data-l2="true"] {
229
+ background: #dbeafe;
230
+ color: #1d4ed8;
231
+ }
232
+
233
+ .sip-eth-privacy-toggle-group {
234
+ display: inline-flex;
235
+ background: #f3f4f6;
236
+ border-radius: 12px;
237
+ padding: 4px;
238
+ gap: 4px;
239
+ }
240
+
241
+ .sip-eth-privacy-toggle-group[data-size="sm"] {
242
+ padding: 2px;
243
+ gap: 2px;
244
+ border-radius: 8px;
245
+ }
246
+
247
+ .sip-eth-privacy-toggle-group[data-size="lg"] {
248
+ padding: 6px;
249
+ gap: 6px;
250
+ border-radius: 16px;
251
+ }
252
+
253
+ .sip-eth-privacy-option {
254
+ position: relative;
255
+ display: flex;
256
+ align-items: center;
257
+ gap: 6px;
258
+ padding: 8px 16px;
259
+ border: none;
260
+ background: transparent;
261
+ border-radius: 8px;
262
+ cursor: pointer;
263
+ font-size: 14px;
264
+ font-weight: 500;
265
+ color: #6b7280;
266
+ transition: all 0.2s ease;
267
+ outline: none;
268
+ }
269
+
270
+ .sip-eth-privacy-option[data-compact="true"] {
271
+ padding: 8px;
272
+ }
273
+
274
+ .sip-eth-privacy-option[data-size="sm"] {
275
+ padding: 6px 12px;
276
+ font-size: 12px;
277
+ border-radius: 6px;
278
+ }
279
+
280
+ .sip-eth-privacy-option[data-size="lg"] {
281
+ padding: 12px 24px;
282
+ font-size: 16px;
283
+ border-radius: 12px;
284
+ }
285
+
286
+ .sip-eth-privacy-option:hover:not(:disabled) {
287
+ color: #374151;
288
+ background: rgba(0, 0, 0, 0.05);
289
+ }
290
+
291
+ .sip-eth-privacy-option:focus-visible {
292
+ box-shadow: 0 0 0 2px #3b82f6;
293
+ }
294
+
295
+ .sip-eth-privacy-option[aria-checked="true"] {
296
+ background: white;
297
+ color: #111827;
298
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
299
+ }
300
+
301
+ .sip-eth-privacy-option[aria-checked="true"][data-level="public"] {
302
+ color: #6b7280;
303
+ }
304
+
305
+ .sip-eth-privacy-option[aria-checked="true"][data-level="stealth"] {
306
+ color: #7c3aed;
307
+ background: linear-gradient(135deg, #faf5ff 0%, #f3e8ff 100%);
308
+ }
309
+
310
+ .sip-eth-privacy-option[aria-checked="true"][data-level="compliant"] {
311
+ color: #059669;
312
+ background: linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%);
313
+ }
314
+
315
+ .sip-eth-privacy-option:disabled {
316
+ opacity: 0.5;
317
+ cursor: not-allowed;
318
+ }
319
+
320
+ .sip-eth-privacy-icon {
321
+ width: 16px;
322
+ height: 16px;
323
+ flex-shrink: 0;
324
+ }
325
+
326
+ .sip-eth-privacy-option[data-size="sm"] .sip-eth-privacy-icon {
327
+ width: 14px;
328
+ height: 14px;
329
+ }
330
+
331
+ .sip-eth-privacy-option[data-size="lg"] .sip-eth-privacy-icon {
332
+ width: 20px;
333
+ height: 20px;
334
+ }
335
+
336
+ .sip-eth-privacy-eip-badge {
337
+ display: inline-flex;
338
+ padding: 1px 4px;
339
+ background: #e0e7ff;
340
+ color: #4338ca;
341
+ font-size: 9px;
342
+ font-weight: 600;
343
+ border-radius: 2px;
344
+ margin-left: 4px;
345
+ }
346
+
347
+ .sip-eth-privacy-tooltip {
348
+ position: absolute;
349
+ bottom: calc(100% + 8px);
350
+ left: 50%;
351
+ transform: translateX(-50%);
352
+ padding: 8px 12px;
353
+ background: #1f2937;
354
+ color: white;
355
+ font-size: 12px;
356
+ font-weight: 400;
357
+ border-radius: 6px;
358
+ white-space: nowrap;
359
+ max-width: 280px;
360
+ white-space: normal;
361
+ text-align: center;
362
+ opacity: 0;
363
+ visibility: hidden;
364
+ transition: opacity 0.2s, visibility 0.2s;
365
+ z-index: 10;
366
+ pointer-events: none;
367
+ }
368
+
369
+ .sip-eth-privacy-tooltip::after {
370
+ content: '';
371
+ position: absolute;
372
+ top: 100%;
373
+ left: 50%;
374
+ transform: translateX(-50%);
375
+ border: 6px solid transparent;
376
+ border-top-color: #1f2937;
377
+ }
378
+
379
+ .sip-eth-privacy-option:hover .sip-eth-privacy-tooltip,
380
+ .sip-eth-privacy-option:focus .sip-eth-privacy-tooltip {
381
+ opacity: 1;
382
+ visibility: visible;
383
+ }
384
+
385
+ .sip-eth-privacy-gas-info {
386
+ display: flex;
387
+ align-items: center;
388
+ flex-wrap: wrap;
389
+ gap: 8px;
390
+ font-size: 12px;
391
+ color: #6b7280;
392
+ padding: 0 4px;
393
+ }
394
+
395
+ .sip-eth-privacy-gas-badge {
396
+ display: inline-flex;
397
+ align-items: center;
398
+ gap: 4px;
399
+ padding: 3px 8px;
400
+ background: #f3f4f6;
401
+ border-radius: 4px;
402
+ font-size: 11px;
403
+ font-weight: 500;
404
+ }
405
+
406
+ .sip-eth-privacy-gas-badge[data-level="stealth"],
407
+ .sip-eth-privacy-gas-badge[data-level="compliant"] {
408
+ background: #fef3c7;
409
+ color: #92400e;
410
+ }
411
+
412
+ .sip-eth-privacy-l2-savings {
413
+ display: inline-flex;
414
+ align-items: center;
415
+ gap: 4px;
416
+ padding: 2px 6px;
417
+ background: #d1fae5;
418
+ color: #047857;
419
+ font-size: 10px;
420
+ font-weight: 600;
421
+ border-radius: 3px;
422
+ }
423
+
424
+ .sip-eth-privacy-gas-units {
425
+ color: #9ca3af;
426
+ font-size: 10px;
427
+ }
428
+
429
+ @keyframes sip-eth-privacy-pulse {
430
+ 0%, 100% { transform: scale(1); }
431
+ 50% { transform: scale(1.03); }
432
+ }
433
+
434
+ .sip-eth-privacy-option[aria-checked="true"] {
435
+ animation: sip-eth-privacy-pulse 0.3s ease;
436
+ }
437
+
438
+ /* Dark mode support */
439
+ @media (prefers-color-scheme: dark) {
440
+ .sip-eth-privacy-toggle-group {
441
+ background: #374151;
442
+ }
443
+
444
+ .sip-eth-privacy-header {
445
+ color: #9ca3af;
446
+ }
447
+
448
+ .sip-eth-privacy-network-badge {
449
+ background: #4b5563;
450
+ color: #d1d5db;
451
+ }
452
+
453
+ .sip-eth-privacy-network-badge[data-l2="true"] {
454
+ background: #1e3a5f;
455
+ color: #93c5fd;
456
+ }
457
+
458
+ .sip-eth-privacy-option {
459
+ color: #9ca3af;
460
+ }
461
+
462
+ .sip-eth-privacy-option:hover:not(:disabled) {
463
+ color: #e5e7eb;
464
+ background: rgba(255, 255, 255, 0.05);
465
+ }
466
+
467
+ .sip-eth-privacy-option[aria-checked="true"] {
468
+ background: #4b5563;
469
+ color: #f9fafb;
470
+ }
471
+
472
+ .sip-eth-privacy-option[aria-checked="true"][data-level="stealth"] {
473
+ background: linear-gradient(135deg, #2e1065 0%, #4c1d95 100%);
474
+ color: #c4b5fd;
475
+ }
476
+
477
+ .sip-eth-privacy-option[aria-checked="true"][data-level="compliant"] {
478
+ background: linear-gradient(135deg, #064e3b 0%, #065f46 100%);
479
+ color: #6ee7b7;
480
+ }
481
+
482
+ .sip-eth-privacy-gas-info {
483
+ color: #9ca3af;
484
+ }
485
+
486
+ .sip-eth-privacy-gas-badge {
487
+ background: #374151;
488
+ color: #d1d5db;
489
+ }
490
+
491
+ .sip-eth-privacy-eip-badge {
492
+ background: #312e81;
493
+ color: #a5b4fc;
494
+ }
495
+ }
496
+ `
497
+
498
+ /**
499
+ * Calculate gas estimate for a given network and level
500
+ */
501
+ function calculateGasEstimate(
502
+ level: EthereumPrivacyLevel,
503
+ network: EthereumNetworkId,
504
+ gasPriceGwei: number = 30
505
+ ): EthereumGasEstimate {
506
+ const config = NETWORK_CONFIGS[network]
507
+ const baseEstimate = DEFAULT_GAS_ESTIMATES[level]
508
+
509
+ const gasUnits = baseEstimate.gasUnits
510
+ const effectiveGasPrice = gasPriceGwei * config.gasMultiplier || gasPriceGwei
511
+
512
+ // Cost in ETH = gasUnits * gasPriceGwei / 1e9
513
+ const costWei = gasUnits * BigInt(Math.floor(effectiveGasPrice * 1e9))
514
+ const costEth = Number(costWei) / 1e18
515
+
516
+ return {
517
+ gasUnits,
518
+ gasPriceGwei: effectiveGasPrice,
519
+ costEth: costEth.toFixed(6),
520
+ costUsd: undefined, // Would need price feed
521
+ l1DataCost: config.isL2 ? '~$0.01' : undefined,
522
+ }
523
+ }
524
+
525
+ /**
526
+ * EthereumPrivacyToggle - Toggle for selecting Ethereum privacy level
527
+ *
528
+ * A three-state toggle for EIP-5564 stealth address privacy on Ethereum and L2s.
529
+ * Shows gas estimates, L2 savings, and EIP references.
530
+ *
531
+ * @example Basic usage
532
+ * ```tsx
533
+ * import { EthereumPrivacyToggle } from '@sip-protocol/react'
534
+ *
535
+ * function SendForm() {
536
+ * const [privacy, setPrivacy] = useState<EthereumPrivacyLevel>('stealth')
537
+ *
538
+ * return (
539
+ * <EthereumPrivacyToggle
540
+ * value={privacy}
541
+ * onChange={setPrivacy}
542
+ * network="base"
543
+ * />
544
+ * )
545
+ * }
546
+ * ```
547
+ *
548
+ * @example With gas estimates and L2 savings
549
+ * ```tsx
550
+ * <EthereumPrivacyToggle
551
+ * value={privacy}
552
+ * onChange={setPrivacy}
553
+ * network="arbitrum"
554
+ * showGasEstimate
555
+ * showL2Savings
556
+ * />
557
+ * ```
558
+ *
559
+ * @example Compact mode for toolbars
560
+ * ```tsx
561
+ * <EthereumPrivacyToggle
562
+ * value={privacy}
563
+ * onChange={setPrivacy}
564
+ * compact
565
+ * size="sm"
566
+ * />
567
+ * ```
568
+ */
569
+ export function EthereumPrivacyToggle({
570
+ value,
571
+ defaultValue = 'stealth',
572
+ onChange,
573
+ disabled = false,
574
+ network = 'mainnet',
575
+ gasEstimates,
576
+ showGasEstimate = false,
577
+ showEipReferences = true,
578
+ showTooltips = true,
579
+ className = '',
580
+ size = 'md',
581
+ compact = false,
582
+ showL2Savings = false,
583
+ 'aria-label': ariaLabel = 'Ethereum privacy level',
584
+ }: EthereumPrivacyToggleProps) {
585
+ const baseId = useId()
586
+
587
+ // Internal state for uncontrolled mode
588
+ const [internalValue, setInternalValue] = useState<EthereumPrivacyLevel>(defaultValue)
589
+
590
+ // Determine if controlled or uncontrolled
591
+ const isControlled = value !== undefined
592
+ const currentValue = isControlled ? value : internalValue
593
+
594
+ // Get network config
595
+ const networkConfig = NETWORK_CONFIGS[network]
596
+
597
+ // Calculate gas estimates
598
+ const computedGasEstimates = useMemo(() => {
599
+ if (gasEstimates) return gasEstimates
600
+
601
+ return {
602
+ public: calculateGasEstimate('public', network),
603
+ stealth: calculateGasEstimate('stealth', network),
604
+ compliant: calculateGasEstimate('compliant', network),
605
+ }
606
+ }, [gasEstimates, network])
607
+
608
+ // Handle selection change
609
+ const handleSelect = useCallback(
610
+ (level: EthereumPrivacyLevel) => {
611
+ if (disabled) return
612
+
613
+ if (!isControlled) {
614
+ setInternalValue(level)
615
+ }
616
+
617
+ onChange?.(level)
618
+ },
619
+ [disabled, isControlled, onChange]
620
+ )
621
+
622
+ // Handle keyboard navigation
623
+ const handleKeyDown = useCallback(
624
+ (event: React.KeyboardEvent, currentLevel: EthereumPrivacyLevel) => {
625
+ const levels: EthereumPrivacyLevel[] = ['public', 'stealth', 'compliant']
626
+ const currentIndex = levels.indexOf(currentLevel)
627
+
628
+ let newIndex = currentIndex
629
+
630
+ switch (event.key) {
631
+ case 'ArrowRight':
632
+ case 'ArrowDown':
633
+ event.preventDefault()
634
+ newIndex = (currentIndex + 1) % levels.length
635
+ break
636
+ case 'ArrowLeft':
637
+ case 'ArrowUp':
638
+ event.preventDefault()
639
+ newIndex = (currentIndex - 1 + levels.length) % levels.length
640
+ break
641
+ case 'Home':
642
+ event.preventDefault()
643
+ newIndex = 0
644
+ break
645
+ case 'End':
646
+ event.preventDefault()
647
+ newIndex = levels.length - 1
648
+ break
649
+ default:
650
+ return
651
+ }
652
+
653
+ handleSelect(levels[newIndex])
654
+ },
655
+ [handleSelect]
656
+ )
657
+
658
+ // Build level info with gas estimates
659
+ const levelInfos: EthereumPrivacyLevelInfo[] = useMemo(
660
+ () =>
661
+ (['public', 'stealth', 'compliant'] as EthereumPrivacyLevel[]).map((level) => ({
662
+ level,
663
+ ...DEFAULT_LEVEL_INFO[level],
664
+ gasEstimate: computedGasEstimates?.[level],
665
+ })),
666
+ [computedGasEstimates]
667
+ )
668
+
669
+ // Calculate L2 savings percentage
670
+ const l2SavingsPercent = useMemo(() => {
671
+ if (!networkConfig.isL2) return null
672
+ const savings = Math.round((1 - networkConfig.gasMultiplier) * 100)
673
+ return savings > 0 ? savings : null
674
+ }, [networkConfig])
675
+
676
+ return (
677
+ <>
678
+ <style>{styles}</style>
679
+
680
+ <div
681
+ className={`sip-eth-privacy-toggle ${className}`}
682
+ data-network={network}
683
+ >
684
+ {/* Header with network info */}
685
+ <div className="sip-eth-privacy-header">
686
+ <span>Privacy Level</span>
687
+ <span
688
+ className="sip-eth-privacy-network-badge"
689
+ data-l2={networkConfig.isL2}
690
+ >
691
+ {networkConfig.displayName}
692
+ {showL2Savings && l2SavingsPercent && (
693
+ <span className="sip-eth-privacy-l2-savings">
694
+ {l2SavingsPercent}% cheaper
695
+ </span>
696
+ )}
697
+ </span>
698
+ </div>
699
+
700
+ {/* Toggle group */}
701
+ <div
702
+ role="radiogroup"
703
+ aria-label={ariaLabel}
704
+ className="sip-eth-privacy-toggle-group"
705
+ data-size={size}
706
+ >
707
+ {levelInfos.map((info) => {
708
+ const isSelected = currentValue === info.level
709
+ const optionId = `${baseId}-${info.level}`
710
+
711
+ return (
712
+ <button
713
+ key={info.level}
714
+ id={optionId}
715
+ role="radio"
716
+ aria-checked={isSelected}
717
+ aria-label={`${info.label}: ${info.description}`}
718
+ disabled={disabled}
719
+ onClick={() => handleSelect(info.level)}
720
+ onKeyDown={(e) => handleKeyDown(e, info.level)}
721
+ tabIndex={isSelected ? 0 : -1}
722
+ className="sip-eth-privacy-option"
723
+ data-level={info.level}
724
+ data-size={size}
725
+ data-compact={compact}
726
+ >
727
+ {info.icon}
728
+ {!compact && (
729
+ <>
730
+ <span>{info.label}</span>
731
+ {showEipReferences && info.eipReference && (
732
+ <span className="sip-eth-privacy-eip-badge">
733
+ {info.eipReference}
734
+ </span>
735
+ )}
736
+ </>
737
+ )}
738
+
739
+ {/* Tooltip */}
740
+ {showTooltips && (
741
+ <span className="sip-eth-privacy-tooltip" role="tooltip">
742
+ {info.description}
743
+ {info.eipReference && (
744
+ <span style={{ display: 'block', marginTop: 4, opacity: 0.7 }}>
745
+ Standard: {info.eipReference}
746
+ </span>
747
+ )}
748
+ </span>
749
+ )}
750
+ </button>
751
+ )
752
+ })}
753
+ </div>
754
+
755
+ {/* Gas estimate info */}
756
+ {showGasEstimate && computedGasEstimates && (
757
+ <div className="sip-eth-privacy-gas-info" aria-live="polite">
758
+ <span>Est. gas:</span>
759
+ <span
760
+ className="sip-eth-privacy-gas-badge"
761
+ data-level={currentValue}
762
+ >
763
+ ~{computedGasEstimates[currentValue]?.costEth ?? '?'} {networkConfig.nativeSymbol}
764
+ {computedGasEstimates[currentValue]?.costUsd && (
765
+ <span> (~${computedGasEstimates[currentValue]?.costUsd})</span>
766
+ )}
767
+ </span>
768
+ <span className="sip-eth-privacy-gas-units">
769
+ {computedGasEstimates[currentValue]?.gasUnits?.toString() ?? '?'} units
770
+ </span>
771
+ {networkConfig.isL2 && computedGasEstimates[currentValue]?.l1DataCost && (
772
+ <span style={{ fontSize: 10, color: '#9ca3af' }}>
773
+ +{computedGasEstimates[currentValue]?.l1DataCost} L1 data
774
+ </span>
775
+ )}
776
+ </div>
777
+ )}
778
+ </div>
779
+ </>
780
+ )
781
+ }
782
+
783
+ /**
784
+ * Hook to use with EthereumPrivacyToggle for managing privacy state
785
+ */
786
+ export function useEthereumPrivacyToggle(initialValue: EthereumPrivacyLevel = 'stealth') {
787
+ const [privacyLevel, setPrivacyLevel] = useState<EthereumPrivacyLevel>(initialValue)
788
+
789
+ const isPrivate = privacyLevel !== 'public'
790
+ const isCompliant = privacyLevel === 'compliant'
791
+ const isStealth = privacyLevel === 'stealth'
792
+
793
+ const setPublic = useCallback(() => setPrivacyLevel('public'), [])
794
+ const setStealth = useCallback(() => setPrivacyLevel('stealth'), [])
795
+ const setCompliant = useCallback(() => setPrivacyLevel('compliant'), [])
796
+
797
+ // Get EIP standard for current level
798
+ const eipStandard = useMemo(() => {
799
+ switch (privacyLevel) {
800
+ case 'stealth':
801
+ return 'EIP-5564'
802
+ case 'compliant':
803
+ return 'EIP-5564 + EIP-6538'
804
+ default:
805
+ return null
806
+ }
807
+ }, [privacyLevel])
808
+
809
+ return {
810
+ privacyLevel,
811
+ setPrivacyLevel,
812
+ isPrivate,
813
+ isCompliant,
814
+ isStealth,
815
+ setPublic,
816
+ setStealth,
817
+ setCompliant,
818
+ eipStandard,
819
+ }
820
+ }
821
+
822
+ export default EthereumPrivacyToggle