@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,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 }
|