@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.
@@ -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
+ }