@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,252 @@
1
+ /**
2
+ * useStealthTransfer - Mobile-optimized private transfer hook
3
+ *
4
+ * Handles shielded SPL token transfers on Solana from mobile devices.
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * import { useStealthTransfer } from '@sip-protocol/react-native'
9
+ *
10
+ * function SendScreen() {
11
+ * const { transfer, status, error, isLoading } = useStealthTransfer({
12
+ * connection,
13
+ * wallet: walletAdapter,
14
+ * })
15
+ *
16
+ * const handleSend = async () => {
17
+ * const result = await transfer({
18
+ * recipientMetaAddress: 'sip:solana:...',
19
+ * amount: 1000000n, // 1 USDC
20
+ * mint: USDC_MINT,
21
+ * })
22
+ * if (result.success) {
23
+ * Alert.alert('Success', 'Payment sent privately!')
24
+ * }
25
+ * }
26
+ *
27
+ * return (
28
+ * <TouchableOpacity onPress={handleSend} disabled={isLoading}>
29
+ * <Text>{isLoading ? 'Sending...' : 'Send Private Payment'}</Text>
30
+ * </TouchableOpacity>
31
+ * )
32
+ * }
33
+ * ```
34
+ */
35
+
36
+ import { useState, useCallback } from 'react'
37
+
38
+ /**
39
+ * Wallet adapter interface for mobile wallets
40
+ */
41
+ export interface MobileWalletAdapter {
42
+ publicKey: { toBase58(): string } | null
43
+ signTransaction: <T>(transaction: T) => Promise<T>
44
+ signAllTransactions?: <T>(transactions: T[]) => Promise<T[]>
45
+ }
46
+
47
+ /**
48
+ * Transfer status enum
49
+ */
50
+ export type TransferStatus =
51
+ | 'idle'
52
+ | 'preparing'
53
+ | 'signing'
54
+ | 'sending'
55
+ | 'confirming'
56
+ | 'success'
57
+ | 'error'
58
+
59
+ /**
60
+ * Parameters for stealth transfer
61
+ */
62
+ export interface TransferParams {
63
+ /** Recipient's stealth meta-address */
64
+ recipientMetaAddress: string
65
+ /** Amount in smallest units */
66
+ amount: bigint
67
+ /** SPL token mint address */
68
+ mint: string
69
+ }
70
+
71
+ /**
72
+ * Result of stealth transfer
73
+ */
74
+ export interface TransferResult {
75
+ success: boolean
76
+ signature?: string
77
+ stealthAddress?: string
78
+ error?: Error
79
+ }
80
+
81
+ /**
82
+ * Connection interface (subset of @solana/web3.js Connection)
83
+ */
84
+ export interface SolanaConnection {
85
+ confirmTransaction(
86
+ signature: string,
87
+ commitment?: string
88
+ ): Promise<{ value: { err: unknown } }>
89
+ }
90
+
91
+ /**
92
+ * Parameters for useStealthTransfer hook
93
+ */
94
+ export interface UseStealthTransferParams {
95
+ /** Solana connection */
96
+ connection: SolanaConnection
97
+ /** Wallet adapter */
98
+ wallet: MobileWalletAdapter
99
+ }
100
+
101
+ /**
102
+ * Return type for useStealthTransfer hook
103
+ */
104
+ export interface UseStealthTransferReturn {
105
+ /** Execute a private transfer */
106
+ transfer: (params: TransferParams) => Promise<TransferResult>
107
+ /** Current transfer status */
108
+ status: TransferStatus
109
+ /** Error if any occurred */
110
+ error: Error | null
111
+ /** Whether a transfer is in progress */
112
+ isLoading: boolean
113
+ /** Reset the hook state */
114
+ reset: () => void
115
+ }
116
+
117
+ /**
118
+ * Mobile-optimized stealth transfer hook
119
+ *
120
+ * @param params - Hook parameters
121
+ */
122
+ export function useStealthTransfer(
123
+ params: UseStealthTransferParams
124
+ ): UseStealthTransferReturn {
125
+ const { connection, wallet } = params
126
+
127
+ const [status, setStatus] = useState<TransferStatus>('idle')
128
+ const [error, setError] = useState<Error | null>(null)
129
+
130
+ const transfer = useCallback(
131
+ async (transferParams: TransferParams): Promise<TransferResult> => {
132
+ const { recipientMetaAddress, amount, mint } = transferParams
133
+
134
+ // Validate wallet connected
135
+ if (!wallet.publicKey) {
136
+ const err = new Error('Wallet not connected')
137
+ setError(err)
138
+ setStatus('error')
139
+ return { success: false, error: err }
140
+ }
141
+
142
+ try {
143
+ setStatus('preparing')
144
+ setError(null)
145
+
146
+ // Dynamic import SDK functions to avoid bundling issues
147
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
148
+ const sdk: any = await import('@sip-protocol/sdk')
149
+
150
+ // Check if sendPrivateSPLTransfer is available
151
+ if (!sdk.sendPrivateSPLTransfer) {
152
+ throw new Error(
153
+ 'sendPrivateSPLTransfer not available. Install @sip-protocol/sdk with Solana support.'
154
+ )
155
+ }
156
+
157
+ const sendPrivateSPLTransfer = sdk.sendPrivateSPLTransfer as (params: {
158
+ connection: unknown
159
+ sender: unknown
160
+ senderTokenAccount: unknown
161
+ recipientMetaAddress: string
162
+ mint: unknown
163
+ amount: bigint
164
+ signTransaction: unknown
165
+ }) => Promise<{ signature: string; stealthAddress: string }>
166
+
167
+ // Dynamic import Solana libraries
168
+ const { PublicKey } = await import('@solana/web3.js')
169
+ const { getAssociatedTokenAddress } = await import('@solana/spl-token')
170
+
171
+ // Get sender's token account
172
+ const mintPubkey = new PublicKey(mint)
173
+ const senderTokenAccount = await getAssociatedTokenAddress(
174
+ mintPubkey,
175
+ new PublicKey(wallet.publicKey.toBase58())
176
+ )
177
+
178
+ setStatus('signing')
179
+
180
+ // Execute private transfer
181
+ const result = await sendPrivateSPLTransfer({
182
+ connection,
183
+ sender: new PublicKey(wallet.publicKey.toBase58()),
184
+ senderTokenAccount,
185
+ recipientMetaAddress,
186
+ mint: mintPubkey,
187
+ amount,
188
+ signTransaction: wallet.signTransaction,
189
+ })
190
+
191
+ setStatus('confirming')
192
+
193
+ // Wait for confirmation
194
+ const confirmation = await connection.confirmTransaction(
195
+ result.signature,
196
+ 'confirmed'
197
+ )
198
+
199
+ if (confirmation.value.err) {
200
+ throw new Error(`Transaction failed: ${confirmation.value.err}`)
201
+ }
202
+
203
+ setStatus('success')
204
+ setError(null)
205
+
206
+ return {
207
+ success: true,
208
+ signature: result.signature,
209
+ stealthAddress: result.stealthAddress,
210
+ }
211
+ } catch (err) {
212
+ const error = err instanceof Error ? err : new Error('Transfer failed')
213
+ setError(error)
214
+ setStatus('error')
215
+ return { success: false, error }
216
+ }
217
+ },
218
+ [connection, wallet]
219
+ )
220
+
221
+ const reset = useCallback(() => {
222
+ setStatus('idle')
223
+ setError(null)
224
+ }, [])
225
+
226
+ return {
227
+ transfer,
228
+ status,
229
+ error,
230
+ isLoading: status !== 'idle' && status !== 'success' && status !== 'error',
231
+ reset,
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Get associated token address helper
237
+ *
238
+ * Dynamically imports from @solana/spl-token
239
+ *
240
+ * @param mint - Token mint address string
241
+ * @param owner - Owner address string
242
+ * @returns Associated token address
243
+ */
244
+ export async function getAssociatedTokenAddress(
245
+ mint: string,
246
+ owner: string
247
+ ): Promise<string> {
248
+ const { PublicKey } = await import('@solana/web3.js')
249
+ const { getAssociatedTokenAddress: getATA } = await import('@solana/spl-token')
250
+ const ata = await getATA(new PublicKey(mint), new PublicKey(owner))
251
+ return ata.toBase58()
252
+ }
package/src/index.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @sip-protocol/react-native
3
+ *
4
+ * React Native SDK for Shielded Intents Protocol - privacy on iOS/Android.
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * import {
9
+ * useStealthAddress,
10
+ * useStealthTransfer,
11
+ * useScanPayments,
12
+ * SecureStorage,
13
+ * } from '@sip-protocol/react-native'
14
+ *
15
+ * function PrivacyWallet() {
16
+ * const { metaAddress, saveToKeychain } = useStealthAddress('solana', {
17
+ * autoSave: true,
18
+ * requireBiometrics: true,
19
+ * })
20
+ *
21
+ * return <Text>Your private address: {metaAddress}</Text>
22
+ * }
23
+ * ```
24
+ *
25
+ * @packageDocumentation
26
+ */
27
+
28
+ // Hooks
29
+ export {
30
+ // Stealth address generation
31
+ useStealthAddress,
32
+ type UseStealthAddressOptions,
33
+ type UseStealthAddressReturn,
34
+ // Private transfers
35
+ useStealthTransfer,
36
+ type MobileWalletAdapter,
37
+ type TransferStatus,
38
+ type TransferParams,
39
+ type TransferResult,
40
+ type UseStealthTransferParams,
41
+ type UseStealthTransferReturn,
42
+ getAssociatedTokenAddress,
43
+ // Payment scanning
44
+ useScanPayments,
45
+ type ScannedPayment,
46
+ type ScanStatus,
47
+ type UseScanPaymentsParams,
48
+ type UseScanPaymentsReturn,
49
+ } from './hooks'
50
+
51
+ // Secure Storage (Keychain/Keystore)
52
+ export { SecureStorage, type SecureStorageOptions, type KeyType } from './storage'
53
+
54
+ // Utilities
55
+ export { copyToClipboard, readFromClipboard, isClipboardAvailable } from './utils'
56
+
57
+ // Re-export core types for convenience
58
+ export type { ChainId, Asset, HexString, PrivacyLevel } from '@sip-protocol/types'
@@ -0,0 +1 @@
1
+ export { SecureStorage, type SecureStorageOptions, type KeyType } from './secure-storage'
@@ -0,0 +1,395 @@
1
+ /**
2
+ * Secure Key Storage for React Native
3
+ *
4
+ * Provides secure storage for private keys using native platform capabilities:
5
+ * - iOS: Keychain with biometric protection
6
+ * - Android: Keystore with biometric protection
7
+ *
8
+ * Falls back to in-memory storage if react-native-keychain is not available.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { SecureStorage } from '@sip-protocol/react-native'
13
+ *
14
+ * // Store viewing key
15
+ * await SecureStorage.setViewingKey('my-wallet', viewingPrivateKey)
16
+ *
17
+ * // Retrieve with biometric prompt
18
+ * const key = await SecureStorage.getViewingKey('my-wallet')
19
+ *
20
+ * // Clear on logout
21
+ * await SecureStorage.clearAll()
22
+ * ```
23
+ */
24
+
25
+ /**
26
+ * Storage options for secure key storage
27
+ */
28
+ export interface SecureStorageOptions {
29
+ /** Key identifier/service name */
30
+ service?: string
31
+ /** Require biometric authentication to access */
32
+ requireBiometrics?: boolean
33
+ /** iOS: Accessibility level */
34
+ accessible?: 'whenUnlocked' | 'afterFirstUnlock' | 'always'
35
+ /** Storage backend to use */
36
+ backend?: 'keychain' | 'memory'
37
+ }
38
+
39
+ /**
40
+ * Default service name for SIP keys
41
+ */
42
+ const DEFAULT_SERVICE = 'com.sip-protocol.keys'
43
+
44
+ /**
45
+ * Key prefixes for different key types
46
+ */
47
+ const KEY_PREFIXES = {
48
+ spending: 'sip:spending:',
49
+ viewing: 'sip:viewing:',
50
+ ephemeral: 'sip:ephemeral:',
51
+ meta: 'sip:meta:',
52
+ } as const
53
+
54
+ type KeyType = keyof typeof KEY_PREFIXES
55
+
56
+ /**
57
+ * In-memory fallback storage (for testing or when keychain unavailable)
58
+ */
59
+ const memoryStorage = new Map<string, string>()
60
+
61
+ /**
62
+ * Keychain module interface (subset of react-native-keychain)
63
+ * Defined locally to avoid requiring type declarations at build time
64
+ */
65
+ interface KeychainModule {
66
+ ACCESSIBLE: {
67
+ WHEN_UNLOCKED: number
68
+ AFTER_FIRST_UNLOCK: number
69
+ ALWAYS: number
70
+ WHEN_UNLOCKED_THIS_DEVICE_ONLY: number
71
+ }
72
+ ACCESS_CONTROL: {
73
+ BIOMETRY_CURRENT_SET: number
74
+ }
75
+ AUTHENTICATION_TYPE: {
76
+ BIOMETRICS: number
77
+ }
78
+ setGenericPassword(
79
+ username: string,
80
+ password: string,
81
+ options?: {
82
+ service?: string
83
+ accessible?: number
84
+ accessControl?: number
85
+ authenticationType?: number
86
+ }
87
+ ): Promise<boolean | { service: string; storage: string }>
88
+ getGenericPassword(options?: {
89
+ service?: string
90
+ authenticationPrompt?: {
91
+ title: string
92
+ subtitle?: string
93
+ description?: string
94
+ cancel?: string
95
+ }
96
+ }): Promise<false | { username: string; password: string; service: string; storage: string }>
97
+ resetGenericPassword(options?: { service?: string }): Promise<boolean>
98
+ getSupportedBiometryType(): Promise<string | null>
99
+ }
100
+
101
+ /**
102
+ * Check if react-native-keychain is available
103
+ */
104
+ let Keychain: KeychainModule | null = null
105
+ try {
106
+ // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
107
+ Keychain = require('react-native-keychain') as KeychainModule
108
+ } catch {
109
+ // Keychain not available, will use memory fallback
110
+ }
111
+
112
+ /**
113
+ * Get the appropriate storage backend
114
+ */
115
+ function getBackend(options?: SecureStorageOptions): 'keychain' | 'memory' {
116
+ if (options?.backend) {
117
+ return options.backend
118
+ }
119
+ return Keychain ? 'keychain' : 'memory'
120
+ }
121
+
122
+ /**
123
+ * Build the full key name for storage
124
+ */
125
+ function buildKeyName(type: KeyType, identifier: string): string {
126
+ return `${KEY_PREFIXES[type]}${identifier}`
127
+ }
128
+
129
+ /**
130
+ * Store a key securely
131
+ *
132
+ * @param type - Type of key (spending, viewing, ephemeral, meta)
133
+ * @param identifier - Unique identifier (e.g., wallet address)
134
+ * @param value - Key value (hex string)
135
+ * @param options - Storage options
136
+ */
137
+ async function setKey(
138
+ type: KeyType,
139
+ identifier: string,
140
+ value: string,
141
+ options?: SecureStorageOptions
142
+ ): Promise<boolean> {
143
+ const keyName = buildKeyName(type, identifier)
144
+ const backend = getBackend(options)
145
+
146
+ if (backend === 'memory') {
147
+ memoryStorage.set(keyName, value)
148
+ return true
149
+ }
150
+
151
+ if (!Keychain) {
152
+ throw new Error(
153
+ 'react-native-keychain is required for secure storage. ' +
154
+ 'Install it or use backend: "memory" for testing.'
155
+ )
156
+ }
157
+
158
+ const service = options?.service ?? DEFAULT_SERVICE
159
+
160
+ // Build keychain options
161
+ const keychainOptions: {
162
+ service?: string
163
+ accessible?: number
164
+ accessControl?: number
165
+ authenticationType?: number
166
+ } = {
167
+ service,
168
+ accessible: mapAccessible(options?.accessible),
169
+ }
170
+
171
+ // Add biometric authentication if requested
172
+ if (options?.requireBiometrics) {
173
+ keychainOptions.accessControl = Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET
174
+ keychainOptions.authenticationType =
175
+ Keychain.AUTHENTICATION_TYPE.BIOMETRICS
176
+ }
177
+
178
+ const result = await Keychain.setGenericPassword(
179
+ keyName,
180
+ value,
181
+ keychainOptions
182
+ )
183
+
184
+ return !!result
185
+ }
186
+
187
+ /**
188
+ * Retrieve a key from secure storage
189
+ *
190
+ * @param type - Type of key (spending, viewing, ephemeral, meta)
191
+ * @param identifier - Unique identifier (e.g., wallet address)
192
+ * @param options - Storage options
193
+ * @returns Key value or null if not found
194
+ */
195
+ async function getKey(
196
+ type: KeyType,
197
+ identifier: string,
198
+ options?: SecureStorageOptions
199
+ ): Promise<string | null> {
200
+ const keyName = buildKeyName(type, identifier)
201
+ const backend = getBackend(options)
202
+
203
+ if (backend === 'memory') {
204
+ return memoryStorage.get(keyName) ?? null
205
+ }
206
+
207
+ if (!Keychain) {
208
+ throw new Error(
209
+ 'react-native-keychain is required for secure storage. ' +
210
+ 'Install it or use backend: "memory" for testing.'
211
+ )
212
+ }
213
+
214
+ const service = options?.service ?? DEFAULT_SERVICE
215
+
216
+ const keychainOptions: {
217
+ service?: string
218
+ authenticationPrompt?: {
219
+ title: string
220
+ subtitle?: string
221
+ description?: string
222
+ cancel?: string
223
+ }
224
+ } = {
225
+ service,
226
+ }
227
+
228
+ // Add biometric prompt if required
229
+ if (options?.requireBiometrics) {
230
+ keychainOptions.authenticationPrompt = {
231
+ title: 'Authenticate to access key',
232
+ subtitle: 'SIP Protocol requires authentication',
233
+ description: 'Use biometrics to unlock your private keys',
234
+ cancel: 'Cancel',
235
+ }
236
+ }
237
+
238
+ const result = await Keychain.getGenericPassword(keychainOptions)
239
+
240
+ if (!result) {
241
+ return null
242
+ }
243
+
244
+ // Verify we got the right key
245
+ if (result.username !== keyName) {
246
+ return null
247
+ }
248
+
249
+ return result.password
250
+ }
251
+
252
+ /**
253
+ * Delete a key from secure storage
254
+ *
255
+ * @param type - Type of key (spending, viewing, ephemeral, meta)
256
+ * @param identifier - Unique identifier (e.g., wallet address)
257
+ * @param options - Storage options
258
+ */
259
+ async function deleteKey(
260
+ type: KeyType,
261
+ identifier: string,
262
+ options?: SecureStorageOptions
263
+ ): Promise<boolean> {
264
+ const keyName = buildKeyName(type, identifier)
265
+ const backend = getBackend(options)
266
+
267
+ if (backend === 'memory') {
268
+ return memoryStorage.delete(keyName)
269
+ }
270
+
271
+ if (!Keychain) {
272
+ throw new Error('react-native-keychain is required for secure storage.')
273
+ }
274
+
275
+ const service = options?.service ?? DEFAULT_SERVICE
276
+
277
+ return await Keychain.resetGenericPassword({ service })
278
+ }
279
+
280
+ /**
281
+ * Clear all stored keys
282
+ *
283
+ * @param options - Storage options
284
+ */
285
+ async function clearAll(options?: SecureStorageOptions): Promise<boolean> {
286
+ const backend = getBackend(options)
287
+
288
+ if (backend === 'memory') {
289
+ memoryStorage.clear()
290
+ return true
291
+ }
292
+
293
+ if (!Keychain) {
294
+ throw new Error('react-native-keychain is required for secure storage.')
295
+ }
296
+
297
+ const service = options?.service ?? DEFAULT_SERVICE
298
+
299
+ return await Keychain.resetGenericPassword({ service })
300
+ }
301
+
302
+ /**
303
+ * Map our accessibility levels to keychain constants
304
+ */
305
+ function mapAccessible(
306
+ level?: 'whenUnlocked' | 'afterFirstUnlock' | 'always'
307
+ ): number | undefined {
308
+ if (!Keychain) return undefined
309
+
310
+ switch (level) {
311
+ case 'whenUnlocked':
312
+ return Keychain.ACCESSIBLE.WHEN_UNLOCKED
313
+ case 'afterFirstUnlock':
314
+ return Keychain.ACCESSIBLE.AFTER_FIRST_UNLOCK
315
+ case 'always':
316
+ return Keychain.ACCESSIBLE.ALWAYS
317
+ default:
318
+ return Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Check if biometrics are available on this device
324
+ *
325
+ * @returns Biometrics support info
326
+ */
327
+ async function getSupportedBiometrics(): Promise<{
328
+ available: boolean
329
+ biometryType: 'FaceID' | 'TouchID' | 'Fingerprint' | 'None'
330
+ }> {
331
+ if (!Keychain) {
332
+ return { available: false, biometryType: 'None' }
333
+ }
334
+
335
+ const biometryType = await Keychain.getSupportedBiometryType()
336
+
337
+ if (!biometryType) {
338
+ return { available: false, biometryType: 'None' }
339
+ }
340
+
341
+ return {
342
+ available: true,
343
+ biometryType: biometryType as 'FaceID' | 'TouchID' | 'Fingerprint',
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Check if secure storage is available
349
+ */
350
+ function isAvailable(): boolean {
351
+ return !!Keychain
352
+ }
353
+
354
+ /**
355
+ * SecureStorage API
356
+ *
357
+ * Unified API for secure key storage on mobile devices.
358
+ */
359
+ export const SecureStorage = {
360
+ // Key operations
361
+ setKey,
362
+ getKey,
363
+ deleteKey,
364
+ clearAll,
365
+
366
+ // Convenience methods for viewing keys
367
+ setViewingKey: (identifier: string, key: string, options?: SecureStorageOptions) =>
368
+ setKey('viewing', identifier, key, options),
369
+ getViewingKey: (identifier: string, options?: SecureStorageOptions) =>
370
+ getKey('viewing', identifier, options),
371
+ deleteViewingKey: (identifier: string, options?: SecureStorageOptions) =>
372
+ deleteKey('viewing', identifier, options),
373
+
374
+ // Convenience methods for spending keys
375
+ setSpendingKey: (identifier: string, key: string, options?: SecureStorageOptions) =>
376
+ setKey('spending', identifier, key, options),
377
+ getSpendingKey: (identifier: string, options?: SecureStorageOptions) =>
378
+ getKey('spending', identifier, options),
379
+ deleteSpendingKey: (identifier: string, options?: SecureStorageOptions) =>
380
+ deleteKey('spending', identifier, options),
381
+
382
+ // Convenience methods for meta addresses
383
+ setMetaAddress: (identifier: string, meta: string, options?: SecureStorageOptions) =>
384
+ setKey('meta', identifier, meta, options),
385
+ getMetaAddress: (identifier: string, options?: SecureStorageOptions) =>
386
+ getKey('meta', identifier, options),
387
+ deleteMetaAddress: (identifier: string, options?: SecureStorageOptions) =>
388
+ deleteKey('meta', identifier, options),
389
+
390
+ // Biometrics support
391
+ getSupportedBiometrics,
392
+ isAvailable,
393
+ } as const
394
+
395
+ export type { KeyType }