@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.
Files changed (275) hide show
  1. package/README.md +551 -0
  2. package/dist/__mocks__/react.d.ts +40 -0
  3. package/dist/__mocks__/react.js +33 -0
  4. package/dist/__mocks__/react.js.map +1 -0
  5. package/dist/__mocks__/secureStorage.d.ts +26 -0
  6. package/dist/__mocks__/secureStorage.js +80 -0
  7. package/dist/__mocks__/secureStorage.js.map +1 -0
  8. package/dist/__tests__/hooks/useBalance.test.d.ts +6 -0
  9. package/dist/__tests__/hooks/useBalance.test.js +217 -0
  10. package/dist/__tests__/hooks/useBalance.test.js.map +1 -0
  11. package/dist/__tests__/hooks/useWallet.test.d.ts +6 -0
  12. package/dist/__tests__/hooks/useWallet.test.js +105 -0
  13. package/dist/__tests__/hooks/useWallet.test.js.map +1 -0
  14. package/dist/__tests__/hooks/useWalletManager.test.d.ts +6 -0
  15. package/dist/__tests__/hooks/useWalletManager.test.js +193 -0
  16. package/dist/__tests__/hooks/useWalletManager.test.js.map +1 -0
  17. package/dist/__tests__/hooks/useWdkApp.test.d.ts +6 -0
  18. package/dist/__tests__/hooks/useWdkApp.test.js +55 -0
  19. package/dist/__tests__/hooks/useWdkApp.test.js.map +1 -0
  20. package/dist/__tests__/hooks/useWorklet.test.d.ts +6 -0
  21. package/dist/__tests__/hooks/useWorklet.test.js +168 -0
  22. package/dist/__tests__/hooks/useWorklet.test.js.map +1 -0
  23. package/dist/__tests__/provider/WdkAppProvider.test.d.ts +6 -0
  24. package/dist/__tests__/provider/WdkAppProvider.test.js +57 -0
  25. package/dist/__tests__/provider/WdkAppProvider.test.js.map +1 -0
  26. package/dist/__tests__/services/accountService.test.d.ts +6 -0
  27. package/dist/__tests__/services/accountService.test.js +216 -0
  28. package/dist/__tests__/services/accountService.test.js.map +1 -0
  29. package/dist/__tests__/services/addressService.test.d.ts +6 -0
  30. package/dist/__tests__/services/addressService.test.js +246 -0
  31. package/dist/__tests__/services/addressService.test.js.map +1 -0
  32. package/dist/__tests__/services/balanceService.test.d.ts +6 -0
  33. package/dist/__tests__/services/balanceService.test.js +333 -0
  34. package/dist/__tests__/services/balanceService.test.js.map +1 -0
  35. package/dist/__tests__/services/walletSetupService.test.d.ts +6 -0
  36. package/dist/__tests__/services/walletSetupService.test.js +304 -0
  37. package/dist/__tests__/services/walletSetupService.test.js.map +1 -0
  38. package/dist/__tests__/services/walletSwitchingService.test.d.ts +6 -0
  39. package/dist/__tests__/services/walletSwitchingService.test.js +209 -0
  40. package/dist/__tests__/services/walletSwitchingService.test.js.map +1 -0
  41. package/dist/__tests__/services/workletLifecycleService.test.d.ts +6 -0
  42. package/dist/__tests__/services/workletLifecycleService.test.js +451 -0
  43. package/dist/__tests__/services/workletLifecycleService.test.js.map +1 -0
  44. package/dist/__tests__/setup.d.ts +7 -0
  45. package/dist/__tests__/setup.js +49 -0
  46. package/dist/__tests__/setup.js.map +1 -0
  47. package/dist/__tests__/storage/mmkvStorage.test.d.ts +6 -0
  48. package/dist/__tests__/storage/mmkvStorage.test.js +213 -0
  49. package/dist/__tests__/storage/mmkvStorage.test.js.map +1 -0
  50. package/dist/__tests__/store/walletStore.test.d.ts +4 -0
  51. package/dist/__tests__/store/walletStore.test.js +86 -0
  52. package/dist/__tests__/store/walletStore.test.js.map +1 -0
  53. package/dist/__tests__/store/workletStore.test.d.ts +4 -0
  54. package/dist/__tests__/store/workletStore.test.js +255 -0
  55. package/dist/__tests__/store/workletStore.test.js.map +1 -0
  56. package/dist/__tests__/types/hrpc.test.d.ts +4 -0
  57. package/dist/__tests__/types/hrpc.test.js +99 -0
  58. package/dist/__tests__/types/hrpc.test.js.map +1 -0
  59. package/dist/__tests__/utils/balanceUtils.test.d.ts +4 -0
  60. package/dist/__tests__/utils/balanceUtils.test.js +133 -0
  61. package/dist/__tests__/utils/balanceUtils.test.js.map +1 -0
  62. package/dist/__tests__/utils/errorHandling.test.d.ts +6 -0
  63. package/dist/__tests__/utils/errorHandling.test.js +119 -0
  64. package/dist/__tests__/utils/errorHandling.test.js.map +1 -0
  65. package/dist/__tests__/utils/errorUtils.test.d.ts +4 -0
  66. package/dist/__tests__/utils/errorUtils.test.js +133 -0
  67. package/dist/__tests__/utils/errorUtils.test.js.map +1 -0
  68. package/dist/__tests__/utils/initializationState.test.d.ts +6 -0
  69. package/dist/__tests__/utils/initializationState.test.js +243 -0
  70. package/dist/__tests__/utils/initializationState.test.js.map +1 -0
  71. package/dist/__tests__/utils/jsonUtils.test.d.ts +4 -0
  72. package/dist/__tests__/utils/jsonUtils.test.js +118 -0
  73. package/dist/__tests__/utils/jsonUtils.test.js.map +1 -0
  74. package/dist/__tests__/utils/mmkvKeyManager.test.d.ts +6 -0
  75. package/dist/__tests__/utils/mmkvKeyManager.test.js +158 -0
  76. package/dist/__tests__/utils/mmkvKeyManager.test.js.map +1 -0
  77. package/dist/__tests__/utils/mnemonicUtils.test.d.ts +4 -0
  78. package/dist/__tests__/utils/mnemonicUtils.test.js +40 -0
  79. package/dist/__tests__/utils/mnemonicUtils.test.js.map +1 -0
  80. package/dist/__tests__/utils/raceConditions.test.d.ts +6 -0
  81. package/dist/__tests__/utils/raceConditions.test.js +239 -0
  82. package/dist/__tests__/utils/raceConditions.test.js.map +1 -0
  83. package/dist/__tests__/utils/result.test.d.ts +4 -0
  84. package/dist/__tests__/utils/result.test.js +66 -0
  85. package/dist/__tests__/utils/result.test.js.map +1 -0
  86. package/dist/__tests__/utils/schemas.test.d.ts +4 -0
  87. package/dist/__tests__/utils/schemas.test.js +66 -0
  88. package/dist/__tests__/utils/schemas.test.js.map +1 -0
  89. package/dist/__tests__/utils/storeHelpers.test.d.ts +4 -0
  90. package/dist/__tests__/utils/storeHelpers.test.js +227 -0
  91. package/dist/__tests__/utils/storeHelpers.test.js.map +1 -0
  92. package/dist/__tests__/utils/typeGuards.test.d.ts +4 -0
  93. package/dist/__tests__/utils/typeGuards.test.js +175 -0
  94. package/dist/__tests__/utils/typeGuards.test.js.map +1 -0
  95. package/dist/__tests__/utils/validation.test.d.ts +4 -0
  96. package/dist/__tests__/utils/validation.test.js +126 -0
  97. package/dist/__tests__/utils/validation.test.js.map +1 -0
  98. package/dist/__tests__/utils/walletUtils.test.d.ts +4 -0
  99. package/dist/__tests__/utils/walletUtils.test.js +156 -0
  100. package/dist/__tests__/utils/walletUtils.test.js.map +1 -0
  101. package/dist/hooks/useBalance.d.ts +170 -0
  102. package/dist/hooks/useBalance.js +442 -0
  103. package/dist/hooks/useBalance.js.map +1 -0
  104. package/dist/hooks/useWallet.d.ts +43 -0
  105. package/dist/hooks/useWallet.js +193 -0
  106. package/dist/hooks/useWallet.js.map +1 -0
  107. package/dist/hooks/useWalletManager.d.ts +100 -0
  108. package/dist/hooks/useWalletManager.js +526 -0
  109. package/dist/hooks/useWalletManager.js.map +1 -0
  110. package/dist/hooks/useWdkApp.d.ts +66 -0
  111. package/dist/hooks/useWdkApp.js +74 -0
  112. package/dist/hooks/useWdkApp.js.map +1 -0
  113. package/dist/hooks/useWorklet.d.ts +66 -0
  114. package/dist/hooks/useWorklet.js +42 -0
  115. package/dist/hooks/useWorklet.js.map +1 -0
  116. package/dist/index.d.ts +33 -0
  117. package/dist/index.js +36 -0
  118. package/dist/index.js.map +1 -0
  119. package/dist/provider/WdkAppProvider.d.ts +112 -0
  120. package/dist/provider/WdkAppProvider.js +558 -0
  121. package/dist/provider/WdkAppProvider.js.map +1 -0
  122. package/dist/services/accountService.d.ts +52 -0
  123. package/dist/services/accountService.js +119 -0
  124. package/dist/services/accountService.js.map +1 -0
  125. package/dist/services/addressService.d.ts +22 -0
  126. package/dist/services/addressService.js +110 -0
  127. package/dist/services/addressService.js.map +1 -0
  128. package/dist/services/balanceService.d.ts +114 -0
  129. package/dist/services/balanceService.js +208 -0
  130. package/dist/services/balanceService.js.map +1 -0
  131. package/dist/services/walletSetupService.d.ts +217 -0
  132. package/dist/services/walletSetupService.js +535 -0
  133. package/dist/services/walletSetupService.js.map +1 -0
  134. package/dist/services/walletSwitchingService.d.ts +48 -0
  135. package/dist/services/walletSwitchingService.js +143 -0
  136. package/dist/services/walletSwitchingService.js.map +1 -0
  137. package/dist/services/workletLifecycleService.d.ts +117 -0
  138. package/dist/services/workletLifecycleService.js +433 -0
  139. package/dist/services/workletLifecycleService.js.map +1 -0
  140. package/dist/storage/mmkvStorage.d.ts +44 -0
  141. package/dist/storage/mmkvStorage.js +348 -0
  142. package/dist/storage/mmkvStorage.js.map +1 -0
  143. package/dist/store/walletStore.d.ts +130 -0
  144. package/dist/store/walletStore.js +204 -0
  145. package/dist/store/walletStore.js.map +1 -0
  146. package/dist/store/workletStore.d.ts +121 -0
  147. package/dist/store/workletStore.js +256 -0
  148. package/dist/store/workletStore.js.map +1 -0
  149. package/dist/types/hrpc.d.ts +60 -0
  150. package/dist/types/hrpc.js +26 -0
  151. package/dist/types/hrpc.js.map +1 -0
  152. package/dist/types.d.ts +168 -0
  153. package/dist/types.js +7 -0
  154. package/dist/types.js.map +1 -0
  155. package/dist/utils/balanceUtils.d.ts +30 -0
  156. package/dist/utils/balanceUtils.js +86 -0
  157. package/dist/utils/balanceUtils.js.map +1 -0
  158. package/dist/utils/constants.d.ts +66 -0
  159. package/dist/utils/constants.js +73 -0
  160. package/dist/utils/constants.js.map +1 -0
  161. package/dist/utils/errorHandling.d.ts +47 -0
  162. package/dist/utils/errorHandling.js +54 -0
  163. package/dist/utils/errorHandling.js.map +1 -0
  164. package/dist/utils/errorUtils.d.ts +89 -0
  165. package/dist/utils/errorUtils.js +264 -0
  166. package/dist/utils/errorUtils.js.map +1 -0
  167. package/dist/utils/initializationState.d.ts +129 -0
  168. package/dist/utils/initializationState.js +220 -0
  169. package/dist/utils/initializationState.js.map +1 -0
  170. package/dist/utils/jsonUtils.d.ts +21 -0
  171. package/dist/utils/jsonUtils.js +67 -0
  172. package/dist/utils/jsonUtils.js.map +1 -0
  173. package/dist/utils/logger.d.ts +27 -0
  174. package/dist/utils/logger.js +50 -0
  175. package/dist/utils/logger.js.map +1 -0
  176. package/dist/utils/mmkvKeyManager.d.ts +57 -0
  177. package/dist/utils/mmkvKeyManager.js +214 -0
  178. package/dist/utils/mmkvKeyManager.js.map +1 -0
  179. package/dist/utils/mnemonicUtils.d.ts +17 -0
  180. package/dist/utils/mnemonicUtils.js +28 -0
  181. package/dist/utils/mnemonicUtils.js.map +1 -0
  182. package/dist/utils/operationMutex.d.ts +69 -0
  183. package/dist/utils/operationMutex.js +150 -0
  184. package/dist/utils/operationMutex.js.map +1 -0
  185. package/dist/utils/result.d.ts +34 -0
  186. package/dist/utils/result.js +43 -0
  187. package/dist/utils/result.js.map +1 -0
  188. package/dist/utils/schemas.d.ts +365 -0
  189. package/dist/utils/schemas.js +152 -0
  190. package/dist/utils/schemas.js.map +1 -0
  191. package/dist/utils/storeHelpers.d.ts +112 -0
  192. package/dist/utils/storeHelpers.js +198 -0
  193. package/dist/utils/storeHelpers.js.map +1 -0
  194. package/dist/utils/typeGuards.d.ts +70 -0
  195. package/dist/utils/typeGuards.js +95 -0
  196. package/dist/utils/typeGuards.js.map +1 -0
  197. package/dist/utils/validation.d.ts +53 -0
  198. package/dist/utils/validation.js +152 -0
  199. package/dist/utils/validation.js.map +1 -0
  200. package/dist/utils/walletStateHelpers.d.ts +62 -0
  201. package/dist/utils/walletStateHelpers.js +81 -0
  202. package/dist/utils/walletStateHelpers.js.map +1 -0
  203. package/dist/utils/walletUtils.d.ts +38 -0
  204. package/dist/utils/walletUtils.js +87 -0
  205. package/dist/utils/walletUtils.js.map +1 -0
  206. package/package.json +79 -0
  207. package/src/__mocks__/react.ts +34 -0
  208. package/src/__mocks__/secureStorage.ts +87 -0
  209. package/src/__tests__/hooks/useBalance.test.ts +286 -0
  210. package/src/__tests__/hooks/useWallet.test.ts +122 -0
  211. package/src/__tests__/hooks/useWalletManager.test.ts +271 -0
  212. package/src/__tests__/hooks/useWdkApp.test.tsx +59 -0
  213. package/src/__tests__/hooks/useWorklet.test.ts +195 -0
  214. package/src/__tests__/provider/WdkAppProvider.test.tsx +67 -0
  215. package/src/__tests__/services/accountService.test.ts +337 -0
  216. package/src/__tests__/services/addressService.test.ts +312 -0
  217. package/src/__tests__/services/balanceService.test.ts +411 -0
  218. package/src/__tests__/services/walletSetupService.test.ts +463 -0
  219. package/src/__tests__/services/walletSwitchingService.test.ts +258 -0
  220. package/src/__tests__/services/workletLifecycleService.test.ts +514 -0
  221. package/src/__tests__/setup.ts +54 -0
  222. package/src/__tests__/storage/mmkvStorage.test.ts +325 -0
  223. package/src/__tests__/store/walletStore.test.ts +101 -0
  224. package/src/__tests__/store/workletStore.test.ts +329 -0
  225. package/src/__tests__/types/hrpc.test.ts +125 -0
  226. package/src/__tests__/utils/balanceUtils.test.ts +151 -0
  227. package/src/__tests__/utils/errorHandling.test.ts +147 -0
  228. package/src/__tests__/utils/errorUtils.test.ts +170 -0
  229. package/src/__tests__/utils/initializationState.test.ts +337 -0
  230. package/src/__tests__/utils/jsonUtils.test.ts +131 -0
  231. package/src/__tests__/utils/mmkvKeyManager.test.ts +205 -0
  232. package/src/__tests__/utils/mnemonicUtils.test.ts +48 -0
  233. package/src/__tests__/utils/raceConditions.test.ts +292 -0
  234. package/src/__tests__/utils/result.test.ts +73 -0
  235. package/src/__tests__/utils/schemas.test.ts +82 -0
  236. package/src/__tests__/utils/storeHelpers.test.ts +323 -0
  237. package/src/__tests__/utils/typeGuards.test.ts +209 -0
  238. package/src/__tests__/utils/validation.test.ts +152 -0
  239. package/src/__tests__/utils/walletUtils.test.ts +195 -0
  240. package/src/hooks/useBalance.ts +597 -0
  241. package/src/hooks/useWallet.ts +288 -0
  242. package/src/hooks/useWalletManager.ts +654 -0
  243. package/src/hooks/useWdkApp.ts +78 -0
  244. package/src/hooks/useWorklet.ts +109 -0
  245. package/src/index.ts +112 -0
  246. package/src/provider/WdkAppProvider.tsx +764 -0
  247. package/src/services/accountService.ts +134 -0
  248. package/src/services/addressService.ts +125 -0
  249. package/src/services/balanceService.ts +291 -0
  250. package/src/services/walletSetupService.ts +695 -0
  251. package/src/services/walletSwitchingService.ts +157 -0
  252. package/src/services/workletLifecycleService.ts +561 -0
  253. package/src/storage/mmkvStorage.ts +417 -0
  254. package/src/store/walletStore.ts +301 -0
  255. package/src/store/workletStore.ts +340 -0
  256. package/src/types/hrpc.ts +78 -0
  257. package/src/types.ts +188 -0
  258. package/src/utils/balanceUtils.ts +96 -0
  259. package/src/utils/constants.ts +84 -0
  260. package/src/utils/errorHandling.md +163 -0
  261. package/src/utils/errorHandling.ts +61 -0
  262. package/src/utils/errorUtils.ts +300 -0
  263. package/src/utils/initializationState.ts +242 -0
  264. package/src/utils/jsonUtils.ts +75 -0
  265. package/src/utils/logger.ts +54 -0
  266. package/src/utils/mmkvKeyManager.ts +256 -0
  267. package/src/utils/mnemonicUtils.ts +35 -0
  268. package/src/utils/operationMutex.ts +181 -0
  269. package/src/utils/result.ts +57 -0
  270. package/src/utils/schemas.ts +190 -0
  271. package/src/utils/storeHelpers.ts +236 -0
  272. package/src/utils/typeGuards.ts +129 -0
  273. package/src/utils/validation.ts +174 -0
  274. package/src/utils/walletStateHelpers.ts +104 -0
  275. 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
+