@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,597 @@
1
+ /**
2
+ * Balance Hooks with TanStack Query
3
+ *
4
+ * Provides React hooks for fetching and managing wallet balances using TanStack Query.
5
+ *
6
+ * ## Single Source of Truth Architecture
7
+ *
8
+ * This module uses Zustand as the single source of truth for balances, with TanStack Query
9
+ * as the fetching and caching layer.
10
+ *
11
+ * ### Zustand Store (Single Source of Truth)
12
+ * - **Purpose**: Store and persist balances across app restarts
13
+ * - **Lifetime**: Persisted to MMKV storage (survives app restarts)
14
+ * - **Features**: Single source of truth, persistence, immediate availability on app restart
15
+ * - **Usage**: All balance reads should go through TanStack Query hooks, which read from Zustand
16
+ *
17
+ * ### TanStack Query (Fetching & Caching Layer)
18
+ * - **Purpose**: Balance fetching, caching, refetching, and stale time management
19
+ * - **Lifetime**: Runtime-only (cache cleared on app restart, but reads initial data from Zustand)
20
+ * - **Features**: Automatic refetching, stale time management, cache invalidation, optimistic updates
21
+ * - **Usage**: Always use TanStack Query hooks (`useBalance`, `useBalances`) for balance operations
22
+ *
23
+ * ### Data Flow:
24
+ * 1. On app start: TanStack Query reads initial data from Zustand (via `initialData`)
25
+ * 2. If data is stale or missing: TanStack Query fetches balance from worklet (via `fetchBalance()`)
26
+ * 3. After fetch: Balance is updated in Zustand store (single source of truth update)
27
+ * 4. TanStack Query cache is updated with the fetched data
28
+ * 5. Components re-render with fresh data from TanStack Query
29
+ *
30
+ * ### Sync Guarantees:
31
+ * - **Zustand is always the source of truth** - TanStack Query reads from and writes to Zustand
32
+ * - **No sync logic needed** - TanStack Query updates Zustand directly after fetch
33
+ * - **Initial data consistency** - TanStack Query uses Zustand's persisted data as `initialData`
34
+ * - **No race conditions** - All updates go through Zustand, ensuring consistency
35
+ *
36
+ * ### Important Notes:
37
+ * - **Always use TanStack Query hooks** for balance operations - they handle Zustand integration
38
+ * - **Zustand is the single source of truth** - TanStack Query is a fetching/caching layer on top
39
+ * - **Addresses remain in Zustand only** (derived/computed state, deterministic, no refetching needed)
40
+ * - **Updates happen automatically** in `fetchBalance()` - Zustand is updated after each successful fetch
41
+ */
42
+
43
+ import { useQuery, useQueries, useMutation, useQueryClient } from '@tanstack/react-query'
44
+
45
+ import { AccountService } from '../services/accountService'
46
+ import { BalanceService } from '../services/balanceService'
47
+ import { getWalletStore } from '../store/walletStore'
48
+ import { getWorkletStore } from '../store/workletStore'
49
+ import { resolveWalletId } from '../utils/storeHelpers'
50
+ import { convertBalanceToString } from '../utils/balanceUtils'
51
+ import {
52
+ ACCOUNT_METHOD_GET_BALANCE,
53
+ ACCOUNT_METHOD_GET_TOKEN_BALANCE,
54
+ NATIVE_TOKEN_KEY,
55
+ DEFAULT_QUERY_STALE_TIME_MS,
56
+ DEFAULT_QUERY_GC_TIME_MS,
57
+ } from '../utils/constants'
58
+ import { logError } from '../utils/logger'
59
+ import { validateWalletParams } from '../utils/validation'
60
+ import type { BalanceFetchResult, TokenConfigProvider } from '../types'
61
+
62
+ /**
63
+ * Balance query options
64
+ */
65
+ export interface BalanceQueryOptions {
66
+ /** Whether the query is enabled */
67
+ enabled?: boolean
68
+ /** Refetch interval in milliseconds (false to disable) */
69
+ refetchInterval?: number | false
70
+ /** Stale time in milliseconds */
71
+ staleTime?: number
72
+ /** Wallet identifier (defaults to activeWalletId) */
73
+ walletId?: string
74
+ }
75
+
76
+ /**
77
+ * Parameters for balance operations
78
+ */
79
+ export interface BalanceParams {
80
+ /** Network name */
81
+ network: string
82
+ /** Account index */
83
+ accountIndex: number
84
+ /** Token address (null for native token) */
85
+ tokenAddress: string | null
86
+ /** Optional wallet identifier */
87
+ walletId?: string
88
+ }
89
+
90
+ /**
91
+ * Parameters for refreshing balances
92
+ */
93
+ export interface RefreshBalanceParams {
94
+ /** Network name (required for 'token' or 'network' type) */
95
+ network?: string
96
+ /** Account index */
97
+ accountIndex: number
98
+ /** Token address (required for 'token' type) */
99
+ tokenAddress?: string | null
100
+ /** Refresh type: 'token' (single), 'wallet' (all for wallet), 'network' (all for network), 'all' (everything) */
101
+ type?: 'token' | 'wallet' | 'network' | 'all'
102
+ /** Wallet identifier (defaults to activeWalletId) */
103
+ walletId?: string
104
+ }
105
+
106
+ /**
107
+ * Query key factory for balance queries
108
+ */
109
+ export const balanceQueryKeys = {
110
+ all: ['balances'] as const,
111
+ byWallet: (walletId: string, accountIndex: number) => ['balances', 'wallet', walletId, accountIndex] as const,
112
+ byNetwork: (network: string) => ['balances', 'network', network] as const,
113
+ byWalletAndNetwork: (walletId: string, accountIndex: number, network: string) =>
114
+ ['balances', 'wallet', walletId, accountIndex, 'network', network] as const,
115
+ byToken: (walletId: string, accountIndex: number, network: string, tokenAddress: string | null) =>
116
+ ['balances', 'wallet', walletId, accountIndex, 'network', network, 'token', tokenAddress || NATIVE_TOKEN_KEY] as const,
117
+ }
118
+
119
+ /**
120
+ * Validated balance query key structure
121
+ */
122
+ interface ValidatedBalanceQueryKey {
123
+ accountIndex: number
124
+ network: string
125
+ tokenAddress: string | null
126
+ }
127
+
128
+ /**
129
+ * Check if a query should be enabled
130
+ */
131
+ function isQueryEnabled(
132
+ enabledOption: boolean | undefined,
133
+ isInitialized: boolean,
134
+ additionalCondition: boolean = true
135
+ ): boolean {
136
+ return (enabledOption !== false) && isInitialized && additionalCondition
137
+ }
138
+
139
+ /**
140
+ * Validate and parse a balance query key structure
141
+ *
142
+ * @param queryKey - Query key array from balanceQueryKeys.byToken()
143
+ * @returns Validated query key components
144
+ * @throws Error if query key structure is invalid
145
+ */
146
+ function validateQueryKeyStructure(queryKey: unknown): ValidatedBalanceQueryKey {
147
+ // Validate queryKey structure before destructuring
148
+ if (!Array.isArray(queryKey) || queryKey.length < 8) {
149
+ throw new Error(`Invalid queryKey structure: ${JSON.stringify(queryKey)}`)
150
+ }
151
+
152
+ const [, , , accountIdx, , network, , tokenAddress] = queryKey
153
+
154
+ // Validate types instead of using assertions
155
+ if (typeof accountIdx !== 'number' || accountIdx < 0) {
156
+ throw new Error(`Invalid accountIndex in queryKey: ${accountIdx}`)
157
+ }
158
+ if (typeof network !== 'string' || network.length === 0) {
159
+ throw new Error(`Invalid network in queryKey: ${network}`)
160
+ }
161
+ if (tokenAddress !== NATIVE_TOKEN_KEY && (typeof tokenAddress !== 'string' || tokenAddress.length === 0)) {
162
+ throw new Error(`Invalid tokenAddress in queryKey: ${tokenAddress}`)
163
+ }
164
+
165
+ return {
166
+ accountIndex: accountIdx,
167
+ network,
168
+ tokenAddress: tokenAddress === NATIVE_TOKEN_KEY ? null : tokenAddress,
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Fetch balance for a specific token (native or ERC20)
174
+ *
175
+ * @param network - Network name
176
+ * @param accountIndex - Account index
177
+ * @param tokenAddress - Token address (null for native token)
178
+ * @param walletId - Optional wallet identifier (defaults to activeWalletId)
179
+ * @returns Promise with balance fetch result
180
+ */
181
+ async function fetchBalance(
182
+ network: string,
183
+ accountIndex: number,
184
+ tokenAddress: string | null,
185
+ walletId?: string
186
+ ): Promise<BalanceFetchResult> {
187
+ validateWalletParams(network, accountIndex, tokenAddress)
188
+
189
+ const workletStore = getWorkletStore()
190
+ if (!workletStore.getState().isInitialized) {
191
+ return {
192
+ success: false,
193
+ network,
194
+ accountIndex,
195
+ tokenAddress,
196
+ balance: null,
197
+ error: 'Wallet not initialized',
198
+ }
199
+ }
200
+
201
+ try {
202
+ const isNative = tokenAddress === null
203
+ const methodName = isNative ? ACCOUNT_METHOD_GET_BALANCE : ACCOUNT_METHOD_GET_TOKEN_BALANCE
204
+ const methodArg = isNative ? null : tokenAddress
205
+
206
+ const balanceResult = await AccountService.callAccountMethod<string>(
207
+ network,
208
+ accountIndex,
209
+ methodName,
210
+ methodArg
211
+ )
212
+
213
+ // Convert to string (handles BigInt values)
214
+ const balance = convertBalanceToString(balanceResult)
215
+
216
+ // Update Zustand store (single source of truth)
217
+ // IMPORTANT: Zustand is the single source of truth for balances.
218
+ // TanStack Query is a fetching/caching layer that reads from and updates Zustand.
219
+ // This update ensures balances are persisted and available immediately on app restart.
220
+ const targetWalletId = resolveWalletId(walletId)
221
+ BalanceService.updateBalance(accountIndex, network, tokenAddress, balance, targetWalletId)
222
+ BalanceService.updateLastBalanceUpdate(network, accountIndex, targetWalletId)
223
+
224
+ return {
225
+ success: true,
226
+ network,
227
+ accountIndex,
228
+ tokenAddress,
229
+ balance,
230
+ }
231
+ } catch (error) {
232
+ const errorMessage = error instanceof Error ? error.message : String(error)
233
+ const tokenInfo = tokenAddress ? `:${tokenAddress}` : ''
234
+ const balanceType = tokenAddress ? 'token' : 'native'
235
+ logError(
236
+ `Failed to fetch ${balanceType} balance for ${network}:${accountIndex}${tokenInfo}:`,
237
+ error
238
+ )
239
+
240
+ return {
241
+ success: false,
242
+ network,
243
+ accountIndex,
244
+ tokenAddress,
245
+ balance: null,
246
+ error: errorMessage,
247
+ }
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Hook to fetch a single balance
253
+ *
254
+ * @param network - Network name
255
+ * @param accountIndex - Account index
256
+ * @param tokenAddress - Token address (null for native token)
257
+ * @param options - Query options (enabled, refetchInterval, identifier, etc.)
258
+ * @returns TanStack Query result with balance data
259
+ *
260
+ * @example
261
+ * ```tsx
262
+ * const { data: balance, isLoading, error } = useBalance('ethereum', 0, null)
263
+ *
264
+ * // With specific wallet identifier
265
+ * const { data: balance } = useBalance('ethereum', 0, null, { walletId: 'user@example.com' })
266
+ *
267
+ * if (isLoading) return <Loading />
268
+ * if (error) return <Error message={error.message} />
269
+ * if (balance?.success) {
270
+ * return <Text>Balance: {balance.balance}</Text>
271
+ * }
272
+ * ```
273
+ */
274
+ export function useBalance(
275
+ network: string,
276
+ accountIndex: number,
277
+ tokenAddress: string | null,
278
+ options?: BalanceQueryOptions
279
+ ) {
280
+ const workletStore = getWorkletStore()
281
+ const walletStore = getWalletStore()
282
+
283
+ // Check if wallet is initialized
284
+ const isInitialized = workletStore.getState().isInitialized
285
+
286
+ // Get walletId from options or activeWalletId
287
+ const activeWalletId = walletStore.getState().activeWalletId
288
+ const walletId = options?.walletId || activeWalletId || '__temporary__'
289
+
290
+ // Get initial data from Zustand (single source of truth)
291
+ // This ensures balances are available immediately on app restart before refetch
292
+ const initialBalance = BalanceService.getBalance(accountIndex, network, tokenAddress, walletId)
293
+ const initialData: BalanceFetchResult | undefined = initialBalance !== null
294
+ ? {
295
+ success: true,
296
+ network,
297
+ accountIndex,
298
+ tokenAddress,
299
+ balance: initialBalance,
300
+ }
301
+ : undefined
302
+
303
+ return useQuery({
304
+ queryKey: balanceQueryKeys.byToken(walletId, accountIndex, network, tokenAddress),
305
+ queryFn: () => fetchBalance(network, accountIndex, tokenAddress, walletId),
306
+ enabled: isQueryEnabled(options?.enabled, isInitialized),
307
+ refetchInterval: options?.refetchInterval,
308
+ staleTime: options?.staleTime ?? DEFAULT_QUERY_STALE_TIME_MS,
309
+ gcTime: DEFAULT_QUERY_GC_TIME_MS,
310
+ // Use Zustand as initial data source (single source of truth)
311
+ initialData,
312
+ })
313
+ }
314
+
315
+ /**
316
+ * Build query keys for all tokens across all networks
317
+ */
318
+ function buildBalanceQueryKeys(
319
+ walletId: string,
320
+ accountIndex: number,
321
+ tokenConfigs: TokenConfigProvider
322
+ ): ReturnType<typeof balanceQueryKeys.byToken>[] {
323
+ const tokenConfigsObj = typeof tokenConfigs === 'function' ? tokenConfigs() : tokenConfigs
324
+ const networks = Object.keys(tokenConfigsObj)
325
+
326
+ return networks.flatMap((network) => {
327
+ const networkTokens = tokenConfigsObj[network]
328
+ if (!networkTokens) return []
329
+
330
+ const tokens = [networkTokens.native, ...networkTokens.tokens]
331
+ return tokens.map((token) =>
332
+ balanceQueryKeys.byToken(walletId, accountIndex, network, token.address)
333
+ )
334
+ })
335
+ }
336
+
337
+ /**
338
+ * Fetch balances for all query keys
339
+ */
340
+ async function fetchBalancesForQueryKeys(
341
+ queryKeys: ReturnType<typeof balanceQueryKeys.byToken>[],
342
+ walletId: string
343
+ ): Promise<BalanceFetchResult[]> {
344
+ return Promise.all(
345
+ queryKeys.map(async (queryKey) => {
346
+ const validated = validateQueryKeyStructure(queryKey)
347
+ return fetchBalance(
348
+ validated.network,
349
+ validated.accountIndex,
350
+ validated.tokenAddress,
351
+ walletId
352
+ )
353
+ })
354
+ )
355
+ }
356
+
357
+ /**
358
+ * Hook to fetch all balances for a wallet across all networks
359
+ *
360
+ * @param accountIndex - Account index
361
+ * @param tokenConfigs - Token configurations
362
+ * @param options - Query options (including identifier)
363
+ * @returns TanStack Query result with all balances
364
+ */
365
+ export function useBalancesForWallet(
366
+ accountIndex: number,
367
+ tokenConfigs: TokenConfigProvider,
368
+ options?: BalanceQueryOptions
369
+ ) {
370
+ const workletStore = getWorkletStore()
371
+ const isInitialized = workletStore.getState().isInitialized
372
+
373
+ // Resolve walletId from options or store
374
+ const walletId = resolveWalletId(options?.walletId)
375
+
376
+ // Create query keys for all tokens (with walletId)
377
+ const queryKeys = buildBalanceQueryKeys(walletId, accountIndex, tokenConfigs)
378
+
379
+ // Get initial data from Zustand (single source of truth)
380
+ // Build initial data array from persisted balances
381
+ const initialData: BalanceFetchResult[] | undefined = (() => {
382
+ const tokenConfigsObj = typeof tokenConfigs === 'function' ? tokenConfigs() : tokenConfigs
383
+ const networks = Object.keys(tokenConfigsObj)
384
+
385
+ const initialBalances: BalanceFetchResult[] = []
386
+ let hasAnyInitialData = false
387
+
388
+ for (const network of networks) {
389
+ const networkTokens = tokenConfigsObj[network]
390
+ if (!networkTokens) continue
391
+
392
+ const tokens = [networkTokens.native, ...networkTokens.tokens]
393
+ for (const token of tokens) {
394
+ const balance = BalanceService.getBalance(accountIndex, network, token.address, walletId)
395
+ if (balance !== null) {
396
+ hasAnyInitialData = true
397
+ initialBalances.push({
398
+ success: true,
399
+ network,
400
+ accountIndex,
401
+ tokenAddress: token.address,
402
+ balance,
403
+ })
404
+ } else {
405
+ // Include placeholder for missing balances to maintain structure
406
+ initialBalances.push({
407
+ success: false,
408
+ network,
409
+ accountIndex,
410
+ tokenAddress: token.address,
411
+ balance: null,
412
+ error: 'Balance not available',
413
+ })
414
+ }
415
+ }
416
+ }
417
+
418
+ // Only return initial data if we have at least one persisted balance
419
+ return hasAnyInitialData ? initialBalances : undefined
420
+ })()
421
+
422
+ return useQuery({
423
+ queryKey: [...balanceQueryKeys.byWallet(walletId, accountIndex), 'all'],
424
+ queryFn: () => fetchBalancesForQueryKeys(queryKeys, walletId),
425
+ enabled: isQueryEnabled(options?.enabled, isInitialized, queryKeys.length > 0),
426
+ refetchInterval: options?.refetchInterval,
427
+ staleTime: options?.staleTime ?? DEFAULT_QUERY_STALE_TIME_MS,
428
+ gcTime: DEFAULT_QUERY_GC_TIME_MS,
429
+ // Use Zustand as initial data source (single source of truth)
430
+ initialData,
431
+ })
432
+ }
433
+
434
+ /**
435
+ * Hook to fetch balances for multiple wallets
436
+ *
437
+ * This hook is designed to handle dynamic arrays of wallets without violating React's Rules of Hooks.
438
+ * It uses TanStack Query's useQueries which is specifically designed for this use case.
439
+ *
440
+ * @param wallets - Array of wallets with accountIndex and identifier
441
+ * @param tokenConfigs - Token configurations
442
+ * @param options - Query options (enabled, refetchInterval, etc.)
443
+ * @returns Array of TanStack Query results, one for each wallet
444
+ *
445
+ * @example
446
+ * ```tsx
447
+ * const wallets = [
448
+ * { accountIndex: 0, identifier: 'user@example.com' },
449
+ * { accountIndex: 1, identifier: 'channel-123' },
450
+ * ]
451
+ * const balanceQueries = useBalancesForWallets(wallets, tokenConfigs, { enabled: true })
452
+ *
453
+ * const isLoading = balanceQueries.some(q => q.isLoading)
454
+ * const hasError = balanceQueries.some(q => q.isError)
455
+ * ```
456
+ */
457
+ export function useBalancesForWallets(
458
+ wallets: Array<{ accountIndex: number; identifier: string }>,
459
+ tokenConfigs: TokenConfigProvider,
460
+ options?: BalanceQueryOptions
461
+ ) {
462
+ const workletStore = getWorkletStore()
463
+ const isInitialized = workletStore.getState().isInitialized
464
+
465
+ return useQueries({
466
+ queries: wallets.map((wallet) => {
467
+ const { accountIndex, identifier } = wallet
468
+
469
+ // Create query keys for all tokens (with walletId)
470
+ const queryKeys = buildBalanceQueryKeys(identifier, accountIndex, tokenConfigs)
471
+
472
+ // Get initial data from Zustand (single source of truth)
473
+ const initialData: BalanceFetchResult[] | undefined = (() => {
474
+ const tokenConfigsObj = typeof tokenConfigs === 'function' ? tokenConfigs() : tokenConfigs
475
+ const networks = Object.keys(tokenConfigsObj)
476
+
477
+ const initialBalances: BalanceFetchResult[] = []
478
+ let hasAnyInitialData = false
479
+
480
+ for (const network of networks) {
481
+ const networkTokens = tokenConfigsObj[network]
482
+ if (!networkTokens) continue
483
+
484
+ const tokens = [networkTokens.native, ...networkTokens.tokens]
485
+ for (const token of tokens) {
486
+ const balance = BalanceService.getBalance(accountIndex, network, token.address, identifier)
487
+ if (balance !== null) {
488
+ hasAnyInitialData = true
489
+ initialBalances.push({
490
+ success: true,
491
+ network,
492
+ accountIndex,
493
+ tokenAddress: token.address,
494
+ balance,
495
+ })
496
+ } else {
497
+ // Include placeholder for missing balances to maintain structure
498
+ initialBalances.push({
499
+ success: false,
500
+ network,
501
+ accountIndex,
502
+ tokenAddress: token.address,
503
+ balance: null,
504
+ error: 'Balance not available',
505
+ })
506
+ }
507
+ }
508
+ }
509
+
510
+ // Only return initial data if we have at least one persisted balance
511
+ return hasAnyInitialData ? initialBalances : undefined
512
+ })()
513
+
514
+ return {
515
+ queryKey: [...balanceQueryKeys.byWallet(identifier, accountIndex), 'all'],
516
+ queryFn: () => fetchBalancesForQueryKeys(queryKeys, identifier),
517
+ enabled: isQueryEnabled(options?.enabled, isInitialized, queryKeys.length > 0),
518
+ refetchInterval: options?.refetchInterval,
519
+ staleTime: options?.staleTime ?? DEFAULT_QUERY_STALE_TIME_MS,
520
+ gcTime: DEFAULT_QUERY_GC_TIME_MS,
521
+ // Use Zustand as initial data source (single source of truth)
522
+ initialData,
523
+ }
524
+ }),
525
+ })
526
+ }
527
+
528
+ /**
529
+ * Invalidate balance queries based on refresh type
530
+ */
531
+ async function invalidateBalanceQueries(
532
+ queryClient: ReturnType<typeof useQueryClient>,
533
+ params: RefreshBalanceParams,
534
+ walletId: string
535
+ ): Promise<void> {
536
+ const { network, accountIndex, tokenAddress, type = 'token' } = params
537
+
538
+ switch (type) {
539
+ case 'all':
540
+ await queryClient.invalidateQueries({ queryKey: balanceQueryKeys.all })
541
+ break
542
+ case 'wallet':
543
+ await queryClient.invalidateQueries({
544
+ queryKey: balanceQueryKeys.byWallet(walletId, accountIndex),
545
+ })
546
+ break
547
+ case 'network':
548
+ if (network) {
549
+ await queryClient.invalidateQueries({
550
+ queryKey: balanceQueryKeys.byNetwork(network),
551
+ })
552
+ }
553
+ break
554
+ case 'token':
555
+ default:
556
+ if (network && tokenAddress !== undefined) {
557
+ await queryClient.invalidateQueries({
558
+ queryKey: balanceQueryKeys.byToken(walletId, accountIndex, network, tokenAddress),
559
+ })
560
+ }
561
+ break
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Hook to invalidate and refetch balances
567
+ *
568
+ * @returns Mutation function to refresh balances
569
+ *
570
+ * @example
571
+ * ```tsx
572
+ * const { mutate: refreshBalance } = useRefreshBalance()
573
+ *
574
+ * // Refresh single balance
575
+ * refreshBalance({ network: 'ethereum', accountIndex: 0, tokenAddress: null })
576
+ *
577
+ * // Refresh all balances for a wallet
578
+ * refreshBalance({ accountIndex: 0, type: 'wallet' })
579
+ * ```
580
+ */
581
+ export function useRefreshBalance() {
582
+ const queryClient = useQueryClient()
583
+
584
+ return useMutation({
585
+ mutationFn: async (params: RefreshBalanceParams) => {
586
+ // Resolve walletId from params or store
587
+ const walletId = resolveWalletId(params.walletId)
588
+
589
+ // Invalidate queries based on type
590
+ await invalidateBalanceQueries(queryClient, params, walletId)
591
+
592
+ // Refetch the invalidated queries
593
+ await queryClient.refetchQueries()
594
+ },
595
+ })
596
+ }
597
+