@spacesops/wdk-react-native-core 1.0.0-beta.40
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 +551 -0
- package/dist/__mocks__/react.d.ts +40 -0
- package/dist/__mocks__/react.js +33 -0
- package/dist/__mocks__/react.js.map +1 -0
- package/dist/__mocks__/secureStorage.d.ts +26 -0
- package/dist/__mocks__/secureStorage.js +80 -0
- package/dist/__mocks__/secureStorage.js.map +1 -0
- package/dist/__tests__/hooks/useBalance.test.d.ts +6 -0
- package/dist/__tests__/hooks/useBalance.test.js +217 -0
- package/dist/__tests__/hooks/useBalance.test.js.map +1 -0
- package/dist/__tests__/hooks/useWallet.test.d.ts +6 -0
- package/dist/__tests__/hooks/useWallet.test.js +105 -0
- package/dist/__tests__/hooks/useWallet.test.js.map +1 -0
- package/dist/__tests__/hooks/useWalletManager.test.d.ts +6 -0
- package/dist/__tests__/hooks/useWalletManager.test.js +193 -0
- package/dist/__tests__/hooks/useWalletManager.test.js.map +1 -0
- package/dist/__tests__/hooks/useWdkApp.test.d.ts +6 -0
- package/dist/__tests__/hooks/useWdkApp.test.js +55 -0
- package/dist/__tests__/hooks/useWdkApp.test.js.map +1 -0
- package/dist/__tests__/hooks/useWorklet.test.d.ts +6 -0
- package/dist/__tests__/hooks/useWorklet.test.js +168 -0
- package/dist/__tests__/hooks/useWorklet.test.js.map +1 -0
- package/dist/__tests__/provider/WdkAppProvider.test.d.ts +6 -0
- package/dist/__tests__/provider/WdkAppProvider.test.js +57 -0
- package/dist/__tests__/provider/WdkAppProvider.test.js.map +1 -0
- package/dist/__tests__/services/accountService.test.d.ts +6 -0
- package/dist/__tests__/services/accountService.test.js +216 -0
- package/dist/__tests__/services/accountService.test.js.map +1 -0
- package/dist/__tests__/services/addressService.test.d.ts +6 -0
- package/dist/__tests__/services/addressService.test.js +246 -0
- package/dist/__tests__/services/addressService.test.js.map +1 -0
- package/dist/__tests__/services/balanceService.test.d.ts +6 -0
- package/dist/__tests__/services/balanceService.test.js +333 -0
- package/dist/__tests__/services/balanceService.test.js.map +1 -0
- package/dist/__tests__/services/walletSetupService.test.d.ts +6 -0
- package/dist/__tests__/services/walletSetupService.test.js +304 -0
- package/dist/__tests__/services/walletSetupService.test.js.map +1 -0
- package/dist/__tests__/services/walletSwitchingService.test.d.ts +6 -0
- package/dist/__tests__/services/walletSwitchingService.test.js +209 -0
- package/dist/__tests__/services/walletSwitchingService.test.js.map +1 -0
- package/dist/__tests__/services/workletLifecycleService.test.d.ts +6 -0
- package/dist/__tests__/services/workletLifecycleService.test.js +451 -0
- package/dist/__tests__/services/workletLifecycleService.test.js.map +1 -0
- package/dist/__tests__/setup.d.ts +7 -0
- package/dist/__tests__/setup.js +49 -0
- package/dist/__tests__/setup.js.map +1 -0
- package/dist/__tests__/storage/mmkvStorage.test.d.ts +6 -0
- package/dist/__tests__/storage/mmkvStorage.test.js +213 -0
- package/dist/__tests__/storage/mmkvStorage.test.js.map +1 -0
- package/dist/__tests__/store/walletStore.test.d.ts +4 -0
- package/dist/__tests__/store/walletStore.test.js +86 -0
- package/dist/__tests__/store/walletStore.test.js.map +1 -0
- package/dist/__tests__/store/workletStore.test.d.ts +4 -0
- package/dist/__tests__/store/workletStore.test.js +255 -0
- package/dist/__tests__/store/workletStore.test.js.map +1 -0
- package/dist/__tests__/types/hrpc.test.d.ts +4 -0
- package/dist/__tests__/types/hrpc.test.js +99 -0
- package/dist/__tests__/types/hrpc.test.js.map +1 -0
- package/dist/__tests__/utils/balanceUtils.test.d.ts +4 -0
- package/dist/__tests__/utils/balanceUtils.test.js +133 -0
- package/dist/__tests__/utils/balanceUtils.test.js.map +1 -0
- package/dist/__tests__/utils/errorHandling.test.d.ts +6 -0
- package/dist/__tests__/utils/errorHandling.test.js +119 -0
- package/dist/__tests__/utils/errorHandling.test.js.map +1 -0
- package/dist/__tests__/utils/errorUtils.test.d.ts +4 -0
- package/dist/__tests__/utils/errorUtils.test.js +133 -0
- package/dist/__tests__/utils/errorUtils.test.js.map +1 -0
- package/dist/__tests__/utils/initializationState.test.d.ts +6 -0
- package/dist/__tests__/utils/initializationState.test.js +243 -0
- package/dist/__tests__/utils/initializationState.test.js.map +1 -0
- package/dist/__tests__/utils/jsonUtils.test.d.ts +4 -0
- package/dist/__tests__/utils/jsonUtils.test.js +118 -0
- package/dist/__tests__/utils/jsonUtils.test.js.map +1 -0
- package/dist/__tests__/utils/mmkvKeyManager.test.d.ts +6 -0
- package/dist/__tests__/utils/mmkvKeyManager.test.js +158 -0
- package/dist/__tests__/utils/mmkvKeyManager.test.js.map +1 -0
- package/dist/__tests__/utils/mnemonicUtils.test.d.ts +4 -0
- package/dist/__tests__/utils/mnemonicUtils.test.js +40 -0
- package/dist/__tests__/utils/mnemonicUtils.test.js.map +1 -0
- package/dist/__tests__/utils/raceConditions.test.d.ts +6 -0
- package/dist/__tests__/utils/raceConditions.test.js +239 -0
- package/dist/__tests__/utils/raceConditions.test.js.map +1 -0
- package/dist/__tests__/utils/result.test.d.ts +4 -0
- package/dist/__tests__/utils/result.test.js +66 -0
- package/dist/__tests__/utils/result.test.js.map +1 -0
- package/dist/__tests__/utils/schemas.test.d.ts +4 -0
- package/dist/__tests__/utils/schemas.test.js +66 -0
- package/dist/__tests__/utils/schemas.test.js.map +1 -0
- package/dist/__tests__/utils/storeHelpers.test.d.ts +4 -0
- package/dist/__tests__/utils/storeHelpers.test.js +227 -0
- package/dist/__tests__/utils/storeHelpers.test.js.map +1 -0
- package/dist/__tests__/utils/typeGuards.test.d.ts +4 -0
- package/dist/__tests__/utils/typeGuards.test.js +175 -0
- package/dist/__tests__/utils/typeGuards.test.js.map +1 -0
- package/dist/__tests__/utils/validation.test.d.ts +4 -0
- package/dist/__tests__/utils/validation.test.js +126 -0
- package/dist/__tests__/utils/validation.test.js.map +1 -0
- package/dist/__tests__/utils/walletUtils.test.d.ts +4 -0
- package/dist/__tests__/utils/walletUtils.test.js +156 -0
- package/dist/__tests__/utils/walletUtils.test.js.map +1 -0
- package/dist/hooks/useBalance.d.ts +170 -0
- package/dist/hooks/useBalance.js +442 -0
- package/dist/hooks/useBalance.js.map +1 -0
- package/dist/hooks/useWallet.d.ts +43 -0
- package/dist/hooks/useWallet.js +193 -0
- package/dist/hooks/useWallet.js.map +1 -0
- package/dist/hooks/useWalletManager.d.ts +100 -0
- package/dist/hooks/useWalletManager.js +526 -0
- package/dist/hooks/useWalletManager.js.map +1 -0
- package/dist/hooks/useWdkApp.d.ts +66 -0
- package/dist/hooks/useWdkApp.js +74 -0
- package/dist/hooks/useWdkApp.js.map +1 -0
- package/dist/hooks/useWorklet.d.ts +66 -0
- package/dist/hooks/useWorklet.js +42 -0
- package/dist/hooks/useWorklet.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/provider/WdkAppProvider.d.ts +112 -0
- package/dist/provider/WdkAppProvider.js +558 -0
- package/dist/provider/WdkAppProvider.js.map +1 -0
- package/dist/services/accountService.d.ts +52 -0
- package/dist/services/accountService.js +119 -0
- package/dist/services/accountService.js.map +1 -0
- package/dist/services/addressService.d.ts +22 -0
- package/dist/services/addressService.js +110 -0
- package/dist/services/addressService.js.map +1 -0
- package/dist/services/balanceService.d.ts +114 -0
- package/dist/services/balanceService.js +208 -0
- package/dist/services/balanceService.js.map +1 -0
- package/dist/services/walletSetupService.d.ts +217 -0
- package/dist/services/walletSetupService.js +535 -0
- package/dist/services/walletSetupService.js.map +1 -0
- package/dist/services/walletSwitchingService.d.ts +48 -0
- package/dist/services/walletSwitchingService.js +143 -0
- package/dist/services/walletSwitchingService.js.map +1 -0
- package/dist/services/workletLifecycleService.d.ts +117 -0
- package/dist/services/workletLifecycleService.js +433 -0
- package/dist/services/workletLifecycleService.js.map +1 -0
- package/dist/storage/mmkvStorage.d.ts +44 -0
- package/dist/storage/mmkvStorage.js +348 -0
- package/dist/storage/mmkvStorage.js.map +1 -0
- package/dist/store/walletStore.d.ts +130 -0
- package/dist/store/walletStore.js +204 -0
- package/dist/store/walletStore.js.map +1 -0
- package/dist/store/workletStore.d.ts +121 -0
- package/dist/store/workletStore.js +256 -0
- package/dist/store/workletStore.js.map +1 -0
- package/dist/types/hrpc.d.ts +60 -0
- package/dist/types/hrpc.js +26 -0
- package/dist/types/hrpc.js.map +1 -0
- package/dist/types.d.ts +168 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/balanceUtils.d.ts +30 -0
- package/dist/utils/balanceUtils.js +86 -0
- package/dist/utils/balanceUtils.js.map +1 -0
- package/dist/utils/constants.d.ts +66 -0
- package/dist/utils/constants.js +73 -0
- package/dist/utils/constants.js.map +1 -0
- package/dist/utils/errorHandling.d.ts +47 -0
- package/dist/utils/errorHandling.js +54 -0
- package/dist/utils/errorHandling.js.map +1 -0
- package/dist/utils/errorUtils.d.ts +89 -0
- package/dist/utils/errorUtils.js +264 -0
- package/dist/utils/errorUtils.js.map +1 -0
- package/dist/utils/initializationState.d.ts +129 -0
- package/dist/utils/initializationState.js +220 -0
- package/dist/utils/initializationState.js.map +1 -0
- package/dist/utils/jsonUtils.d.ts +21 -0
- package/dist/utils/jsonUtils.js +67 -0
- package/dist/utils/jsonUtils.js.map +1 -0
- package/dist/utils/logger.d.ts +27 -0
- package/dist/utils/logger.js +50 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/mmkvKeyManager.d.ts +57 -0
- package/dist/utils/mmkvKeyManager.js +214 -0
- package/dist/utils/mmkvKeyManager.js.map +1 -0
- package/dist/utils/mnemonicUtils.d.ts +17 -0
- package/dist/utils/mnemonicUtils.js +28 -0
- package/dist/utils/mnemonicUtils.js.map +1 -0
- package/dist/utils/operationMutex.d.ts +69 -0
- package/dist/utils/operationMutex.js +150 -0
- package/dist/utils/operationMutex.js.map +1 -0
- package/dist/utils/result.d.ts +34 -0
- package/dist/utils/result.js +43 -0
- package/dist/utils/result.js.map +1 -0
- package/dist/utils/schemas.d.ts +365 -0
- package/dist/utils/schemas.js +152 -0
- package/dist/utils/schemas.js.map +1 -0
- package/dist/utils/storeHelpers.d.ts +112 -0
- package/dist/utils/storeHelpers.js +198 -0
- package/dist/utils/storeHelpers.js.map +1 -0
- package/dist/utils/typeGuards.d.ts +70 -0
- package/dist/utils/typeGuards.js +95 -0
- package/dist/utils/typeGuards.js.map +1 -0
- package/dist/utils/validation.d.ts +53 -0
- package/dist/utils/validation.js +152 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/utils/walletStateHelpers.d.ts +62 -0
- package/dist/utils/walletStateHelpers.js +81 -0
- package/dist/utils/walletStateHelpers.js.map +1 -0
- package/dist/utils/walletUtils.d.ts +38 -0
- package/dist/utils/walletUtils.js +87 -0
- package/dist/utils/walletUtils.js.map +1 -0
- package/package.json +79 -0
- package/src/__mocks__/react.ts +34 -0
- package/src/__mocks__/secureStorage.ts +87 -0
- package/src/__tests__/hooks/useBalance.test.ts +286 -0
- package/src/__tests__/hooks/useWallet.test.ts +122 -0
- package/src/__tests__/hooks/useWalletManager.test.ts +271 -0
- package/src/__tests__/hooks/useWdkApp.test.tsx +59 -0
- package/src/__tests__/hooks/useWorklet.test.ts +195 -0
- package/src/__tests__/provider/WdkAppProvider.test.tsx +67 -0
- package/src/__tests__/services/accountService.test.ts +337 -0
- package/src/__tests__/services/addressService.test.ts +312 -0
- package/src/__tests__/services/balanceService.test.ts +411 -0
- package/src/__tests__/services/walletSetupService.test.ts +463 -0
- package/src/__tests__/services/walletSwitchingService.test.ts +258 -0
- package/src/__tests__/services/workletLifecycleService.test.ts +514 -0
- package/src/__tests__/setup.ts +54 -0
- package/src/__tests__/storage/mmkvStorage.test.ts +325 -0
- package/src/__tests__/store/walletStore.test.ts +101 -0
- package/src/__tests__/store/workletStore.test.ts +329 -0
- package/src/__tests__/types/hrpc.test.ts +125 -0
- package/src/__tests__/utils/balanceUtils.test.ts +151 -0
- package/src/__tests__/utils/errorHandling.test.ts +147 -0
- package/src/__tests__/utils/errorUtils.test.ts +170 -0
- package/src/__tests__/utils/initializationState.test.ts +337 -0
- package/src/__tests__/utils/jsonUtils.test.ts +131 -0
- package/src/__tests__/utils/mmkvKeyManager.test.ts +205 -0
- package/src/__tests__/utils/mnemonicUtils.test.ts +48 -0
- package/src/__tests__/utils/raceConditions.test.ts +292 -0
- package/src/__tests__/utils/result.test.ts +73 -0
- package/src/__tests__/utils/schemas.test.ts +82 -0
- package/src/__tests__/utils/storeHelpers.test.ts +323 -0
- package/src/__tests__/utils/typeGuards.test.ts +209 -0
- package/src/__tests__/utils/validation.test.ts +152 -0
- package/src/__tests__/utils/walletUtils.test.ts +195 -0
- package/src/hooks/useBalance.ts +597 -0
- package/src/hooks/useWallet.ts +288 -0
- package/src/hooks/useWalletManager.ts +654 -0
- package/src/hooks/useWdkApp.ts +78 -0
- package/src/hooks/useWorklet.ts +109 -0
- package/src/index.ts +112 -0
- package/src/provider/WdkAppProvider.tsx +764 -0
- package/src/services/accountService.ts +134 -0
- package/src/services/addressService.ts +125 -0
- package/src/services/balanceService.ts +291 -0
- package/src/services/walletSetupService.ts +695 -0
- package/src/services/walletSwitchingService.ts +157 -0
- package/src/services/workletLifecycleService.ts +561 -0
- package/src/storage/mmkvStorage.ts +417 -0
- package/src/store/walletStore.ts +301 -0
- package/src/store/workletStore.ts +340 -0
- package/src/types/hrpc.ts +78 -0
- package/src/types.ts +188 -0
- package/src/utils/balanceUtils.ts +96 -0
- package/src/utils/constants.ts +84 -0
- package/src/utils/errorHandling.md +163 -0
- package/src/utils/errorHandling.ts +61 -0
- package/src/utils/errorUtils.ts +300 -0
- package/src/utils/initializationState.ts +242 -0
- package/src/utils/jsonUtils.ts +75 -0
- package/src/utils/logger.ts +54 -0
- package/src/utils/mmkvKeyManager.ts +256 -0
- package/src/utils/mnemonicUtils.ts +35 -0
- package/src/utils/operationMutex.ts +181 -0
- package/src/utils/result.ts +57 -0
- package/src/utils/schemas.ts +190 -0
- package/src/utils/storeHelpers.ts +236 -0
- package/src/utils/typeGuards.ts +129 -0
- package/src/utils/validation.ts +174 -0
- package/src/utils/walletStateHelpers.ts +104 -0
- package/src/utils/walletUtils.ts +107 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MMKV Encryption Key Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages encryption keys for MMKV storage on a per-account basis.
|
|
5
|
+
* Each account (identified by email/identifier) gets its own encryption key,
|
|
6
|
+
* allowing multiple accounts on the same device with isolated encrypted storage.
|
|
7
|
+
*
|
|
8
|
+
* SECURITY NOTE: MMKV stores NON-SENSITIVE data only (addresses, balances, metadata).
|
|
9
|
+
* Since the data is non-sensitive, we use DETERMINISTIC key derivation from account identifier.
|
|
10
|
+
* This allows the same account to access the same encrypted data across devices.
|
|
11
|
+
*
|
|
12
|
+
* IMPORTANT: For sensitive data (wallet seeds, encryption keys), use SecureStorage which
|
|
13
|
+
* uses randomly generated keys stored in the device keychain.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import * as Crypto from 'expo-crypto'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Account identifier type (typically email or user ID)
|
|
20
|
+
*/
|
|
21
|
+
export type AccountIdentifier = string
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convert Uint8Array to base64 string using standard encoding
|
|
25
|
+
* This implementation follows RFC 4648 and handles all edge cases correctly
|
|
26
|
+
*
|
|
27
|
+
* @param bytes - Uint8Array to convert
|
|
28
|
+
* @returns Base64 encoded string
|
|
29
|
+
*/
|
|
30
|
+
function bytesToBase64(bytes: Uint8Array): string {
|
|
31
|
+
// Try to use Buffer if available (common in React Native with polyfills)
|
|
32
|
+
if (typeof Buffer !== 'undefined') {
|
|
33
|
+
return Buffer.from(bytes).toString('base64')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Fallback to manual encoding for environments without Buffer
|
|
37
|
+
const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
|
38
|
+
let result = ''
|
|
39
|
+
|
|
40
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
41
|
+
const a = bytes[i] ?? 0
|
|
42
|
+
const b = i + 1 < bytes.length ? (bytes[i + 1] ?? 0) : 0
|
|
43
|
+
const c = i + 2 < bytes.length ? (bytes[i + 2] ?? 0) : 0
|
|
44
|
+
|
|
45
|
+
const bitmap = (a << 16) | (b << 8) | c
|
|
46
|
+
|
|
47
|
+
result += base64Chars.charAt((bitmap >> 18) & 63)
|
|
48
|
+
result += base64Chars.charAt((bitmap >> 12) & 63)
|
|
49
|
+
|
|
50
|
+
if (i + 1 < bytes.length) {
|
|
51
|
+
result += base64Chars.charAt((bitmap >> 6) & 63)
|
|
52
|
+
} else {
|
|
53
|
+
result += '='
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (i + 2 < bytes.length) {
|
|
57
|
+
result += base64Chars.charAt(bitmap & 63)
|
|
58
|
+
} else {
|
|
59
|
+
result += '='
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return result
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Maximum number of keys to cache before evicting least recently used
|
|
68
|
+
* This prevents unbounded memory growth while maintaining performance
|
|
69
|
+
*/
|
|
70
|
+
const MAX_CACHE_SIZE = 100
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Cache for derived keys to avoid repeated async operations
|
|
74
|
+
* Since key derivation is deterministic, we can cache the results
|
|
75
|
+
* Uses LRU (Least Recently Used) eviction policy to limit memory usage
|
|
76
|
+
*/
|
|
77
|
+
const keyCache = new Map<string, string>()
|
|
78
|
+
const keyAccessOrder = new Map<string, number>()
|
|
79
|
+
let accessCounter = 0
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Evict least recently used key from cache when limit is reached
|
|
83
|
+
*/
|
|
84
|
+
function evictLRUKey(): void {
|
|
85
|
+
if (keyCache.size < MAX_CACHE_SIZE) {
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Find the least recently used key
|
|
90
|
+
let oldestKey: string | null = null
|
|
91
|
+
let oldestAccess = Infinity
|
|
92
|
+
|
|
93
|
+
for (const [key, accessTime] of keyAccessOrder.entries()) {
|
|
94
|
+
if (accessTime < oldestAccess) {
|
|
95
|
+
oldestAccess = accessTime
|
|
96
|
+
oldestKey = key
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Remove the least recently used key
|
|
101
|
+
if (oldestKey !== null) {
|
|
102
|
+
keyCache.delete(oldestKey)
|
|
103
|
+
keyAccessOrder.delete(oldestKey)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function deriveKeyFromAccount(accountIdentifier: AccountIdentifier): Promise<string> {
|
|
108
|
+
// Check cache first
|
|
109
|
+
const cachedKey = keyCache.get(accountIdentifier)
|
|
110
|
+
if (cachedKey !== undefined) {
|
|
111
|
+
// Update access time for LRU tracking
|
|
112
|
+
accessCounter++
|
|
113
|
+
keyAccessOrder.set(accountIdentifier, accessCounter)
|
|
114
|
+
return cachedKey
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Use a constant salt for key derivation
|
|
118
|
+
// This salt is public and part of the derivation algorithm
|
|
119
|
+
const SALT = 'wdk-mmkv-encryption-salt-v1'
|
|
120
|
+
const input = `${SALT}:${accountIdentifier}`
|
|
121
|
+
|
|
122
|
+
// Use expo-crypto for production-grade SHA-256 hashing
|
|
123
|
+
// This is a well-tested, battle-hardened implementation
|
|
124
|
+
const hashHex = await Crypto.digestStringAsync(
|
|
125
|
+
Crypto.CryptoDigestAlgorithm.SHA256,
|
|
126
|
+
input
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
// Convert hex string to base64 using standard encoding
|
|
130
|
+
// SHA-256 produces 64 hex characters (32 bytes)
|
|
131
|
+
// Convert hex to bytes, then to base64
|
|
132
|
+
const hexBytes = hashHex.match(/.{1,2}/g)
|
|
133
|
+
if (!hexBytes || hashHex.length !== 64) {
|
|
134
|
+
throw new Error(`Invalid SHA-256 hash format: expected 64 hex characters, got ${hashHex.length}`)
|
|
135
|
+
}
|
|
136
|
+
const hashBytes = new Uint8Array(
|
|
137
|
+
hexBytes.map((byte: string) => parseInt(byte, 16))
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
// Convert bytes to base64 using standard encoding
|
|
141
|
+
// Use a well-tested base64 encoding implementation
|
|
142
|
+
const key = bytesToBase64(hashBytes)
|
|
143
|
+
|
|
144
|
+
// Evict LRU key if cache is full
|
|
145
|
+
evictLRUKey()
|
|
146
|
+
|
|
147
|
+
// Cache the result for future use
|
|
148
|
+
accessCounter++
|
|
149
|
+
keyCache.set(accountIdentifier, key)
|
|
150
|
+
keyAccessOrder.set(accountIdentifier, accessCounter)
|
|
151
|
+
|
|
152
|
+
return key
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Maximum length for account identifiers to prevent DoS attacks
|
|
158
|
+
* 256 characters is reasonable for emails and user IDs
|
|
159
|
+
*/
|
|
160
|
+
const MAX_ACCOUNT_IDENTIFIER_LENGTH = 256
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Validate account identifier input
|
|
164
|
+
*
|
|
165
|
+
* @param accountIdentifier - Account identifier to validate
|
|
166
|
+
* @throws Error if validation fails
|
|
167
|
+
*/
|
|
168
|
+
function validateAccountIdentifier(accountIdentifier: AccountIdentifier): void {
|
|
169
|
+
if (!accountIdentifier || typeof accountIdentifier !== 'string') {
|
|
170
|
+
throw new Error('Account identifier must be a non-empty string')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const trimmed = accountIdentifier.trim()
|
|
174
|
+
if (trimmed === '') {
|
|
175
|
+
throw new Error('Account identifier cannot be empty or whitespace only')
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (trimmed.length > MAX_ACCOUNT_IDENTIFIER_LENGTH) {
|
|
179
|
+
throw new Error(`Account identifier exceeds maximum length of ${MAX_ACCOUNT_IDENTIFIER_LENGTH} characters`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check for valid UTF-8 encoding (basic check)
|
|
183
|
+
try {
|
|
184
|
+
// Ensure the string can be properly encoded
|
|
185
|
+
encodeURIComponent(trimmed)
|
|
186
|
+
} catch (error) {
|
|
187
|
+
throw new Error('Account identifier contains invalid characters')
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Clear the key derivation cache
|
|
193
|
+
*
|
|
194
|
+
* Useful when switching accounts or when you want to free memory.
|
|
195
|
+
* Note: Keys will be re-derived on next access since derivation is deterministic.
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```typescript
|
|
199
|
+
* // Clear cache when user logs out
|
|
200
|
+
* clearKeyCache()
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
export function clearKeyCache(): void {
|
|
204
|
+
keyCache.clear()
|
|
205
|
+
keyAccessOrder.clear()
|
|
206
|
+
accessCounter = 0
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get MMKV encryption key for an account
|
|
211
|
+
*
|
|
212
|
+
* SECURITY: Since MMKV stores non-sensitive data, we use deterministic key derivation.
|
|
213
|
+
* The key is derived from the account identifier, allowing the same account to access
|
|
214
|
+
* the same encrypted data across devices.
|
|
215
|
+
*
|
|
216
|
+
* The key is NOT stored - it's derived on-demand from the account identifier.
|
|
217
|
+
* Results are cached since derivation is deterministic.
|
|
218
|
+
*
|
|
219
|
+
* This ensures:
|
|
220
|
+
* - Account data isolation (different accounts = different keys)
|
|
221
|
+
* - Cross-device compatibility (same account = same key)
|
|
222
|
+
* - No key storage needed (deterministic derivation)
|
|
223
|
+
*
|
|
224
|
+
* @param accountIdentifier - Account identifier (email or user ID)
|
|
225
|
+
* @returns Promise that resolves to encryption key (base64 string, 32 bytes)
|
|
226
|
+
* @throws Error if account identifier is invalid or key derivation fails
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```typescript
|
|
230
|
+
* const key = await getMMKVKey('user@example.com')
|
|
231
|
+
* const mmkv = createMMKV({ encryptionKey: key })
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
export async function getMMKVKey(accountIdentifier: AccountIdentifier): Promise<string> {
|
|
235
|
+
// Validate input before processing
|
|
236
|
+
validateAccountIdentifier(accountIdentifier)
|
|
237
|
+
|
|
238
|
+
// Use trimmed identifier for consistency
|
|
239
|
+
const trimmedIdentifier = accountIdentifier.trim()
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
// Derive key deterministically from account identifier
|
|
243
|
+
// No need to store it - same account identifier always produces same key
|
|
244
|
+
return await deriveKeyFromAccount(trimmedIdentifier)
|
|
245
|
+
} catch (error) {
|
|
246
|
+
// Provide more context in error messages
|
|
247
|
+
if (error instanceof Error) {
|
|
248
|
+
throw new Error(`Failed to derive encryption key for account: ${error.message}`)
|
|
249
|
+
}
|
|
250
|
+
throw new Error(`Failed to derive encryption key for account: ${String(error)}`)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mnemonic utility functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validate a mnemonic phrase
|
|
7
|
+
*
|
|
8
|
+
* @param mnemonic - The mnemonic phrase to validate
|
|
9
|
+
* @returns true if the mnemonic is valid (12 or 24 words, all non-empty)
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* validateMnemonic("word1 word2 ... word12") // true
|
|
14
|
+
* validateMnemonic("word1 word2 ... word24") // true
|
|
15
|
+
* validateMnemonic("word1 word2") // false (too few words)
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function validateMnemonic(mnemonic: string): boolean {
|
|
19
|
+
const trimmed = mnemonic.trim()
|
|
20
|
+
|
|
21
|
+
// Normalize whitespace first (multiple spaces become single spaces)
|
|
22
|
+
// This handles cases like "word1 word2" where multiple spaces should be normalized
|
|
23
|
+
const normalized = trimmed.replace(/\s+/g, ' ')
|
|
24
|
+
|
|
25
|
+
// Split and validate
|
|
26
|
+
const words = normalized.split(' ')
|
|
27
|
+
const validLengths = [12, 24]
|
|
28
|
+
|
|
29
|
+
// Check word count and that all words are non-empty
|
|
30
|
+
return validLengths.includes(words.length) && words.every(word => word.length > 0)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operation Mutex - Prevents concurrent wallet operations
|
|
3
|
+
*
|
|
4
|
+
* This utility provides a mutex/lock mechanism to ensure only one wallet operation
|
|
5
|
+
* can execute at a time, preventing race conditions and state corruption.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getWalletStore } from '../store/walletStore'
|
|
9
|
+
import { log, logWarn } from './logger'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Operation mutex result
|
|
13
|
+
*/
|
|
14
|
+
export interface MutexResult {
|
|
15
|
+
/** Whether the operation was acquired */
|
|
16
|
+
acquired: boolean
|
|
17
|
+
/** Current operation description if mutex is held */
|
|
18
|
+
currentOperation: string | null
|
|
19
|
+
/** Release function to call when operation completes */
|
|
20
|
+
release: () => void
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Try to acquire operation mutex
|
|
25
|
+
*
|
|
26
|
+
* @param operationDescription - Description of the operation (for debugging)
|
|
27
|
+
* @returns MutexResult with acquired status and release function
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const mutex = acquireOperationMutex('switchWallet')
|
|
32
|
+
* if (!mutex.acquired) {
|
|
33
|
+
* throw new Error(`Operation in progress: ${mutex.currentOperation}`)
|
|
34
|
+
* }
|
|
35
|
+
* try {
|
|
36
|
+
* // Perform operation
|
|
37
|
+
* } finally {
|
|
38
|
+
* mutex.release()
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function acquireOperationMutex(operationDescription: string): MutexResult {
|
|
43
|
+
const walletStore = getWalletStore()
|
|
44
|
+
const state = walletStore.getState()
|
|
45
|
+
|
|
46
|
+
// Check if operation is already in progress
|
|
47
|
+
if (state.isOperationInProgress) {
|
|
48
|
+
logWarn(`[OperationMutex] Operation "${operationDescription}" blocked by: ${state.currentOperation}`)
|
|
49
|
+
return {
|
|
50
|
+
acquired: false,
|
|
51
|
+
currentOperation: state.currentOperation,
|
|
52
|
+
release: () => {}, // No-op release
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Acquire mutex
|
|
57
|
+
walletStore.setState({
|
|
58
|
+
isOperationInProgress: true,
|
|
59
|
+
currentOperation: operationDescription,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
log(`[OperationMutex] Acquired mutex for: ${operationDescription}`)
|
|
63
|
+
|
|
64
|
+
// Return release function
|
|
65
|
+
return {
|
|
66
|
+
acquired: true,
|
|
67
|
+
currentOperation: null,
|
|
68
|
+
release: () => {
|
|
69
|
+
const currentState = walletStore.getState()
|
|
70
|
+
// Only release if we're still the current operation
|
|
71
|
+
if (currentState.currentOperation === operationDescription) {
|
|
72
|
+
walletStore.setState({
|
|
73
|
+
isOperationInProgress: false,
|
|
74
|
+
currentOperation: null,
|
|
75
|
+
})
|
|
76
|
+
log(`[OperationMutex] Released mutex for: ${operationDescription}`)
|
|
77
|
+
} else {
|
|
78
|
+
logWarn(`[OperationMutex] Attempted to release mutex for "${operationDescription}" but current operation is "${currentState.currentOperation}"`)
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Default timeout for operations (30 seconds)
|
|
86
|
+
* Operations that exceed this timeout will be automatically released
|
|
87
|
+
*/
|
|
88
|
+
const DEFAULT_OPERATION_TIMEOUT_MS = 30 * 1000
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Execute an operation with mutex protection
|
|
92
|
+
* Automatically acquires and releases mutex
|
|
93
|
+
* Includes timeout protection to prevent stuck operations
|
|
94
|
+
*
|
|
95
|
+
* @param operationDescription - Description of the operation
|
|
96
|
+
* @param operation - Async operation to execute
|
|
97
|
+
* @param timeoutMs - Optional timeout in milliseconds (default: 30000ms / 30 seconds)
|
|
98
|
+
* @returns Promise with operation result
|
|
99
|
+
* @throws Error if mutex cannot be acquired, operation fails, or timeout is exceeded
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```typescript
|
|
103
|
+
* await withOperationMutex('switchWallet', async () => {
|
|
104
|
+
* await WalletSwitchingService.switchToWallet(walletId)
|
|
105
|
+
* })
|
|
106
|
+
*
|
|
107
|
+
* // With custom timeout
|
|
108
|
+
* await withOperationMutex('longOperation', async () => {
|
|
109
|
+
* await longRunningOperation()
|
|
110
|
+
* }, 60000) // 60 second timeout
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
export async function withOperationMutex<T>(
|
|
114
|
+
operationDescription: string,
|
|
115
|
+
operation: () => Promise<T>,
|
|
116
|
+
timeoutMs: number = DEFAULT_OPERATION_TIMEOUT_MS
|
|
117
|
+
): Promise<T> {
|
|
118
|
+
const mutex = acquireOperationMutex(operationDescription)
|
|
119
|
+
|
|
120
|
+
if (!mutex.acquired) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Cannot execute "${operationDescription}": Another operation is in progress (${mutex.currentOperation})`
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Set up timeout protection
|
|
127
|
+
let timeoutId: NodeJS.Timeout | null = null
|
|
128
|
+
let timeoutExceeded = false
|
|
129
|
+
|
|
130
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
131
|
+
timeoutId = setTimeout(() => {
|
|
132
|
+
timeoutExceeded = true
|
|
133
|
+
logWarn(`[OperationMutex] Operation "${operationDescription}" exceeded timeout of ${timeoutMs}ms`)
|
|
134
|
+
mutex.release()
|
|
135
|
+
reject(new Error(
|
|
136
|
+
`Operation "${operationDescription}" exceeded timeout of ${timeoutMs}ms`
|
|
137
|
+
))
|
|
138
|
+
}, timeoutMs)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
// Race between operation and timeout
|
|
143
|
+
return await Promise.race([
|
|
144
|
+
operation(),
|
|
145
|
+
timeoutPromise,
|
|
146
|
+
])
|
|
147
|
+
} catch (error) {
|
|
148
|
+
// If timeout occurred, error is already handled
|
|
149
|
+
if (timeoutExceeded) {
|
|
150
|
+
throw error
|
|
151
|
+
}
|
|
152
|
+
// Otherwise, re-throw the operation error
|
|
153
|
+
throw error
|
|
154
|
+
} finally {
|
|
155
|
+
// Clear timeout if operation completed before timeout
|
|
156
|
+
if (timeoutId) {
|
|
157
|
+
clearTimeout(timeoutId)
|
|
158
|
+
}
|
|
159
|
+
// Only release if timeout didn't already release it
|
|
160
|
+
if (!timeoutExceeded) {
|
|
161
|
+
mutex.release()
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check if an operation is currently in progress
|
|
168
|
+
*/
|
|
169
|
+
export function isOperationInProgress(): boolean {
|
|
170
|
+
const walletStore = getWalletStore()
|
|
171
|
+
return walletStore.getState().isOperationInProgress
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get current operation description
|
|
176
|
+
*/
|
|
177
|
+
export function getCurrentOperation(): string | null {
|
|
178
|
+
const walletStore = getWalletStore()
|
|
179
|
+
return walletStore.getState().currentOperation
|
|
180
|
+
}
|
|
181
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result Type for Error Handling
|
|
3
|
+
*
|
|
4
|
+
* Provides a consistent pattern for handling operations that can succeed or fail.
|
|
5
|
+
* This eliminates the need for try-catch blocks and provides type-safe error handling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Result type representing success or failure
|
|
10
|
+
*/
|
|
11
|
+
export type Result<T, E = Error> =
|
|
12
|
+
| { success: true; data: T; error?: never }
|
|
13
|
+
| { success: false; error: E; data?: never }
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Create a success result
|
|
17
|
+
*/
|
|
18
|
+
export function ok<T>(data: T): Result<T, never> {
|
|
19
|
+
return { success: true, data }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create an error result
|
|
24
|
+
*/
|
|
25
|
+
export function err<E>(error: E): Result<never, E> {
|
|
26
|
+
return { success: false, error }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Wrap an async function to return a Result instead of throwing
|
|
31
|
+
*/
|
|
32
|
+
export async function toResult<T, E = Error>(
|
|
33
|
+
fn: () => Promise<T>
|
|
34
|
+
): Promise<Result<T, E>> {
|
|
35
|
+
try {
|
|
36
|
+
const data = await fn()
|
|
37
|
+
return ok(data)
|
|
38
|
+
} catch (error) {
|
|
39
|
+
return err(error as E)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Wrap a synchronous function to return a Result instead of throwing
|
|
45
|
+
*/
|
|
46
|
+
export function toResultSync<T, E = Error>(
|
|
47
|
+
fn: () => T
|
|
48
|
+
): Result<T, E> {
|
|
49
|
+
try {
|
|
50
|
+
const data = fn()
|
|
51
|
+
return ok(data)
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return err(error as E)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|