@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,548 @@
|
|
|
1
|
+
import React, { useState, useCallback, useId, useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Privacy level options
|
|
5
|
+
*/
|
|
6
|
+
export type PrivacyLevel = 'off' | 'shielded' | 'compliant'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Gas estimate for different privacy levels
|
|
10
|
+
*/
|
|
11
|
+
export interface GasEstimate {
|
|
12
|
+
/** Gas in native units (e.g., yoctoNEAR, gwei) */
|
|
13
|
+
gas: string
|
|
14
|
+
/** Formatted cost in native token */
|
|
15
|
+
cost: string
|
|
16
|
+
/** Formatted cost in USD (optional) */
|
|
17
|
+
costUsd?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Privacy level metadata
|
|
22
|
+
*/
|
|
23
|
+
export interface PrivacyLevelInfo {
|
|
24
|
+
level: PrivacyLevel
|
|
25
|
+
label: string
|
|
26
|
+
description: string
|
|
27
|
+
icon: React.ReactNode
|
|
28
|
+
gasEstimate?: GasEstimate
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* PrivacyToggle component props
|
|
33
|
+
*/
|
|
34
|
+
export interface PrivacyToggleProps {
|
|
35
|
+
/** Current privacy level (controlled mode) */
|
|
36
|
+
value?: PrivacyLevel
|
|
37
|
+
/** Default privacy level (uncontrolled mode) */
|
|
38
|
+
defaultValue?: PrivacyLevel
|
|
39
|
+
/** Callback when privacy level changes */
|
|
40
|
+
onChange?: (level: PrivacyLevel) => void
|
|
41
|
+
/** Whether the toggle is disabled */
|
|
42
|
+
disabled?: boolean
|
|
43
|
+
/** Gas estimates for each level */
|
|
44
|
+
gasEstimates?: Partial<Record<PrivacyLevel, GasEstimate>>
|
|
45
|
+
/** Show gas/fee difference */
|
|
46
|
+
showGasEstimate?: boolean
|
|
47
|
+
/** Show tooltips */
|
|
48
|
+
showTooltips?: boolean
|
|
49
|
+
/** Custom class name */
|
|
50
|
+
className?: string
|
|
51
|
+
/** Size variant */
|
|
52
|
+
size?: 'sm' | 'md' | 'lg'
|
|
53
|
+
/** Chain identifier for context */
|
|
54
|
+
chain?: string
|
|
55
|
+
/** Aria label for the toggle group */
|
|
56
|
+
'aria-label'?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Default privacy level descriptions
|
|
61
|
+
*/
|
|
62
|
+
const DEFAULT_LEVEL_INFO: Record<PrivacyLevel, Omit<PrivacyLevelInfo, 'level' | 'gasEstimate'>> = {
|
|
63
|
+
off: {
|
|
64
|
+
label: 'Public',
|
|
65
|
+
description: 'Transaction details are fully visible on-chain. Sender, recipient, and amount are public.',
|
|
66
|
+
icon: (
|
|
67
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="sip-privacy-icon">
|
|
68
|
+
<circle cx="12" cy="12" r="10" />
|
|
69
|
+
<path d="M12 6v6l4 2" />
|
|
70
|
+
</svg>
|
|
71
|
+
),
|
|
72
|
+
},
|
|
73
|
+
shielded: {
|
|
74
|
+
label: 'Shielded',
|
|
75
|
+
description: 'Full privacy. Sender, recipient, and amount are hidden using stealth addresses and commitments.',
|
|
76
|
+
icon: (
|
|
77
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="sip-privacy-icon">
|
|
78
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
79
|
+
</svg>
|
|
80
|
+
),
|
|
81
|
+
},
|
|
82
|
+
compliant: {
|
|
83
|
+
label: 'Compliant',
|
|
84
|
+
description: 'Privacy with viewing keys. Transaction is shielded but can be audited by authorized parties.',
|
|
85
|
+
icon: (
|
|
86
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="sip-privacy-icon">
|
|
87
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
88
|
+
<path d="M9 12l2 2 4-4" />
|
|
89
|
+
</svg>
|
|
90
|
+
),
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* CSS styles for the component (can be overridden via className)
|
|
96
|
+
*/
|
|
97
|
+
const styles = `
|
|
98
|
+
.sip-privacy-toggle {
|
|
99
|
+
display: inline-flex;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
gap: 8px;
|
|
102
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.sip-privacy-toggle-group {
|
|
106
|
+
display: inline-flex;
|
|
107
|
+
background: #f3f4f6;
|
|
108
|
+
border-radius: 12px;
|
|
109
|
+
padding: 4px;
|
|
110
|
+
gap: 4px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.sip-privacy-toggle-group[data-size="sm"] {
|
|
114
|
+
padding: 2px;
|
|
115
|
+
gap: 2px;
|
|
116
|
+
border-radius: 8px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.sip-privacy-toggle-group[data-size="lg"] {
|
|
120
|
+
padding: 6px;
|
|
121
|
+
gap: 6px;
|
|
122
|
+
border-radius: 16px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.sip-privacy-option {
|
|
126
|
+
position: relative;
|
|
127
|
+
display: flex;
|
|
128
|
+
align-items: center;
|
|
129
|
+
gap: 6px;
|
|
130
|
+
padding: 8px 16px;
|
|
131
|
+
border: none;
|
|
132
|
+
background: transparent;
|
|
133
|
+
border-radius: 8px;
|
|
134
|
+
cursor: pointer;
|
|
135
|
+
font-size: 14px;
|
|
136
|
+
font-weight: 500;
|
|
137
|
+
color: #6b7280;
|
|
138
|
+
transition: all 0.2s ease;
|
|
139
|
+
outline: none;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.sip-privacy-option[data-size="sm"] {
|
|
143
|
+
padding: 6px 12px;
|
|
144
|
+
font-size: 12px;
|
|
145
|
+
border-radius: 6px;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.sip-privacy-option[data-size="lg"] {
|
|
149
|
+
padding: 12px 24px;
|
|
150
|
+
font-size: 16px;
|
|
151
|
+
border-radius: 12px;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.sip-privacy-option:hover:not(:disabled) {
|
|
155
|
+
color: #374151;
|
|
156
|
+
background: rgba(0, 0, 0, 0.05);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.sip-privacy-option:focus-visible {
|
|
160
|
+
box-shadow: 0 0 0 2px #3b82f6;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.sip-privacy-option[aria-checked="true"] {
|
|
164
|
+
background: white;
|
|
165
|
+
color: #111827;
|
|
166
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.sip-privacy-option[aria-checked="true"][data-level="off"] {
|
|
170
|
+
color: #6b7280;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.sip-privacy-option[aria-checked="true"][data-level="shielded"] {
|
|
174
|
+
color: #059669;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.sip-privacy-option[aria-checked="true"][data-level="compliant"] {
|
|
178
|
+
color: #2563eb;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.sip-privacy-option:disabled {
|
|
182
|
+
opacity: 0.5;
|
|
183
|
+
cursor: not-allowed;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.sip-privacy-icon {
|
|
187
|
+
width: 16px;
|
|
188
|
+
height: 16px;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.sip-privacy-option[data-size="sm"] .sip-privacy-icon {
|
|
192
|
+
width: 14px;
|
|
193
|
+
height: 14px;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.sip-privacy-option[data-size="lg"] .sip-privacy-icon {
|
|
197
|
+
width: 20px;
|
|
198
|
+
height: 20px;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.sip-privacy-tooltip {
|
|
202
|
+
position: absolute;
|
|
203
|
+
bottom: calc(100% + 8px);
|
|
204
|
+
left: 50%;
|
|
205
|
+
transform: translateX(-50%);
|
|
206
|
+
padding: 8px 12px;
|
|
207
|
+
background: #1f2937;
|
|
208
|
+
color: white;
|
|
209
|
+
font-size: 12px;
|
|
210
|
+
font-weight: 400;
|
|
211
|
+
border-radius: 6px;
|
|
212
|
+
white-space: nowrap;
|
|
213
|
+
max-width: 250px;
|
|
214
|
+
white-space: normal;
|
|
215
|
+
text-align: center;
|
|
216
|
+
opacity: 0;
|
|
217
|
+
visibility: hidden;
|
|
218
|
+
transition: opacity 0.2s, visibility 0.2s;
|
|
219
|
+
z-index: 10;
|
|
220
|
+
pointer-events: none;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.sip-privacy-tooltip::after {
|
|
224
|
+
content: '';
|
|
225
|
+
position: absolute;
|
|
226
|
+
top: 100%;
|
|
227
|
+
left: 50%;
|
|
228
|
+
transform: translateX(-50%);
|
|
229
|
+
border: 6px solid transparent;
|
|
230
|
+
border-top-color: #1f2937;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.sip-privacy-option:hover .sip-privacy-tooltip,
|
|
234
|
+
.sip-privacy-option:focus .sip-privacy-tooltip {
|
|
235
|
+
opacity: 1;
|
|
236
|
+
visibility: visible;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.sip-privacy-gas-info {
|
|
240
|
+
display: flex;
|
|
241
|
+
align-items: center;
|
|
242
|
+
gap: 8px;
|
|
243
|
+
font-size: 12px;
|
|
244
|
+
color: #6b7280;
|
|
245
|
+
padding: 0 4px;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.sip-privacy-gas-badge {
|
|
249
|
+
display: inline-flex;
|
|
250
|
+
align-items: center;
|
|
251
|
+
gap: 4px;
|
|
252
|
+
padding: 2px 8px;
|
|
253
|
+
background: #f3f4f6;
|
|
254
|
+
border-radius: 4px;
|
|
255
|
+
font-size: 11px;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.sip-privacy-gas-badge[data-level="shielded"],
|
|
259
|
+
.sip-privacy-gas-badge[data-level="compliant"] {
|
|
260
|
+
background: #fef3c7;
|
|
261
|
+
color: #92400e;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@keyframes sip-privacy-pulse {
|
|
265
|
+
0%, 100% { transform: scale(1); }
|
|
266
|
+
50% { transform: scale(1.05); }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.sip-privacy-option[aria-checked="true"] {
|
|
270
|
+
animation: sip-privacy-pulse 0.3s ease;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* Dark mode support */
|
|
274
|
+
@media (prefers-color-scheme: dark) {
|
|
275
|
+
.sip-privacy-toggle-group {
|
|
276
|
+
background: #374151;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.sip-privacy-option {
|
|
280
|
+
color: #9ca3af;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.sip-privacy-option:hover:not(:disabled) {
|
|
284
|
+
color: #e5e7eb;
|
|
285
|
+
background: rgba(255, 255, 255, 0.05);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.sip-privacy-option[aria-checked="true"] {
|
|
289
|
+
background: #4b5563;
|
|
290
|
+
color: #f9fafb;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.sip-privacy-gas-info {
|
|
294
|
+
color: #9ca3af;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.sip-privacy-gas-badge {
|
|
298
|
+
background: #374151;
|
|
299
|
+
color: #d1d5db;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
`
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* PrivacyToggle - Toggle component for selecting privacy level
|
|
306
|
+
*
|
|
307
|
+
* A three-state toggle for controlling transaction privacy on NEAR and other chains.
|
|
308
|
+
* Supports controlled and uncontrolled modes, accessibility features, and gas estimates.
|
|
309
|
+
*
|
|
310
|
+
* @example Basic usage
|
|
311
|
+
* ```tsx
|
|
312
|
+
* import { PrivacyToggle } from '@sip-protocol/react'
|
|
313
|
+
*
|
|
314
|
+
* function SendForm() {
|
|
315
|
+
* const [privacy, setPrivacy] = useState<PrivacyLevel>('shielded')
|
|
316
|
+
*
|
|
317
|
+
* return (
|
|
318
|
+
* <PrivacyToggle
|
|
319
|
+
* value={privacy}
|
|
320
|
+
* onChange={setPrivacy}
|
|
321
|
+
* />
|
|
322
|
+
* )
|
|
323
|
+
* }
|
|
324
|
+
* ```
|
|
325
|
+
*
|
|
326
|
+
* @example With gas estimates
|
|
327
|
+
* ```tsx
|
|
328
|
+
* <PrivacyToggle
|
|
329
|
+
* value={privacy}
|
|
330
|
+
* onChange={setPrivacy}
|
|
331
|
+
* showGasEstimate
|
|
332
|
+
* gasEstimates={{
|
|
333
|
+
* off: { gas: '2500000000000', cost: '0.00025 NEAR' },
|
|
334
|
+
* shielded: { gas: '30000000000000', cost: '0.003 NEAR' },
|
|
335
|
+
* compliant: { gas: '35000000000000', cost: '0.0035 NEAR' },
|
|
336
|
+
* }}
|
|
337
|
+
* />
|
|
338
|
+
* ```
|
|
339
|
+
*
|
|
340
|
+
* @example Uncontrolled mode
|
|
341
|
+
* ```tsx
|
|
342
|
+
* <PrivacyToggle
|
|
343
|
+
* defaultValue="shielded"
|
|
344
|
+
* onChange={(level) => console.log('Selected:', level)}
|
|
345
|
+
* />
|
|
346
|
+
* ```
|
|
347
|
+
*/
|
|
348
|
+
export function PrivacyToggle({
|
|
349
|
+
value,
|
|
350
|
+
defaultValue = 'shielded',
|
|
351
|
+
onChange,
|
|
352
|
+
disabled = false,
|
|
353
|
+
gasEstimates,
|
|
354
|
+
showGasEstimate = false,
|
|
355
|
+
showTooltips = true,
|
|
356
|
+
className = '',
|
|
357
|
+
size = 'md',
|
|
358
|
+
chain = 'near',
|
|
359
|
+
'aria-label': ariaLabel = 'Privacy level',
|
|
360
|
+
}: PrivacyToggleProps) {
|
|
361
|
+
// Generate unique IDs for accessibility
|
|
362
|
+
const baseId = useId()
|
|
363
|
+
|
|
364
|
+
// Internal state for uncontrolled mode
|
|
365
|
+
const [internalValue, setInternalValue] = useState<PrivacyLevel>(defaultValue)
|
|
366
|
+
|
|
367
|
+
// Determine if controlled or uncontrolled
|
|
368
|
+
const isControlled = value !== undefined
|
|
369
|
+
const currentValue = isControlled ? value : internalValue
|
|
370
|
+
|
|
371
|
+
// Handle selection change
|
|
372
|
+
const handleSelect = useCallback(
|
|
373
|
+
(level: PrivacyLevel) => {
|
|
374
|
+
if (disabled) return
|
|
375
|
+
|
|
376
|
+
if (!isControlled) {
|
|
377
|
+
setInternalValue(level)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
onChange?.(level)
|
|
381
|
+
},
|
|
382
|
+
[disabled, isControlled, onChange]
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
// Handle keyboard navigation
|
|
386
|
+
const handleKeyDown = useCallback(
|
|
387
|
+
(event: React.KeyboardEvent, currentLevel: PrivacyLevel) => {
|
|
388
|
+
const levels: PrivacyLevel[] = ['off', 'shielded', 'compliant']
|
|
389
|
+
const currentIndex = levels.indexOf(currentLevel)
|
|
390
|
+
|
|
391
|
+
let newIndex = currentIndex
|
|
392
|
+
|
|
393
|
+
switch (event.key) {
|
|
394
|
+
case 'ArrowRight':
|
|
395
|
+
case 'ArrowDown':
|
|
396
|
+
event.preventDefault()
|
|
397
|
+
newIndex = (currentIndex + 1) % levels.length
|
|
398
|
+
break
|
|
399
|
+
case 'ArrowLeft':
|
|
400
|
+
case 'ArrowUp':
|
|
401
|
+
event.preventDefault()
|
|
402
|
+
newIndex = (currentIndex - 1 + levels.length) % levels.length
|
|
403
|
+
break
|
|
404
|
+
case 'Home':
|
|
405
|
+
event.preventDefault()
|
|
406
|
+
newIndex = 0
|
|
407
|
+
break
|
|
408
|
+
case 'End':
|
|
409
|
+
event.preventDefault()
|
|
410
|
+
newIndex = levels.length - 1
|
|
411
|
+
break
|
|
412
|
+
default:
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
handleSelect(levels[newIndex])
|
|
417
|
+
},
|
|
418
|
+
[handleSelect]
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
// Build level info with gas estimates
|
|
422
|
+
const levelInfos: PrivacyLevelInfo[] = useMemo(
|
|
423
|
+
() =>
|
|
424
|
+
(['off', 'shielded', 'compliant'] as PrivacyLevel[]).map((level) => ({
|
|
425
|
+
level,
|
|
426
|
+
...DEFAULT_LEVEL_INFO[level],
|
|
427
|
+
gasEstimate: gasEstimates?.[level],
|
|
428
|
+
})),
|
|
429
|
+
[gasEstimates]
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
// Calculate gas difference from base (off)
|
|
433
|
+
const getGasDifference = useCallback(
|
|
434
|
+
(level: PrivacyLevel): string | null => {
|
|
435
|
+
if (!gasEstimates?.off || !gasEstimates?.[level]) return null
|
|
436
|
+
if (level === 'off') return null
|
|
437
|
+
|
|
438
|
+
const baseCost = gasEstimates.off.cost
|
|
439
|
+
const levelCost = gasEstimates[level]?.cost
|
|
440
|
+
|
|
441
|
+
if (!levelCost) return null
|
|
442
|
+
|
|
443
|
+
return `+${levelCost} vs ${baseCost}`
|
|
444
|
+
},
|
|
445
|
+
[gasEstimates]
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<>
|
|
450
|
+
{/* Inject styles */}
|
|
451
|
+
<style>{styles}</style>
|
|
452
|
+
|
|
453
|
+
<div
|
|
454
|
+
className={`sip-privacy-toggle ${className}`}
|
|
455
|
+
data-chain={chain}
|
|
456
|
+
>
|
|
457
|
+
{/* Toggle group */}
|
|
458
|
+
<div
|
|
459
|
+
role="radiogroup"
|
|
460
|
+
aria-label={ariaLabel}
|
|
461
|
+
className="sip-privacy-toggle-group"
|
|
462
|
+
data-size={size}
|
|
463
|
+
>
|
|
464
|
+
{levelInfos.map((info) => {
|
|
465
|
+
const isSelected = currentValue === info.level
|
|
466
|
+
const optionId = `${baseId}-${info.level}`
|
|
467
|
+
|
|
468
|
+
return (
|
|
469
|
+
<button
|
|
470
|
+
key={info.level}
|
|
471
|
+
id={optionId}
|
|
472
|
+
role="radio"
|
|
473
|
+
aria-checked={isSelected}
|
|
474
|
+
aria-label={`${info.label}: ${info.description}`}
|
|
475
|
+
disabled={disabled}
|
|
476
|
+
onClick={() => handleSelect(info.level)}
|
|
477
|
+
onKeyDown={(e) => handleKeyDown(e, info.level)}
|
|
478
|
+
tabIndex={isSelected ? 0 : -1}
|
|
479
|
+
className="sip-privacy-option"
|
|
480
|
+
data-level={info.level}
|
|
481
|
+
data-size={size}
|
|
482
|
+
>
|
|
483
|
+
{info.icon}
|
|
484
|
+
<span>{info.label}</span>
|
|
485
|
+
|
|
486
|
+
{/* Tooltip */}
|
|
487
|
+
{showTooltips && (
|
|
488
|
+
<span className="sip-privacy-tooltip" role="tooltip">
|
|
489
|
+
{info.description}
|
|
490
|
+
</span>
|
|
491
|
+
)}
|
|
492
|
+
</button>
|
|
493
|
+
)
|
|
494
|
+
})}
|
|
495
|
+
</div>
|
|
496
|
+
|
|
497
|
+
{/* Gas estimate info */}
|
|
498
|
+
{showGasEstimate && gasEstimates && (
|
|
499
|
+
<div className="sip-privacy-gas-info" aria-live="polite">
|
|
500
|
+
<span>Estimated fee:</span>
|
|
501
|
+
<span
|
|
502
|
+
className="sip-privacy-gas-badge"
|
|
503
|
+
data-level={currentValue}
|
|
504
|
+
>
|
|
505
|
+
{gasEstimates[currentValue]?.cost ?? 'Unknown'}
|
|
506
|
+
{gasEstimates[currentValue]?.costUsd && (
|
|
507
|
+
<span> (~{gasEstimates[currentValue]?.costUsd})</span>
|
|
508
|
+
)}
|
|
509
|
+
</span>
|
|
510
|
+
{currentValue !== 'off' && getGasDifference(currentValue) && (
|
|
511
|
+
<span className="sip-privacy-gas-diff">
|
|
512
|
+
({getGasDifference(currentValue)})
|
|
513
|
+
</span>
|
|
514
|
+
)}
|
|
515
|
+
</div>
|
|
516
|
+
)}
|
|
517
|
+
</div>
|
|
518
|
+
</>
|
|
519
|
+
)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Hook to use with PrivacyToggle for managing privacy state
|
|
524
|
+
*/
|
|
525
|
+
export function usePrivacyToggle(initialValue: PrivacyLevel = 'shielded') {
|
|
526
|
+
const [privacyLevel, setPrivacyLevel] = useState<PrivacyLevel>(initialValue)
|
|
527
|
+
|
|
528
|
+
const isPrivate = privacyLevel !== 'off'
|
|
529
|
+
const isCompliant = privacyLevel === 'compliant'
|
|
530
|
+
const isShielded = privacyLevel === 'shielded'
|
|
531
|
+
|
|
532
|
+
const setPublic = useCallback(() => setPrivacyLevel('off'), [])
|
|
533
|
+
const setShielded = useCallback(() => setPrivacyLevel('shielded'), [])
|
|
534
|
+
const setCompliant = useCallback(() => setPrivacyLevel('compliant'), [])
|
|
535
|
+
|
|
536
|
+
return {
|
|
537
|
+
privacyLevel,
|
|
538
|
+
setPrivacyLevel,
|
|
539
|
+
isPrivate,
|
|
540
|
+
isCompliant,
|
|
541
|
+
isShielded,
|
|
542
|
+
setPublic,
|
|
543
|
+
setShielded,
|
|
544
|
+
setCompliant,
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export default PrivacyToggle
|