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