@sip-protocol/react-native 0.1.0
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/LICENSE +21 -0
- package/README.md +186 -0
- package/dist/index.d.mts +442 -0
- package/dist/index.d.ts +442 -0
- package/dist/index.js +612 -0
- package/dist/index.mjs +575 -0
- package/package.json +86 -0
- package/src/hooks/index.ts +30 -0
- package/src/hooks/use-scan-payments.ts +288 -0
- package/src/hooks/use-stealth-address.ts +329 -0
- package/src/hooks/use-stealth-transfer.ts +252 -0
- package/src/index.ts +58 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/secure-storage.ts +395 -0
- package/src/utils/clipboard.ts +67 -0
- package/src/utils/index.ts +1 -0
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useScanPayments - Mobile-optimized payment scanning hook
|
|
3
|
+
*
|
|
4
|
+
* Scans for incoming stealth payments using viewing key.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* import { useScanPayments } from '@sip-protocol/react-native'
|
|
9
|
+
*
|
|
10
|
+
* function InboxScreen() {
|
|
11
|
+
* const { payments, isScanning, scan, claim } = useScanPayments({
|
|
12
|
+
* connection,
|
|
13
|
+
* provider: heliusProvider,
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* useEffect(() => {
|
|
17
|
+
* // Load keys from keychain and scan
|
|
18
|
+
* loadKeysAndScan()
|
|
19
|
+
* }, [])
|
|
20
|
+
*
|
|
21
|
+
* return (
|
|
22
|
+
* <FlatList
|
|
23
|
+
* data={payments}
|
|
24
|
+
* renderItem={({ item }) => (
|
|
25
|
+
* <PaymentCard
|
|
26
|
+
* payment={item}
|
|
27
|
+
* onClaim={() => claim(item.signature)}
|
|
28
|
+
* />
|
|
29
|
+
* )}
|
|
30
|
+
* />
|
|
31
|
+
* )
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { useState, useCallback, useRef } from 'react'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Solana connection interface (subset of @solana/web3.js Connection)
|
|
40
|
+
*/
|
|
41
|
+
export interface SolanaConnection {
|
|
42
|
+
getAccountInfo(publicKey: unknown): Promise<unknown>
|
|
43
|
+
getSignaturesForAddress(address: unknown, options?: unknown): Promise<unknown[]>
|
|
44
|
+
getTransaction(signature: string, options?: unknown): Promise<unknown>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Scanned payment result from SDK
|
|
49
|
+
*/
|
|
50
|
+
interface SDKPaymentResult {
|
|
51
|
+
signature: string
|
|
52
|
+
stealthAddress: string
|
|
53
|
+
ephemeralPublicKey: string
|
|
54
|
+
mint: string
|
|
55
|
+
amount: bigint
|
|
56
|
+
timestamp?: number
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Scanned payment info
|
|
61
|
+
*/
|
|
62
|
+
export interface ScannedPayment {
|
|
63
|
+
/** Transaction signature */
|
|
64
|
+
signature: string
|
|
65
|
+
/** Stealth address that received the payment */
|
|
66
|
+
stealthAddress: string
|
|
67
|
+
/** Ephemeral public key */
|
|
68
|
+
ephemeralPublicKey: string
|
|
69
|
+
/** Token mint address */
|
|
70
|
+
mint: string
|
|
71
|
+
/** Amount in smallest units */
|
|
72
|
+
amount: bigint
|
|
73
|
+
/** Block timestamp */
|
|
74
|
+
timestamp: number
|
|
75
|
+
/** Whether this payment has been claimed */
|
|
76
|
+
claimed: boolean
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Scan status
|
|
81
|
+
*/
|
|
82
|
+
export type ScanStatus = 'idle' | 'scanning' | 'claiming' | 'error'
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Hook parameters
|
|
86
|
+
*/
|
|
87
|
+
export interface UseScanPaymentsParams {
|
|
88
|
+
/** Solana connection */
|
|
89
|
+
connection: SolanaConnection
|
|
90
|
+
/** RPC provider (Helius recommended) */
|
|
91
|
+
provider?: unknown
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Hook return type
|
|
96
|
+
*/
|
|
97
|
+
export interface UseScanPaymentsReturn {
|
|
98
|
+
/** Scanned payments */
|
|
99
|
+
payments: ScannedPayment[]
|
|
100
|
+
/** Current status */
|
|
101
|
+
status: ScanStatus
|
|
102
|
+
/** Error if any */
|
|
103
|
+
error: Error | null
|
|
104
|
+
/** Whether scanning is in progress */
|
|
105
|
+
isScanning: boolean
|
|
106
|
+
/** Scan for payments */
|
|
107
|
+
scan: (viewingPrivateKey: string, spendingPublicKey: string) => Promise<void>
|
|
108
|
+
/** Claim a specific payment */
|
|
109
|
+
claim: (signature: string, spendingPrivateKey: string, destinationAddress: string) => Promise<string | null>
|
|
110
|
+
/** Claim all unclaimed payments */
|
|
111
|
+
claimAll: (spendingPrivateKey: string, destinationAddress: string) => Promise<string[]>
|
|
112
|
+
/** Clear payments */
|
|
113
|
+
clear: () => void
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Mobile payment scanning hook
|
|
118
|
+
*/
|
|
119
|
+
export function useScanPayments(params: UseScanPaymentsParams): UseScanPaymentsReturn {
|
|
120
|
+
const { connection } = params
|
|
121
|
+
|
|
122
|
+
const [payments, setPayments] = useState<ScannedPayment[]>([])
|
|
123
|
+
const [status, setStatus] = useState<ScanStatus>('idle')
|
|
124
|
+
const [error, setError] = useState<Error | null>(null)
|
|
125
|
+
const scanningRef = useRef(false)
|
|
126
|
+
|
|
127
|
+
const scan = useCallback(
|
|
128
|
+
async (viewingPrivateKey: string, spendingPublicKey: string) => {
|
|
129
|
+
if (scanningRef.current) return
|
|
130
|
+
scanningRef.current = true
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
setStatus('scanning')
|
|
134
|
+
setError(null)
|
|
135
|
+
|
|
136
|
+
// Dynamic import with runtime check
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
138
|
+
const sdk: any = await import('@sip-protocol/sdk')
|
|
139
|
+
|
|
140
|
+
if (!sdk.scanForPayments) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
'scanForPayments not available. Install @sip-protocol/sdk with Solana support.'
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const scanForPayments = sdk.scanForPayments as (params: {
|
|
147
|
+
connection: unknown
|
|
148
|
+
viewingPrivateKey: string
|
|
149
|
+
spendingPublicKey: string
|
|
150
|
+
}) => Promise<SDKPaymentResult[]>
|
|
151
|
+
|
|
152
|
+
const result = await scanForPayments({
|
|
153
|
+
connection,
|
|
154
|
+
viewingPrivateKey,
|
|
155
|
+
spendingPublicKey,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Map to our payment type
|
|
159
|
+
const scannedPayments: ScannedPayment[] = result.map((p: SDKPaymentResult) => ({
|
|
160
|
+
signature: p.signature,
|
|
161
|
+
stealthAddress: p.stealthAddress,
|
|
162
|
+
ephemeralPublicKey: p.ephemeralPublicKey,
|
|
163
|
+
mint: p.mint,
|
|
164
|
+
amount: p.amount,
|
|
165
|
+
timestamp: p.timestamp ?? Date.now(),
|
|
166
|
+
claimed: false,
|
|
167
|
+
}))
|
|
168
|
+
|
|
169
|
+
setPayments((prev) => {
|
|
170
|
+
// Merge with existing, avoiding duplicates
|
|
171
|
+
const existingSigs = new Set(prev.map((p) => p.signature))
|
|
172
|
+
const newPayments = scannedPayments.filter((p) => !existingSigs.has(p.signature))
|
|
173
|
+
return [...prev, ...newPayments]
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
setStatus('idle')
|
|
177
|
+
} catch (err) {
|
|
178
|
+
const error = err instanceof Error ? err : new Error('Scan failed')
|
|
179
|
+
setError(error)
|
|
180
|
+
setStatus('error')
|
|
181
|
+
} finally {
|
|
182
|
+
scanningRef.current = false
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
[connection]
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
const claim = useCallback(
|
|
189
|
+
async (
|
|
190
|
+
signature: string,
|
|
191
|
+
spendingPrivateKey: string,
|
|
192
|
+
destinationAddress: string
|
|
193
|
+
): Promise<string | null> => {
|
|
194
|
+
try {
|
|
195
|
+
setStatus('claiming')
|
|
196
|
+
setError(null)
|
|
197
|
+
|
|
198
|
+
const payment = payments.find((p) => p.signature === signature)
|
|
199
|
+
if (!payment) {
|
|
200
|
+
throw new Error('Payment not found')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
204
|
+
const sdk: any = await import('@sip-protocol/sdk')
|
|
205
|
+
const { PublicKey } = await import('@solana/web3.js')
|
|
206
|
+
|
|
207
|
+
if (!sdk.claimStealthPayment) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
'claimStealthPayment not available. Install @sip-protocol/sdk with Solana support.'
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const claimStealthPayment = sdk.claimStealthPayment as (params: {
|
|
214
|
+
connection: unknown
|
|
215
|
+
stealthAddress: string
|
|
216
|
+
ephemeralPublicKey: string
|
|
217
|
+
viewingPrivateKey: string
|
|
218
|
+
spendingPrivateKey: string
|
|
219
|
+
destinationAddress: unknown
|
|
220
|
+
mint: unknown
|
|
221
|
+
}) => Promise<{ signature: string }>
|
|
222
|
+
|
|
223
|
+
const result = await claimStealthPayment({
|
|
224
|
+
connection,
|
|
225
|
+
stealthAddress: payment.stealthAddress,
|
|
226
|
+
ephemeralPublicKey: payment.ephemeralPublicKey,
|
|
227
|
+
viewingPrivateKey: '', // Not needed for claiming
|
|
228
|
+
spendingPrivateKey,
|
|
229
|
+
destinationAddress: new PublicKey(destinationAddress),
|
|
230
|
+
mint: new PublicKey(payment.mint),
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
// Mark as claimed
|
|
234
|
+
setPayments((prev) =>
|
|
235
|
+
prev.map((p) =>
|
|
236
|
+
p.signature === signature ? { ...p, claimed: true } : p
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
setStatus('idle')
|
|
241
|
+
return result.signature
|
|
242
|
+
} catch (err) {
|
|
243
|
+
const error = err instanceof Error ? err : new Error('Claim failed')
|
|
244
|
+
setError(error)
|
|
245
|
+
setStatus('error')
|
|
246
|
+
return null
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
[connection, payments]
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
const claimAll = useCallback(
|
|
253
|
+
async (
|
|
254
|
+
spendingPrivateKey: string,
|
|
255
|
+
destinationAddress: string
|
|
256
|
+
): Promise<string[]> => {
|
|
257
|
+
const unclaimedPayments = payments.filter((p) => !p.claimed)
|
|
258
|
+
const signatures: string[] = []
|
|
259
|
+
|
|
260
|
+
for (const payment of unclaimedPayments) {
|
|
261
|
+
const sig = await claim(payment.signature, spendingPrivateKey, destinationAddress)
|
|
262
|
+
if (sig) {
|
|
263
|
+
signatures.push(sig)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return signatures
|
|
268
|
+
},
|
|
269
|
+
[payments, claim]
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
const clear = useCallback(() => {
|
|
273
|
+
setPayments([])
|
|
274
|
+
setError(null)
|
|
275
|
+
setStatus('idle')
|
|
276
|
+
}, [])
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
payments,
|
|
280
|
+
status,
|
|
281
|
+
error,
|
|
282
|
+
isScanning: status === 'scanning',
|
|
283
|
+
scan,
|
|
284
|
+
claim,
|
|
285
|
+
claimAll,
|
|
286
|
+
clear,
|
|
287
|
+
}
|
|
288
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useStealthAddress - Mobile-optimized stealth address hook
|
|
3
|
+
*
|
|
4
|
+
* React Native version with secure storage and native clipboard support.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```tsx
|
|
8
|
+
* import { useStealthAddress } from '@sip-protocol/react-native'
|
|
9
|
+
*
|
|
10
|
+
* function ReceiveScreen() {
|
|
11
|
+
* const {
|
|
12
|
+
* metaAddress,
|
|
13
|
+
* stealthAddress,
|
|
14
|
+
* isGenerating,
|
|
15
|
+
* regenerate,
|
|
16
|
+
* copyToClipboard,
|
|
17
|
+
* saveToKeychain,
|
|
18
|
+
* } = useStealthAddress('solana')
|
|
19
|
+
*
|
|
20
|
+
* return (
|
|
21
|
+
* <View>
|
|
22
|
+
* <Text>Share: {metaAddress}</Text>
|
|
23
|
+
* <TouchableOpacity onPress={copyToClipboard}>
|
|
24
|
+
* <Text>Copy</Text>
|
|
25
|
+
* </TouchableOpacity>
|
|
26
|
+
* <TouchableOpacity onPress={saveToKeychain}>
|
|
27
|
+
* <Text>Save Securely</Text>
|
|
28
|
+
* </TouchableOpacity>
|
|
29
|
+
* </View>
|
|
30
|
+
* )
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
36
|
+
import { copyToClipboard as nativeCopyToClipboard } from '../utils/clipboard'
|
|
37
|
+
import { SecureStorage } from '../storage/secure-storage'
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Supported chain IDs (matches @sip-protocol/types ChainId)
|
|
41
|
+
*/
|
|
42
|
+
export type SupportedChainId =
|
|
43
|
+
| 'ethereum'
|
|
44
|
+
| 'solana'
|
|
45
|
+
| 'near'
|
|
46
|
+
| 'bitcoin'
|
|
47
|
+
| 'polygon'
|
|
48
|
+
| 'arbitrum'
|
|
49
|
+
| 'optimism'
|
|
50
|
+
| 'base'
|
|
51
|
+
| 'bsc'
|
|
52
|
+
| 'avalanche'
|
|
53
|
+
| 'cosmos'
|
|
54
|
+
| 'aptos'
|
|
55
|
+
| 'sui'
|
|
56
|
+
| 'polkadot'
|
|
57
|
+
| 'tezos'
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Stealth meta-address structure from SDK
|
|
61
|
+
*/
|
|
62
|
+
interface StealthMetaAddressResult {
|
|
63
|
+
metaAddress: {
|
|
64
|
+
chain: string
|
|
65
|
+
spendingKey: string
|
|
66
|
+
viewingKey: string
|
|
67
|
+
}
|
|
68
|
+
spendingPrivateKey: string
|
|
69
|
+
viewingPrivateKey: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Stealth address generation result from SDK
|
|
74
|
+
*/
|
|
75
|
+
interface StealthAddressResult {
|
|
76
|
+
stealthAddress: {
|
|
77
|
+
address: string
|
|
78
|
+
}
|
|
79
|
+
ephemeralPublicKey: string
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Options for useStealthAddress hook
|
|
84
|
+
*/
|
|
85
|
+
export interface UseStealthAddressOptions {
|
|
86
|
+
/** Auto-save to secure storage on generation */
|
|
87
|
+
autoSave?: boolean
|
|
88
|
+
/** Require biometrics to access stored keys */
|
|
89
|
+
requireBiometrics?: boolean
|
|
90
|
+
/** Wallet identifier for storage (default: 'default') */
|
|
91
|
+
walletId?: string
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Return type for useStealthAddress hook
|
|
96
|
+
*/
|
|
97
|
+
export interface UseStealthAddressReturn {
|
|
98
|
+
/** Encoded meta-address for sharing */
|
|
99
|
+
metaAddress: string | null
|
|
100
|
+
/** One-time stealth address */
|
|
101
|
+
stealthAddress: string | null
|
|
102
|
+
/** Spending private key (for claiming) */
|
|
103
|
+
spendingPrivateKey: string | null
|
|
104
|
+
/** Viewing private key (for scanning) */
|
|
105
|
+
viewingPrivateKey: string | null
|
|
106
|
+
/** Whether generation is in progress */
|
|
107
|
+
isGenerating: boolean
|
|
108
|
+
/** Error if any occurred */
|
|
109
|
+
error: Error | null
|
|
110
|
+
/** Generate a new stealth address */
|
|
111
|
+
regenerate: () => void
|
|
112
|
+
/** Copy stealth address to clipboard */
|
|
113
|
+
copyToClipboard: () => Promise<boolean>
|
|
114
|
+
/** Save keys to secure storage */
|
|
115
|
+
saveToKeychain: () => Promise<boolean>
|
|
116
|
+
/** Load keys from secure storage */
|
|
117
|
+
loadFromKeychain: () => Promise<boolean>
|
|
118
|
+
/** Clear error state */
|
|
119
|
+
clearError: () => void
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Mobile-optimized stealth address hook
|
|
124
|
+
*
|
|
125
|
+
* @param chain - Target blockchain
|
|
126
|
+
* @param options - Hook options
|
|
127
|
+
*/
|
|
128
|
+
export function useStealthAddress(
|
|
129
|
+
chain: SupportedChainId,
|
|
130
|
+
options: UseStealthAddressOptions = {}
|
|
131
|
+
): UseStealthAddressReturn {
|
|
132
|
+
const { autoSave = false, requireBiometrics = false, walletId = 'default' } = options
|
|
133
|
+
|
|
134
|
+
const [metaAddress, setMetaAddress] = useState<string | null>(null)
|
|
135
|
+
const [stealthAddress, setStealthAddress] = useState<string | null>(null)
|
|
136
|
+
const [spendingPrivateKey, setSpendingPrivateKey] = useState<string | null>(null)
|
|
137
|
+
const [viewingPrivateKey, setViewingPrivateKey] = useState<string | null>(null)
|
|
138
|
+
const [isGenerating, setIsGenerating] = useState<boolean>(false)
|
|
139
|
+
const [error, setError] = useState<Error | null>(null)
|
|
140
|
+
|
|
141
|
+
// Generate keys on mount
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
let cancelled = false
|
|
144
|
+
|
|
145
|
+
const generate = async () => {
|
|
146
|
+
setIsGenerating(true)
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
// Dynamic import SDK functions
|
|
150
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
151
|
+
const sdk: any = await import('@sip-protocol/sdk')
|
|
152
|
+
|
|
153
|
+
// Generate meta-address with keys (handles Ed25519 vs secp256k1 internally)
|
|
154
|
+
const generateStealthMetaAddress = sdk.generateStealthMetaAddress as (
|
|
155
|
+
chain: string
|
|
156
|
+
) => StealthMetaAddressResult
|
|
157
|
+
|
|
158
|
+
const metaAddressData = generateStealthMetaAddress(chain)
|
|
159
|
+
|
|
160
|
+
if (cancelled) return
|
|
161
|
+
|
|
162
|
+
const encodeStealthMetaAddress = sdk.encodeStealthMetaAddress as (
|
|
163
|
+
metaAddress: { chain: string; spendingKey: string; viewingKey: string }
|
|
164
|
+
) => string
|
|
165
|
+
|
|
166
|
+
const encoded = encodeStealthMetaAddress(metaAddressData.metaAddress)
|
|
167
|
+
setMetaAddress(encoded)
|
|
168
|
+
setSpendingPrivateKey(metaAddressData.spendingPrivateKey)
|
|
169
|
+
setViewingPrivateKey(metaAddressData.viewingPrivateKey)
|
|
170
|
+
|
|
171
|
+
// Generate initial stealth address
|
|
172
|
+
const generateStealthAddress = sdk.generateStealthAddress as (
|
|
173
|
+
metaAddress: { chain: string; spendingKey: string; viewingKey: string }
|
|
174
|
+
) => StealthAddressResult
|
|
175
|
+
|
|
176
|
+
const stealthData = generateStealthAddress(metaAddressData.metaAddress)
|
|
177
|
+
|
|
178
|
+
if (cancelled) return
|
|
179
|
+
setStealthAddress(stealthData.stealthAddress.address)
|
|
180
|
+
setError(null)
|
|
181
|
+
|
|
182
|
+
// Auto-save if enabled
|
|
183
|
+
if (autoSave && !cancelled) {
|
|
184
|
+
await SecureStorage.setSpendingKey(walletId, metaAddressData.spendingPrivateKey, {
|
|
185
|
+
requireBiometrics,
|
|
186
|
+
})
|
|
187
|
+
await SecureStorage.setViewingKey(walletId, metaAddressData.viewingPrivateKey, {
|
|
188
|
+
requireBiometrics,
|
|
189
|
+
})
|
|
190
|
+
await SecureStorage.setMetaAddress(walletId, encoded, { requireBiometrics })
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (cancelled) return
|
|
194
|
+
const error = err instanceof Error ? err : new Error('Failed to generate stealth addresses')
|
|
195
|
+
setError(error)
|
|
196
|
+
setMetaAddress(null)
|
|
197
|
+
setStealthAddress(null)
|
|
198
|
+
setSpendingPrivateKey(null)
|
|
199
|
+
setViewingPrivateKey(null)
|
|
200
|
+
} finally {
|
|
201
|
+
if (!cancelled) {
|
|
202
|
+
setIsGenerating(false)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
generate()
|
|
208
|
+
|
|
209
|
+
return () => {
|
|
210
|
+
cancelled = true
|
|
211
|
+
}
|
|
212
|
+
}, [chain, autoSave, requireBiometrics, walletId])
|
|
213
|
+
|
|
214
|
+
// Regenerate stealth address
|
|
215
|
+
const regenerate = useCallback(() => {
|
|
216
|
+
if (!metaAddress) return
|
|
217
|
+
|
|
218
|
+
setIsGenerating(true)
|
|
219
|
+
|
|
220
|
+
// Use setTimeout to avoid blocking UI
|
|
221
|
+
setTimeout(async () => {
|
|
222
|
+
try {
|
|
223
|
+
const parts = metaAddress.split(':')
|
|
224
|
+
if (parts.length < 4) {
|
|
225
|
+
throw new Error('Invalid meta-address format')
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const [, chainId, spendingKey, viewingKey] = parts
|
|
229
|
+
const metaAddressObj = {
|
|
230
|
+
chain: chainId,
|
|
231
|
+
spendingKey: spendingKey.startsWith('0x') ? spendingKey : `0x${spendingKey}`,
|
|
232
|
+
viewingKey: viewingKey.startsWith('0x') ? viewingKey : `0x${viewingKey}`,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Dynamic import SDK
|
|
236
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
237
|
+
const sdk: any = await import('@sip-protocol/sdk')
|
|
238
|
+
const generateStealthAddress = sdk.generateStealthAddress as (
|
|
239
|
+
metaAddress: { chain: string; spendingKey: string; viewingKey: string }
|
|
240
|
+
) => StealthAddressResult
|
|
241
|
+
|
|
242
|
+
const stealthData = generateStealthAddress(metaAddressObj)
|
|
243
|
+
|
|
244
|
+
setStealthAddress(stealthData.stealthAddress.address)
|
|
245
|
+
setError(null)
|
|
246
|
+
} catch (err) {
|
|
247
|
+
const error = err instanceof Error ? err : new Error('Failed to regenerate stealth address')
|
|
248
|
+
setError(error)
|
|
249
|
+
} finally {
|
|
250
|
+
setIsGenerating(false)
|
|
251
|
+
}
|
|
252
|
+
}, 0)
|
|
253
|
+
}, [metaAddress])
|
|
254
|
+
|
|
255
|
+
// Copy to clipboard (native)
|
|
256
|
+
const copyToClipboard = useCallback(async (): Promise<boolean> => {
|
|
257
|
+
if (!stealthAddress) return false
|
|
258
|
+
const success = await nativeCopyToClipboard(stealthAddress)
|
|
259
|
+
if (!success) {
|
|
260
|
+
setError(new Error('Failed to copy to clipboard'))
|
|
261
|
+
} else {
|
|
262
|
+
setError(null)
|
|
263
|
+
}
|
|
264
|
+
return success
|
|
265
|
+
}, [stealthAddress])
|
|
266
|
+
|
|
267
|
+
// Save to secure storage
|
|
268
|
+
const saveToKeychain = useCallback(async (): Promise<boolean> => {
|
|
269
|
+
if (!spendingPrivateKey || !viewingPrivateKey || !metaAddress) {
|
|
270
|
+
setError(new Error('No keys to save'))
|
|
271
|
+
return false
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
await SecureStorage.setSpendingKey(walletId, spendingPrivateKey, { requireBiometrics })
|
|
276
|
+
await SecureStorage.setViewingKey(walletId, viewingPrivateKey, { requireBiometrics })
|
|
277
|
+
await SecureStorage.setMetaAddress(walletId, metaAddress, { requireBiometrics })
|
|
278
|
+
setError(null)
|
|
279
|
+
return true
|
|
280
|
+
} catch (err) {
|
|
281
|
+
const error = err instanceof Error ? err : new Error('Failed to save to keychain')
|
|
282
|
+
setError(error)
|
|
283
|
+
return false
|
|
284
|
+
}
|
|
285
|
+
}, [spendingPrivateKey, viewingPrivateKey, metaAddress, walletId, requireBiometrics])
|
|
286
|
+
|
|
287
|
+
// Load from secure storage
|
|
288
|
+
const loadFromKeychain = useCallback(async (): Promise<boolean> => {
|
|
289
|
+
try {
|
|
290
|
+
const storedMeta = await SecureStorage.getMetaAddress(walletId, { requireBiometrics })
|
|
291
|
+
const storedSpending = await SecureStorage.getSpendingKey(walletId, { requireBiometrics })
|
|
292
|
+
const storedViewing = await SecureStorage.getViewingKey(walletId, { requireBiometrics })
|
|
293
|
+
|
|
294
|
+
if (!storedMeta || !storedSpending || !storedViewing) {
|
|
295
|
+
setError(new Error('No keys found in keychain'))
|
|
296
|
+
return false
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
setMetaAddress(storedMeta)
|
|
300
|
+
setSpendingPrivateKey(storedSpending)
|
|
301
|
+
setViewingPrivateKey(storedViewing)
|
|
302
|
+
setError(null)
|
|
303
|
+
return true
|
|
304
|
+
} catch (err) {
|
|
305
|
+
const error = err instanceof Error ? err : new Error('Failed to load from keychain')
|
|
306
|
+
setError(error)
|
|
307
|
+
return false
|
|
308
|
+
}
|
|
309
|
+
}, [walletId, requireBiometrics])
|
|
310
|
+
|
|
311
|
+
// Clear error
|
|
312
|
+
const clearError = useCallback(() => {
|
|
313
|
+
setError(null)
|
|
314
|
+
}, [])
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
metaAddress,
|
|
318
|
+
stealthAddress,
|
|
319
|
+
spendingPrivateKey,
|
|
320
|
+
viewingPrivateKey,
|
|
321
|
+
isGenerating,
|
|
322
|
+
error,
|
|
323
|
+
regenerate,
|
|
324
|
+
copyToClipboard,
|
|
325
|
+
saveToKeychain,
|
|
326
|
+
loadFromKeychain,
|
|
327
|
+
clearError,
|
|
328
|
+
}
|
|
329
|
+
}
|