@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,504 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
scanForPayments,
|
|
4
|
+
claimStealthPayment,
|
|
5
|
+
} from '@sip-protocol/sdk'
|
|
6
|
+
import type {
|
|
7
|
+
SolanaScanParams,
|
|
8
|
+
SolanaScanResult,
|
|
9
|
+
SolanaClaimParams,
|
|
10
|
+
SolanaClaimResult,
|
|
11
|
+
} from '@sip-protocol/sdk'
|
|
12
|
+
import type { HexString } from '@sip-protocol/types'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Scan status states
|
|
16
|
+
*/
|
|
17
|
+
export type ScanStatus = 'idle' | 'scanning' | 'claiming' | 'success' | 'error'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parameters for useScanPayments hook
|
|
21
|
+
*/
|
|
22
|
+
export interface UseScanPaymentsParams {
|
|
23
|
+
/** Solana RPC connection */
|
|
24
|
+
connection: SolanaScanParams['connection']
|
|
25
|
+
/** Recipient's viewing private key (hex) */
|
|
26
|
+
viewingPrivateKey: HexString
|
|
27
|
+
/** Recipient's spending public key (hex) */
|
|
28
|
+
spendingPublicKey: HexString
|
|
29
|
+
/** Optional: Auto-scan interval in milliseconds (0 = disabled) */
|
|
30
|
+
scanInterval?: number
|
|
31
|
+
/** Optional: Initial from slot for scanning */
|
|
32
|
+
fromSlot?: number
|
|
33
|
+
/** Optional: Limit number of results per scan */
|
|
34
|
+
limit?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Payment with claim status
|
|
39
|
+
*/
|
|
40
|
+
export interface PaymentWithStatus extends SolanaScanResult {
|
|
41
|
+
/** Whether this payment has been claimed */
|
|
42
|
+
claimed: boolean
|
|
43
|
+
/** Claim result if claimed */
|
|
44
|
+
claimResult?: SolanaClaimResult
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Return type for useScanPayments hook
|
|
49
|
+
*/
|
|
50
|
+
export interface UseScanPaymentsReturn {
|
|
51
|
+
/** Current scan/claim status */
|
|
52
|
+
status: ScanStatus
|
|
53
|
+
/** Whether scanning is in progress */
|
|
54
|
+
isScanning: boolean
|
|
55
|
+
/** Whether claiming is in progress */
|
|
56
|
+
isClaiming: boolean
|
|
57
|
+
/** Error message if scan/claim failed */
|
|
58
|
+
error: Error | null
|
|
59
|
+
/** Detected payments */
|
|
60
|
+
payments: PaymentWithStatus[]
|
|
61
|
+
/** Total unclaimed balance across all payments */
|
|
62
|
+
totalUnclaimed: bigint
|
|
63
|
+
/** Timestamp of last scan */
|
|
64
|
+
lastScannedAt: Date | null
|
|
65
|
+
/** Trigger a manual scan */
|
|
66
|
+
scan: (options?: { fromSlot?: number; limit?: number }) => Promise<SolanaScanResult[]>
|
|
67
|
+
/** Claim a specific payment */
|
|
68
|
+
claim: (payment: SolanaScanResult, params: ClaimParams) => Promise<SolanaClaimResult | null>
|
|
69
|
+
/** Claim all unclaimed payments */
|
|
70
|
+
claimAll: (params: ClaimAllParams) => Promise<ClaimAllResult>
|
|
71
|
+
/** Start auto-scanning */
|
|
72
|
+
startAutoScan: (intervalMs?: number) => void
|
|
73
|
+
/** Stop auto-scanning */
|
|
74
|
+
stopAutoScan: () => void
|
|
75
|
+
/** Clear all payments and reset state */
|
|
76
|
+
reset: () => void
|
|
77
|
+
/** Clear error */
|
|
78
|
+
clearError: () => void
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parameters for claiming a payment
|
|
83
|
+
*/
|
|
84
|
+
export interface ClaimParams {
|
|
85
|
+
/** Recipient's spending private key (hex) */
|
|
86
|
+
spendingPrivateKey: HexString
|
|
87
|
+
/** Destination address to send claimed funds (base58) */
|
|
88
|
+
destinationAddress: string
|
|
89
|
+
/** SPL token mint address */
|
|
90
|
+
mint: SolanaClaimParams['mint']
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Mint resolver function type
|
|
95
|
+
* Converts a mint address string to a PublicKey-like object for claiming
|
|
96
|
+
*/
|
|
97
|
+
export type MintResolver = (mintAddress: string) => SolanaClaimParams['mint']
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parameters for claiming all payments
|
|
101
|
+
*/
|
|
102
|
+
export interface ClaimAllParams {
|
|
103
|
+
/** Recipient's spending private key (hex) */
|
|
104
|
+
spendingPrivateKey: HexString
|
|
105
|
+
/** Destination address to send claimed funds (base58) */
|
|
106
|
+
destinationAddress: string
|
|
107
|
+
/**
|
|
108
|
+
* Function to convert mint address string to PublicKey object
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* import { PublicKey } from '@solana/web3.js'
|
|
113
|
+
*
|
|
114
|
+
* const mintResolver = (mint: string) => new PublicKey(mint)
|
|
115
|
+
* await claimAll({ spendingPrivateKey, destinationAddress, mintResolver })
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
mintResolver: MintResolver
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Result from claimAll operation
|
|
123
|
+
*/
|
|
124
|
+
export interface ClaimAllResult {
|
|
125
|
+
/** Successful claim results */
|
|
126
|
+
succeeded: SolanaClaimResult[]
|
|
127
|
+
/** Failed claims with error info */
|
|
128
|
+
failed: Array<{
|
|
129
|
+
payment: PaymentWithStatus
|
|
130
|
+
error: Error
|
|
131
|
+
}>
|
|
132
|
+
/** Total number of unclaimed payments attempted */
|
|
133
|
+
totalAttempted: number
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* useScanPayments - Scan for and claim incoming stealth payments on Solana
|
|
138
|
+
*
|
|
139
|
+
* @remarks
|
|
140
|
+
* This hook provides a React-friendly interface for scanning the Solana blockchain
|
|
141
|
+
* for incoming stealth payments and claiming them. It supports both manual and
|
|
142
|
+
* automatic scanning with configurable intervals.
|
|
143
|
+
*
|
|
144
|
+
* Features:
|
|
145
|
+
* - Scan for incoming stealth payments
|
|
146
|
+
* - Auto-scanning with configurable interval
|
|
147
|
+
* - Claim individual or all payments
|
|
148
|
+
* - Track claimed vs unclaimed payments
|
|
149
|
+
* - Error handling and recovery
|
|
150
|
+
*
|
|
151
|
+
* @param params - Hook configuration parameters
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```tsx
|
|
155
|
+
* import { useScanPayments } from '@sip-protocol/react'
|
|
156
|
+
* import { useConnection } from '@solana/wallet-adapter-react'
|
|
157
|
+
* import { PublicKey } from '@solana/web3.js'
|
|
158
|
+
*
|
|
159
|
+
* function ReceivePayments() {
|
|
160
|
+
* const { connection } = useConnection()
|
|
161
|
+
*
|
|
162
|
+
* const {
|
|
163
|
+
* payments,
|
|
164
|
+
* isScanning,
|
|
165
|
+
* scan,
|
|
166
|
+
* claim,
|
|
167
|
+
* totalUnclaimed,
|
|
168
|
+
* lastScannedAt,
|
|
169
|
+
* } = useScanPayments({
|
|
170
|
+
* connection,
|
|
171
|
+
* viewingPrivateKey: '0x...',
|
|
172
|
+
* spendingPublicKey: '0x...',
|
|
173
|
+
* scanInterval: 30000, // Auto-scan every 30 seconds
|
|
174
|
+
* })
|
|
175
|
+
*
|
|
176
|
+
* const handleClaim = async (payment: SolanaScanResult) => {
|
|
177
|
+
* await claim(payment, {
|
|
178
|
+
* spendingPrivateKey: '0x...',
|
|
179
|
+
* destinationAddress: 'myWallet...',
|
|
180
|
+
* mint: new PublicKey(payment.mint),
|
|
181
|
+
* })
|
|
182
|
+
* }
|
|
183
|
+
*
|
|
184
|
+
* return (
|
|
185
|
+
* <div>
|
|
186
|
+
* <button onClick={() => scan()} disabled={isScanning}>
|
|
187
|
+
* {isScanning ? 'Scanning...' : 'Scan for Payments'}
|
|
188
|
+
* </button>
|
|
189
|
+
*
|
|
190
|
+
* <p>Total unclaimed: {totalUnclaimed.toString()}</p>
|
|
191
|
+
* <p>Last scan: {lastScannedAt?.toLocaleString()}</p>
|
|
192
|
+
*
|
|
193
|
+
* {payments.map((payment) => (
|
|
194
|
+
* <div key={payment.txSignature}>
|
|
195
|
+
* <p>{payment.amount.toString()} {payment.tokenSymbol}</p>
|
|
196
|
+
* {!payment.claimed && (
|
|
197
|
+
* <button onClick={() => handleClaim(payment)}>Claim</button>
|
|
198
|
+
* )}
|
|
199
|
+
* </div>
|
|
200
|
+
* ))}
|
|
201
|
+
* </div>
|
|
202
|
+
* )
|
|
203
|
+
* }
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
export function useScanPayments(params: UseScanPaymentsParams): UseScanPaymentsReturn {
|
|
207
|
+
const {
|
|
208
|
+
connection,
|
|
209
|
+
viewingPrivateKey,
|
|
210
|
+
spendingPublicKey,
|
|
211
|
+
scanInterval = 0,
|
|
212
|
+
fromSlot: initialFromSlot,
|
|
213
|
+
limit: defaultLimit = 100,
|
|
214
|
+
} = params
|
|
215
|
+
|
|
216
|
+
const [status, setStatus] = useState<ScanStatus>('idle')
|
|
217
|
+
const [error, setError] = useState<Error | null>(null)
|
|
218
|
+
const [payments, setPayments] = useState<PaymentWithStatus[]>([])
|
|
219
|
+
const [lastScannedAt, setLastScannedAt] = useState<Date | null>(null)
|
|
220
|
+
const [lastScannedSlot, setLastScannedSlot] = useState<number | undefined>(initialFromSlot)
|
|
221
|
+
|
|
222
|
+
const autoScanIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
|
|
223
|
+
const isAutoScanningRef = useRef(false)
|
|
224
|
+
|
|
225
|
+
const isScanning = status === 'scanning'
|
|
226
|
+
const isClaiming = status === 'claiming'
|
|
227
|
+
|
|
228
|
+
// Calculate total unclaimed balance
|
|
229
|
+
const totalUnclaimed = payments
|
|
230
|
+
.filter((p) => !p.claimed)
|
|
231
|
+
.reduce((sum, p) => sum + p.amount, 0n)
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Scan for payments
|
|
235
|
+
*/
|
|
236
|
+
const scan = useCallback(
|
|
237
|
+
async (options?: { fromSlot?: number; limit?: number }): Promise<SolanaScanResult[]> => {
|
|
238
|
+
setStatus('scanning')
|
|
239
|
+
setError(null)
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const scanParams: SolanaScanParams = {
|
|
243
|
+
connection,
|
|
244
|
+
viewingPrivateKey,
|
|
245
|
+
spendingPublicKey,
|
|
246
|
+
fromSlot: options?.fromSlot ?? lastScannedSlot,
|
|
247
|
+
limit: options?.limit ?? defaultLimit,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const results = await scanForPayments(scanParams)
|
|
251
|
+
|
|
252
|
+
// Merge with existing payments, avoiding duplicates
|
|
253
|
+
setPayments((prev) => {
|
|
254
|
+
const existingTxs = new Set(prev.map((p) => p.txSignature))
|
|
255
|
+
const newPayments = results
|
|
256
|
+
.filter((r) => !existingTxs.has(r.txSignature))
|
|
257
|
+
.map((r) => ({ ...r, claimed: false }))
|
|
258
|
+
|
|
259
|
+
return [...prev, ...newPayments]
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
// Update last scanned slot to the highest slot we've seen
|
|
263
|
+
if (results.length > 0) {
|
|
264
|
+
const maxSlot = Math.max(...results.map((r) => r.slot))
|
|
265
|
+
setLastScannedSlot(maxSlot + 1) // Start from next slot on subsequent scans
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
setLastScannedAt(new Date())
|
|
269
|
+
setStatus('idle')
|
|
270
|
+
|
|
271
|
+
return results
|
|
272
|
+
} catch (err) {
|
|
273
|
+
const error = err instanceof Error ? err : new Error('Scan failed')
|
|
274
|
+
setError(error)
|
|
275
|
+
setStatus('error')
|
|
276
|
+
throw error
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
[connection, viewingPrivateKey, spendingPublicKey, lastScannedSlot, defaultLimit]
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Claim a specific payment
|
|
284
|
+
*/
|
|
285
|
+
const claim = useCallback(
|
|
286
|
+
async (payment: SolanaScanResult, claimParams: ClaimParams): Promise<SolanaClaimResult | null> => {
|
|
287
|
+
setStatus('claiming')
|
|
288
|
+
setError(null)
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const sdkParams: SolanaClaimParams = {
|
|
292
|
+
connection,
|
|
293
|
+
stealthAddress: payment.stealthAddress,
|
|
294
|
+
ephemeralPublicKey: payment.ephemeralPublicKey,
|
|
295
|
+
viewingPrivateKey,
|
|
296
|
+
spendingPrivateKey: claimParams.spendingPrivateKey,
|
|
297
|
+
destinationAddress: claimParams.destinationAddress,
|
|
298
|
+
mint: claimParams.mint,
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const result = await claimStealthPayment(sdkParams)
|
|
302
|
+
|
|
303
|
+
// Update payment status
|
|
304
|
+
setPayments((prev) =>
|
|
305
|
+
prev.map((p) =>
|
|
306
|
+
p.txSignature === payment.txSignature
|
|
307
|
+
? { ...p, claimed: true, claimResult: result }
|
|
308
|
+
: p
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
setStatus('success')
|
|
313
|
+
return result
|
|
314
|
+
} catch (err) {
|
|
315
|
+
const error = err instanceof Error ? err : new Error('Claim failed')
|
|
316
|
+
setError(error)
|
|
317
|
+
setStatus('error')
|
|
318
|
+
return null
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
[connection, viewingPrivateKey]
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Claim all unclaimed payments
|
|
326
|
+
*
|
|
327
|
+
* @remarks
|
|
328
|
+
* Iterates through all unclaimed payments and attempts to claim each one.
|
|
329
|
+
* Uses the provided mintResolver to convert mint address strings to PublicKey objects.
|
|
330
|
+
* Handles partial failures gracefully - continues with remaining payments if one fails.
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* ```typescript
|
|
334
|
+
* import { PublicKey } from '@solana/web3.js'
|
|
335
|
+
*
|
|
336
|
+
* const { claimAll } = useScanPayments({ ... })
|
|
337
|
+
*
|
|
338
|
+
* const result = await claimAll({
|
|
339
|
+
* spendingPrivateKey: '0x...',
|
|
340
|
+
* destinationAddress: 'myWallet...',
|
|
341
|
+
* mintResolver: (mint) => new PublicKey(mint),
|
|
342
|
+
* })
|
|
343
|
+
*
|
|
344
|
+
* console.log(`Claimed ${result.succeeded.length} of ${result.totalAttempted}`)
|
|
345
|
+
* if (result.failed.length > 0) {
|
|
346
|
+
* console.error('Failed claims:', result.failed)
|
|
347
|
+
* }
|
|
348
|
+
* ```
|
|
349
|
+
*/
|
|
350
|
+
const claimAll = useCallback(
|
|
351
|
+
async (claimAllParams: ClaimAllParams): Promise<ClaimAllResult> => {
|
|
352
|
+
const { spendingPrivateKey, destinationAddress, mintResolver } = claimAllParams
|
|
353
|
+
const unclaimed = payments.filter((p) => !p.claimed)
|
|
354
|
+
|
|
355
|
+
if (unclaimed.length === 0) {
|
|
356
|
+
return { succeeded: [], failed: [], totalAttempted: 0 }
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
setStatus('claiming')
|
|
360
|
+
setError(null)
|
|
361
|
+
|
|
362
|
+
const succeeded: SolanaClaimResult[] = []
|
|
363
|
+
const failed: Array<{ payment: PaymentWithStatus; error: Error }> = []
|
|
364
|
+
|
|
365
|
+
// Process claims sequentially to avoid overwhelming the RPC
|
|
366
|
+
for (const payment of unclaimed) {
|
|
367
|
+
try {
|
|
368
|
+
const mint = mintResolver(payment.mint)
|
|
369
|
+
const result = await claimStealthPayment({
|
|
370
|
+
connection,
|
|
371
|
+
stealthAddress: payment.stealthAddress,
|
|
372
|
+
ephemeralPublicKey: payment.ephemeralPublicKey,
|
|
373
|
+
viewingPrivateKey,
|
|
374
|
+
spendingPrivateKey,
|
|
375
|
+
destinationAddress,
|
|
376
|
+
mint,
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
// Update payment status immediately
|
|
380
|
+
setPayments((prev) =>
|
|
381
|
+
prev.map((p) =>
|
|
382
|
+
p.txSignature === payment.txSignature
|
|
383
|
+
? { ...p, claimed: true, claimResult: result }
|
|
384
|
+
: p
|
|
385
|
+
)
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
succeeded.push(result)
|
|
389
|
+
} catch (err) {
|
|
390
|
+
const error = err instanceof Error ? err : new Error('Claim failed')
|
|
391
|
+
failed.push({ payment, error })
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Set final status based on results
|
|
396
|
+
if (failed.length > 0 && succeeded.length === 0) {
|
|
397
|
+
setStatus('error')
|
|
398
|
+
setError(failed[0].error)
|
|
399
|
+
} else if (failed.length > 0) {
|
|
400
|
+
// Partial success - still set success but keep error for reference
|
|
401
|
+
setStatus('success')
|
|
402
|
+
setError(new Error(`${failed.length} of ${unclaimed.length} claims failed`))
|
|
403
|
+
} else {
|
|
404
|
+
setStatus('success')
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
succeeded,
|
|
409
|
+
failed,
|
|
410
|
+
totalAttempted: unclaimed.length,
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
[payments, connection, viewingPrivateKey]
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Start auto-scanning
|
|
418
|
+
*/
|
|
419
|
+
const startAutoScan = useCallback(
|
|
420
|
+
(intervalMs?: number) => {
|
|
421
|
+
const interval = intervalMs ?? scanInterval
|
|
422
|
+
if (interval <= 0) return
|
|
423
|
+
|
|
424
|
+
// Stop existing auto-scan
|
|
425
|
+
if (autoScanIntervalRef.current) {
|
|
426
|
+
clearInterval(autoScanIntervalRef.current)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
isAutoScanningRef.current = true
|
|
430
|
+
|
|
431
|
+
// Initial scan
|
|
432
|
+
scan().catch(console.error)
|
|
433
|
+
|
|
434
|
+
// Set up interval
|
|
435
|
+
autoScanIntervalRef.current = setInterval(() => {
|
|
436
|
+
if (isAutoScanningRef.current && status !== 'scanning') {
|
|
437
|
+
scan().catch(console.error)
|
|
438
|
+
}
|
|
439
|
+
}, interval)
|
|
440
|
+
},
|
|
441
|
+
[scan, scanInterval, status]
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Stop auto-scanning
|
|
446
|
+
*/
|
|
447
|
+
const stopAutoScan = useCallback(() => {
|
|
448
|
+
isAutoScanningRef.current = false
|
|
449
|
+
if (autoScanIntervalRef.current) {
|
|
450
|
+
clearInterval(autoScanIntervalRef.current)
|
|
451
|
+
autoScanIntervalRef.current = null
|
|
452
|
+
}
|
|
453
|
+
}, [])
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Reset all state
|
|
457
|
+
*/
|
|
458
|
+
const reset = useCallback(() => {
|
|
459
|
+
stopAutoScan()
|
|
460
|
+
setStatus('idle')
|
|
461
|
+
setError(null)
|
|
462
|
+
setPayments([])
|
|
463
|
+
setLastScannedAt(null)
|
|
464
|
+
setLastScannedSlot(initialFromSlot)
|
|
465
|
+
}, [stopAutoScan, initialFromSlot])
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Clear error state
|
|
469
|
+
*/
|
|
470
|
+
const clearError = useCallback(() => {
|
|
471
|
+
setError(null)
|
|
472
|
+
if (status === 'error') {
|
|
473
|
+
setStatus('idle')
|
|
474
|
+
}
|
|
475
|
+
}, [status])
|
|
476
|
+
|
|
477
|
+
// Auto-start scanning if interval is provided (runs only on mount/unmount)
|
|
478
|
+
useEffect(() => {
|
|
479
|
+
if (scanInterval > 0) {
|
|
480
|
+
startAutoScan(scanInterval)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return () => {
|
|
484
|
+
stopAutoScan()
|
|
485
|
+
}
|
|
486
|
+
}, [])
|
|
487
|
+
|
|
488
|
+
return {
|
|
489
|
+
status,
|
|
490
|
+
isScanning,
|
|
491
|
+
isClaiming,
|
|
492
|
+
error,
|
|
493
|
+
payments,
|
|
494
|
+
totalUnclaimed,
|
|
495
|
+
lastScannedAt,
|
|
496
|
+
scan,
|
|
497
|
+
claim,
|
|
498
|
+
claimAll,
|
|
499
|
+
startAutoScan,
|
|
500
|
+
stopAutoScan,
|
|
501
|
+
reset,
|
|
502
|
+
clearError,
|
|
503
|
+
}
|
|
504
|
+
}
|
|
@@ -54,12 +54,15 @@ export function useStealthAddress(chain: ChainId): {
|
|
|
54
54
|
metaAddress: string | null
|
|
55
55
|
stealthAddress: string | null
|
|
56
56
|
isGenerating: boolean
|
|
57
|
+
error: Error | null
|
|
57
58
|
regenerate: () => void
|
|
58
59
|
copyToClipboard: () => Promise<void>
|
|
60
|
+
clearError: () => void
|
|
59
61
|
} {
|
|
60
62
|
const [metaAddress, setMetaAddress] = useState<string | null>(null)
|
|
61
63
|
const [stealthAddress, setStealthAddress] = useState<string | null>(null)
|
|
62
64
|
const [isGenerating, setIsGenerating] = useState<boolean>(false)
|
|
65
|
+
const [error, setError] = useState<Error | null>(null)
|
|
63
66
|
|
|
64
67
|
// Generate meta-address on mount
|
|
65
68
|
useEffect(() => {
|
|
@@ -90,9 +93,11 @@ export function useStealthAddress(chain: ChainId): {
|
|
|
90
93
|
|
|
91
94
|
if (cancelled) return
|
|
92
95
|
setStealthAddress(stealthData.stealthAddress.address)
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
setError(null) // Clear any previous error on success
|
|
97
|
+
} catch (err) {
|
|
95
98
|
if (cancelled) return
|
|
99
|
+
const error = err instanceof Error ? err : new Error('Failed to generate stealth addresses')
|
|
100
|
+
setError(error)
|
|
96
101
|
setMetaAddress(null)
|
|
97
102
|
setStealthAddress(null)
|
|
98
103
|
} finally {
|
|
@@ -139,8 +144,10 @@ export function useStealthAddress(chain: ChainId): {
|
|
|
139
144
|
: generateStealthAddress(metaAddressObj)
|
|
140
145
|
|
|
141
146
|
setStealthAddress(stealthData.stealthAddress.address)
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
setError(null) // Clear any previous error on success
|
|
148
|
+
} catch (err) {
|
|
149
|
+
const error = err instanceof Error ? err : new Error('Failed to regenerate stealth address')
|
|
150
|
+
setError(error)
|
|
144
151
|
} finally {
|
|
145
152
|
setIsGenerating(false)
|
|
146
153
|
}
|
|
@@ -155,8 +162,8 @@ export function useStealthAddress(chain: ChainId): {
|
|
|
155
162
|
|
|
156
163
|
try {
|
|
157
164
|
await navigator.clipboard.writeText(stealthAddress)
|
|
158
|
-
|
|
159
|
-
|
|
165
|
+
setError(null) // Clear any previous error on success
|
|
166
|
+
} catch (clipboardErr) {
|
|
160
167
|
// Fallback for older browsers
|
|
161
168
|
const textArea = document.createElement('textarea')
|
|
162
169
|
textArea.value = stealthAddress
|
|
@@ -166,19 +173,28 @@ export function useStealthAddress(chain: ChainId): {
|
|
|
166
173
|
textArea.select()
|
|
167
174
|
try {
|
|
168
175
|
document.execCommand('copy')
|
|
176
|
+
setError(null) // Clear any previous error on success
|
|
169
177
|
} catch (err) {
|
|
170
|
-
|
|
178
|
+
const error = err instanceof Error ? err : new Error('Failed to copy to clipboard')
|
|
179
|
+
setError(error)
|
|
171
180
|
} finally {
|
|
172
181
|
document.body.removeChild(textArea)
|
|
173
182
|
}
|
|
174
183
|
}
|
|
175
184
|
}, [stealthAddress])
|
|
176
185
|
|
|
186
|
+
// Clear error state
|
|
187
|
+
const clearError = useCallback(() => {
|
|
188
|
+
setError(null)
|
|
189
|
+
}, [])
|
|
190
|
+
|
|
177
191
|
return {
|
|
178
192
|
metaAddress,
|
|
179
193
|
stealthAddress,
|
|
180
194
|
isGenerating,
|
|
195
|
+
error,
|
|
181
196
|
regenerate,
|
|
182
197
|
copyToClipboard,
|
|
198
|
+
clearError,
|
|
183
199
|
}
|
|
184
200
|
}
|