@exodus/balances 13.16.1 → 13.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/atoms/asset-names-with-balance.d.ts +1 -0
  3. package/atoms/asset-names-with-balance.js +1 -40
  4. package/atoms/asset-total-wallet-amounts.d.ts +1 -0
  5. package/atoms/asset-total-wallet-amounts.js +1 -47
  6. package/atoms/assets-with-balance-count.d.ts +1 -0
  7. package/atoms/assets-with-balance-count.js +1 -8
  8. package/atoms/balances.d.ts +1 -0
  9. package/atoms/balances.js +1 -6
  10. package/atoms/funded-wallet-accounts.d.ts +1 -0
  11. package/atoms/funded-wallet-accounts.js +1 -25
  12. package/atoms/has-balance.d.ts +1 -0
  13. package/atoms/has-balance.js +1 -31
  14. package/atoms/index.d.ts +8 -0
  15. package/atoms/index.js +8 -54
  16. package/default-config.d.ts +1 -0
  17. package/default-config.js +1 -26
  18. package/index.d.ts +2 -35
  19. package/index.js +1 -65
  20. package/lib/atoms/asset-names-with-balance.d.ts +5 -0
  21. package/lib/atoms/asset-names-with-balance.js +31 -0
  22. package/lib/atoms/asset-total-wallet-amounts.d.ts +5 -0
  23. package/lib/atoms/asset-total-wallet-amounts.js +39 -0
  24. package/lib/atoms/assets-with-balance-count.d.ts +5 -0
  25. package/lib/atoms/assets-with-balance-count.js +6 -0
  26. package/lib/atoms/balances.d.ts +3 -0
  27. package/lib/atoms/balances.js +3 -0
  28. package/lib/atoms/funded-wallet-accounts.d.ts +8 -0
  29. package/lib/atoms/funded-wallet-accounts.js +19 -0
  30. package/lib/atoms/has-balance.d.ts +7 -0
  31. package/lib/atoms/has-balance.js +21 -0
  32. package/lib/atoms/index.d.ts +54 -0
  33. package/lib/atoms/index.js +48 -0
  34. package/lib/default-config.d.ts +3 -0
  35. package/lib/default-config.js +24 -0
  36. package/lib/index.d.ts +187 -0
  37. package/lib/index.js +49 -0
  38. package/lib/module/balances-utils.d.ts +13 -0
  39. package/lib/module/balances-utils.js +70 -0
  40. package/lib/module/index.d.ts +11 -0
  41. package/lib/module/index.js +367 -0
  42. package/lib/module/mockable-balances.d.ts +16 -0
  43. package/lib/module/mockable-balances.js +21 -0
  44. package/lib/module/types.d.ts +80 -0
  45. package/lib/module/types.js +1 -0
  46. package/lib/plugins/analytics.d.ts +18 -0
  47. package/lib/plugins/analytics.js +30 -0
  48. package/lib/plugins/index.d.ts +35 -0
  49. package/lib/plugins/index.js +22 -0
  50. package/lib/plugins/lifecycle.d.ts +14 -0
  51. package/lib/plugins/lifecycle.js +32 -0
  52. package/lib/redux/id.d.ts +2 -0
  53. package/lib/redux/id.js +1 -0
  54. package/lib/redux/index.d.ts +588 -0
  55. package/lib/redux/index.js +53 -0
  56. package/lib/redux/initial-state.d.ts +3 -0
  57. package/lib/redux/initial-state.js +6 -0
  58. package/lib/redux/multi-account-helper.d.ts +463 -0
  59. package/lib/redux/multi-account-helper.js +23 -0
  60. package/lib/redux/selectors/by-asset.d.ts +12 -0
  61. package/lib/redux/selectors/by-asset.js +19 -0
  62. package/lib/redux/selectors/create-balance-selectors.d.ts +70 -0
  63. package/lib/redux/selectors/create-balance-selectors.js +32 -0
  64. package/lib/redux/selectors/create-balances.d.ts +11 -0
  65. package/lib/redux/selectors/create-balances.js +13 -0
  66. package/lib/redux/selectors/create-fuel-threshold.d.ts +19 -0
  67. package/lib/redux/selectors/create-fuel-threshold.js +22 -0
  68. package/lib/redux/selectors/create-requires-fuel-threshold.d.ts +19 -0
  69. package/lib/redux/selectors/create-requires-fuel-threshold.js +29 -0
  70. package/lib/redux/selectors/create-unconfirmed-balance.d.ts +15 -0
  71. package/lib/redux/selectors/create-unconfirmed-balance.js +14 -0
  72. package/lib/redux/selectors/get-balance-for-field.d.ts +14 -0
  73. package/lib/redux/selectors/get-balance-for-field.js +19 -0
  74. package/lib/redux/selectors/get-balance-selectors.d.ts +68 -0
  75. package/lib/redux/selectors/get-balance-selectors.js +32 -0
  76. package/lib/redux/selectors/get-balances.d.ts +10 -0
  77. package/lib/redux/selectors/get-balances.js +13 -0
  78. package/lib/redux/selectors/get-unconfirmed-balance.d.ts +19 -0
  79. package/lib/redux/selectors/get-unconfirmed-balance.js +27 -0
  80. package/lib/redux/selectors/index.d.ts +339 -0
  81. package/lib/redux/selectors/index.js +22 -0
  82. package/lib/redux/types.d.ts +45 -0
  83. package/lib/redux/types.js +1 -0
  84. package/lib/report/index.d.ts +34 -0
  85. package/lib/report/index.js +51 -0
  86. package/lib/types.d.ts +24 -0
  87. package/lib/types.js +1 -0
  88. package/module/balances-utils.d.ts +1 -0
  89. package/module/balances-utils.js +1 -86
  90. package/module/index.d.ts +2 -0
  91. package/module/index.js +2 -497
  92. package/module/mockable-balances.d.ts +1 -0
  93. package/module/mockable-balances.js +1 -27
  94. package/package.json +23 -17
  95. package/plugins/analytics.d.ts +1 -0
  96. package/plugins/analytics.js +1 -48
  97. package/plugins/index.d.ts +4 -0
  98. package/plugins/index.js +4 -24
  99. package/plugins/lifecycle.d.ts +1 -0
  100. package/plugins/lifecycle.js +1 -40
  101. package/redux/id.d.ts +1 -0
  102. package/redux/id.js +1 -1
  103. package/redux/index.d.ts +2 -0
  104. package/redux/index.js +1 -61
  105. package/redux/initial-state.d.ts +1 -0
  106. package/redux/initial-state.js +1 -8
  107. package/redux/multi-account-helper.d.ts +1 -0
  108. package/redux/multi-account-helper.js +1 -14
  109. package/redux/selectors/by-asset.d.ts +1 -0
  110. package/redux/selectors/by-asset.js +1 -25
  111. package/redux/selectors/create-balance-selectors.d.ts +1 -0
  112. package/redux/selectors/create-balance-selectors.js +1 -48
  113. package/redux/selectors/create-balances.d.ts +1 -0
  114. package/redux/selectors/create-balances.js +1 -26
  115. package/redux/selectors/create-fuel-threshold.d.ts +1 -0
  116. package/redux/selectors/create-fuel-threshold.js +1 -32
  117. package/redux/selectors/create-requires-fuel-threshold.d.ts +1 -0
  118. package/redux/selectors/create-requires-fuel-threshold.js +1 -59
  119. package/redux/selectors/create-unconfirmed-balance.d.ts +1 -0
  120. package/redux/selectors/create-unconfirmed-balance.js +1 -28
  121. package/redux/selectors/get-balance-for-field.d.ts +1 -0
  122. package/redux/selectors/get-balance-for-field.js +1 -34
  123. package/redux/selectors/get-balance-selectors.d.ts +1 -0
  124. package/redux/selectors/get-balance-selectors.js +1 -42
  125. package/redux/selectors/get-balances.d.ts +1 -0
  126. package/redux/selectors/get-balances.js +1 -24
  127. package/redux/selectors/get-unconfirmed-balance.d.ts +2 -0
  128. package/redux/selectors/get-unconfirmed-balance.js +2 -43
  129. package/redux/selectors/index.d.ts +1 -0
  130. package/redux/selectors/index.js +1 -23
  131. package/report/index.d.ts +1 -0
  132. package/report/index.js +1 -61
  133. package/tsconfig.json +0 -8
@@ -0,0 +1,367 @@
1
+ import { safeString } from '@exodus/safe-string';
2
+ import { TxSet } from '@exodus/models';
3
+ import { difference as getChanges } from '@exodus/atoms';
4
+ import { difference, pick, set, mapValues } from '@exodus/basic-utils';
5
+ import makeConcurrent from 'make-concurrent';
6
+ import { processAssetBalances, validateBalances } from './balances-utils.js';
7
+ import defaultConfig from '../default-config.js';
8
+ const zipObject = (keys, values) => {
9
+ const result = Object.create(null);
10
+ for (const [i, key] of keys.entries()) {
11
+ result[key] = values[i];
12
+ }
13
+ return result;
14
+ };
15
+ export const cloneBalances = (balances) => {
16
+ const result = Object.create(null);
17
+ for (const walletAccount of Object.keys(balances)) {
18
+ const accountBalances = balances[walletAccount];
19
+ const clonedAccount = Object.create(null);
20
+ for (const assetName of Object.keys(accountBalances)) {
21
+ clonedAccount[assetName] = { ...accountBalances[assetName] };
22
+ }
23
+ result[walletAccount] = clonedAccount;
24
+ }
25
+ return result;
26
+ };
27
+ const zipNonNull = (keys, values) => {
28
+ const result = Object.create(null);
29
+ for (const [i, key] of keys.entries()) {
30
+ if (values[i] != null)
31
+ result[key] = values[i];
32
+ }
33
+ return result;
34
+ };
35
+ class Balances {
36
+ #logger;
37
+ #assetsAtom;
38
+ #assets = Object.create(null);
39
+ #availableAssetNamesAtom;
40
+ #availableAssetNames = [];
41
+ #balanceFieldsConfig;
42
+ #balanceFields;
43
+ #walletAccounts = [];
44
+ #ready = false;
45
+ #balances = Object.create(null);
46
+ #balancesAtom;
47
+ #blockchainMetadataAtom;
48
+ #feeDataAtom;
49
+ #enabledWalletAccountsAtom;
50
+ #loaded = false;
51
+ #subscriptions = [];
52
+ #errorTracking;
53
+ constructor({ assetsAtom, availableAssetNamesAtom, enabledWalletAccountsAtom, config, logger, balancesAtom, blockchainMetadataAtom, feeDataAtom, errorTracking, }) {
54
+ this.#logger = logger;
55
+ this.#assetsAtom = assetsAtom;
56
+ this.#availableAssetNamesAtom = availableAssetNamesAtom;
57
+ this.#balanceFieldsConfig = config?.balanceFieldsConfig || defaultConfig.balanceFieldsConfig;
58
+ this.#balanceFields = config?.balanceFields || defaultConfig.balanceFields;
59
+ this.#balancesAtom = balancesAtom;
60
+ this.#blockchainMetadataAtom = blockchainMetadataAtom;
61
+ this.#feeDataAtom = feeDataAtom;
62
+ this.#enabledWalletAccountsAtom = enabledWalletAccountsAtom;
63
+ this.#errorTracking = errorTracking;
64
+ const shareQueue = (fn) => (...args) => {
65
+ if (!this.#ready)
66
+ return;
67
+ void this.#pushIntoSharedQueue(() => fn.apply(this, args));
68
+ };
69
+ const swallowErrors = (fn, method) => async (...args) => {
70
+ try {
71
+ return await fn(...args);
72
+ }
73
+ catch (error) {
74
+ this.#errorTracking.track({ error: new Error(method, { cause: error }) });
75
+ }
76
+ };
77
+ this.#updateAssetSource = shareQueue(this.#updateAssetSource);
78
+ this.#handleBlockchainMetadataUpdate = shareQueue(this.#handleBlockchainMetadataUpdate);
79
+ this.#handleFeeDataUpdate = shareQueue(this.#handleFeeDataUpdate);
80
+ this.#recompute = shareQueue(this.#recompute);
81
+ this.#onAssetsChanged = shareQueue(this.#onAssetsChanged);
82
+ this.#getBalancesForAssetSource = swallowErrors(this.#getBalancesForAssetSource, safeString `getBalancesForAssetSource`);
83
+ this.#setWalletAccounts = swallowErrors(this.#setWalletAccounts, safeString `setWalletAccounts`);
84
+ }
85
+ load = async () => {
86
+ if (this.#loaded)
87
+ return;
88
+ this.#loaded = true;
89
+ const { value } = await this.#assetsAtom.get();
90
+ this.#assets = value;
91
+ this.#subscriptions.push(this.#assetsAtom.observe(({ value }) => {
92
+ this.#assets = value;
93
+ }), this.#enabledWalletAccountsAtom.observe((payload) => {
94
+ void this.#setWalletAccounts(Object.keys(payload));
95
+ }), this.#blockchainMetadataAtom.observe(({ accountStateChanges, txLogChanges }) => {
96
+ this.#flattenToBlockchainMetadataObjects({ accountStateChanges, txLogChanges }).forEach(({ walletAccount, assetName, accountState, txLogsByAssetName }) => this.#handleBlockchainMetadataUpdate({
97
+ walletAccount,
98
+ assetName,
99
+ accountState,
100
+ txLogsByAssetName,
101
+ }));
102
+ }), this.#feeDataAtom.observe((changes) => {
103
+ Object.entries(changes).forEach(([assetName, feeData]) => {
104
+ const asset = this.#getAsset(assetName).baseAsset;
105
+ if (asset.api?.features?.balancesRequireFeeData) {
106
+ this.#walletAccounts.forEach((walletAccount) => {
107
+ return this.#handleFeeDataUpdate({ walletAccount, assetName, feeData });
108
+ });
109
+ }
110
+ });
111
+ }), getChanges(this.#availableAssetNamesAtom).observe(({ previous = [], current }) => {
112
+ this.#availableAssetNames = current;
113
+ const added = difference(current, previous);
114
+ const removed = difference(previous, current);
115
+ if (added.length > 0 || removed.length > 0)
116
+ void this.#onAssetsChanged({ added, removed });
117
+ }));
118
+ this.#availableAssetNames = await this.#availableAssetNamesAtom.get();
119
+ };
120
+ #getAsset = (assetName) => this.#assets[assetName];
121
+ #getAssets = () => this.#assets;
122
+ #flattenToBlockchainMetadataObjects = ({ accountStateChanges = Object.create(null), txLogChanges = Object.create(null), }) => {
123
+ const byBaseAssetName = mapValues(accountStateChanges, (accountStates) => mapValues(accountStates, (accountState) => ({ accountState })));
124
+ Object.entries(txLogChanges).forEach(([walletAccount, txLogs]) => {
125
+ Object.entries(txLogs).map(([assetName, txLog]) => {
126
+ const { baseAsset } = this.#getAsset(assetName);
127
+ set(byBaseAssetName, [walletAccount, baseAsset.name, 'txLogsByAssetName', assetName], txLog);
128
+ });
129
+ });
130
+ return Object.entries(byBaseAssetName).flatMap(([walletAccount, baseAssets]) => Object.entries(baseAssets).map(([baseAssetName, { accountState, txLogsByAssetName }]) => ({
131
+ walletAccount,
132
+ assetName: baseAssetName,
133
+ ...(accountState && { accountState }),
134
+ ...(txLogsByAssetName && { txLogsByAssetName }),
135
+ })));
136
+ };
137
+ #handleBlockchainMetadataUpdate = (opts) => {
138
+ if (!this.#ready) {
139
+ this.#logger.log('ignore update from blockchain-metadata event.', { isReady: this.#ready });
140
+ return;
141
+ }
142
+ return this.#updateAssetSource(opts);
143
+ };
144
+ #handleFeeDataUpdate = (opts) => {
145
+ if (!this.#ready) {
146
+ this.#logger.log('ignore update from fee-data event.', { isReady: this.#ready });
147
+ return;
148
+ }
149
+ return this.#updateAssetSource(opts);
150
+ };
151
+ #pushIntoSharedQueue = makeConcurrent((fn) => fn(), { concurrency: 1 });
152
+ #reset = ({ walletAccounts = this.#walletAccounts } = {}) => {
153
+ walletAccounts.forEach((walletAccount) => {
154
+ this.#balances[walletAccount] = Object.create(null);
155
+ });
156
+ };
157
+ #getBalancesForAssetSource = async ({ walletAccount, assetName, }) => {
158
+ const metadata = await this.#loadBlockchainMetadataForBalance({
159
+ walletAccount,
160
+ assetName,
161
+ });
162
+ return this.#getBalancesFromBlockchainMetadata({
163
+ assetNames: this.#getRelatedAssetNames(assetName),
164
+ ...metadata,
165
+ });
166
+ };
167
+ #getBalancesFromBlockchainMetadata = ({ assetNames, accountState, feeData, txLogsByAssetName, }) => {
168
+ const getAssetBalances = (assetName) => {
169
+ const asset = this.#getAsset(assetName);
170
+ const txLog = txLogsByAssetName[assetName];
171
+ const zero = asset.currency.ZERO;
172
+ const apiBalances = asset.api.getBalances({ asset, accountState, txLog, feeData });
173
+ const balances = processAssetBalances({
174
+ balanceFieldsConfig: this.#balanceFieldsConfig,
175
+ balances: apiBalances || Object.create(null),
176
+ zero,
177
+ });
178
+ return validateBalances({
179
+ balances: pick(balances, this.#balanceFields),
180
+ asset,
181
+ logger: this.#logger,
182
+ });
183
+ };
184
+ const balances = assetNames.map(getAssetBalances);
185
+ return zipNonNull(assetNames, balances);
186
+ };
187
+ #getBalancesForWalletAccount = async ({ walletAccount, }) => {
188
+ const assetNames = this.#getSupportedAssetNames();
189
+ const results = await Promise.allSettled(assetNames.map((assetName) => this.#getBalancesForAssetSource({ assetName, walletAccount })));
190
+ const balances = results
191
+ .map((result) => (result.status === 'fulfilled' ? result.value : undefined))
192
+ .filter(Boolean);
193
+ const accountBalances = Object.create(null);
194
+ Object.assign(accountBalances, ...balances);
195
+ return accountBalances;
196
+ };
197
+ #load = async ({ walletAccount }) => {
198
+ this.#logger.log(`loading walletAccount ${walletAccount}`);
199
+ this.#balances[walletAccount] = await this.#getBalancesForWalletAccount({ walletAccount });
200
+ };
201
+ #setWalletAccounts = async (walletAccounts) => {
202
+ const added = difference(walletAccounts, this.#walletAccounts);
203
+ const removed = difference(this.#walletAccounts, walletAccounts);
204
+ if (added.length === 0 && removed.length === 0)
205
+ return;
206
+ this.#walletAccounts = walletAccounts;
207
+ this.#ready = true;
208
+ if (removed.length > 0) {
209
+ this.#logger.log('recomputing after removing walletAccounts', removed);
210
+ removed.forEach((walletAccount) => {
211
+ delete this.#balances[walletAccount];
212
+ });
213
+ }
214
+ if (added.length > 0) {
215
+ this.#logger.log('recomputing after adding walletAccounts', added);
216
+ await this.#recompute({ walletAccounts: added });
217
+ }
218
+ else {
219
+ this.#flush();
220
+ }
221
+ };
222
+ #onAssetsChanged = async ({ added, removed }) => {
223
+ if (removed.length > 0) {
224
+ for (const walletAccount of this.#walletAccounts) {
225
+ for (const assetName of removed) {
226
+ delete this.#balances[walletAccount]?.[assetName];
227
+ }
228
+ }
229
+ }
230
+ const newSupportedAssets = added.filter(this.#isSupportedAsset);
231
+ const missingAssets = newSupportedAssets.filter((name) => !this.#walletAccounts.every((walletAccount) => !!this.#balances?.[walletAccount]?.[name]));
232
+ if (missingAssets.length === 0) {
233
+ if (removed.length > 0)
234
+ this.#flush();
235
+ return;
236
+ }
237
+ this.#logger.log('computing balances for newly available assets', missingAssets);
238
+ await Promise.all(this.#walletAccounts.map(async (walletAccount) => {
239
+ const balances = await Promise.all(missingAssets.map((assetName) => this.#getBalancesForAssetSource({ assetName, walletAccount })));
240
+ if (this.#balances[walletAccount]) {
241
+ Object.assign(this.#balances[walletAccount], ...balances);
242
+ }
243
+ }));
244
+ this.#flush();
245
+ };
246
+ #isSupportedAsset = (assetName) => {
247
+ if (!this.#availableAssetNames.includes(assetName))
248
+ return false;
249
+ const asset = this.#getAsset(assetName);
250
+ return asset && !asset.isCombined;
251
+ };
252
+ #getSupportedAssetNames = () => Object.keys(this.#getAssets()).filter(this.#isSupportedAsset);
253
+ #getRelatedAssetNames = (assetName) => {
254
+ const asset = this.#assets[assetName];
255
+ if (asset.baseAsset.name === assetName) {
256
+ return Object.keys(this.#assets).filter((name) => this.#assets[name].baseAsset.name === assetName && this.#isSupportedAsset(name));
257
+ }
258
+ return [assetName];
259
+ };
260
+ #loadBlockchainMetadataForBalance = async ({ assetName, walletAccount, feeData: providedFeeData, accountState: providedAccountState = null, txLogsByAssetName: providedTxLogsByAssetName = {}, }) => {
261
+ const { value: { txs: txLogsByAssetSource, accountStates }, } = await this.#blockchainMetadataAtom.get();
262
+ let accountState = providedAccountState;
263
+ const baseAsset = this.#getAsset(assetName).baseAsset;
264
+ if (!accountState) {
265
+ const cached = accountStates[walletAccount]?.[baseAsset.name];
266
+ if (cached) {
267
+ accountState = cached;
268
+ }
269
+ else {
270
+ const AccountState = baseAsset.api?.createAccountState?.();
271
+ accountState = AccountState?.create();
272
+ }
273
+ }
274
+ let feeData = providedFeeData;
275
+ if (!feeData && baseAsset.api?.features?.balancesRequireFeeData) {
276
+ const fees = await this.#feeDataAtom.get();
277
+ feeData = fees[baseAsset.name];
278
+ }
279
+ const assetNames = this.#getRelatedAssetNames(assetName);
280
+ const txLogsByAssetName = zipObject(assetNames, assetNames.map((assetName) => providedTxLogsByAssetName[assetName] ||
281
+ txLogsByAssetSource[walletAccount]?.[assetName] ||
282
+ TxSet.EMPTY));
283
+ return {
284
+ feeData,
285
+ accountState,
286
+ txLogsByAssetName,
287
+ };
288
+ };
289
+ #updateAssetSource = async ({ assetName, walletAccount, accountState: providedAccountState, txLogsByAssetName: providedTxLogsByAssetName, feeData: providedFeeData, }) => {
290
+ if (!this.#isSupportedAsset(assetName))
291
+ return;
292
+ if (!this.#walletAccounts.includes(walletAccount)) {
293
+ this.#logger.warn(`unexpected update for disabled walletAccount ${walletAccount}`);
294
+ return;
295
+ }
296
+ const { accountState, txLogsByAssetName, feeData } = await this.#loadBlockchainMetadataForBalance({
297
+ assetName,
298
+ walletAccount,
299
+ feeData: providedFeeData,
300
+ accountState: providedAccountState,
301
+ txLogsByAssetName: providedTxLogsByAssetName,
302
+ });
303
+ const accountBalances = this.#balances[walletAccount];
304
+ const newBalances = this.#getBalancesFromBlockchainMetadata({
305
+ assetNames: this.#getRelatedAssetNames(assetName),
306
+ accountState,
307
+ txLogsByAssetName,
308
+ feeData,
309
+ });
310
+ const changes = Object.create(null);
311
+ for (const subAssetName in newBalances) {
312
+ const { currency } = this.#getAsset(subAssetName);
313
+ const newAssetBalances = newBalances?.[subAssetName];
314
+ for (const field of this.#balanceFields) {
315
+ const oldBalance = accountBalances?.[subAssetName]?.[field];
316
+ const newBalance = newAssetBalances?.[field];
317
+ if (!newBalance)
318
+ continue;
319
+ if ((oldBalance || currency.ZERO).equals(newBalance))
320
+ continue;
321
+ this.#logger.log(`updating assetSource ${walletAccount}:${assetName}`);
322
+ set(changes, [walletAccount, subAssetName, field], {
323
+ from: oldBalance ?? currency.ZERO,
324
+ to: newBalance,
325
+ });
326
+ set(accountBalances, [subAssetName, field], newBalance);
327
+ }
328
+ }
329
+ if (Object.keys(changes).length > 0)
330
+ this.#flush({ changes });
331
+ };
332
+ #recompute = async ({ walletAccounts = this.#walletAccounts, } = {}) => {
333
+ this.#reset({ walletAccounts });
334
+ await Promise.all(walletAccounts.map((walletAccount) => this.#load({ walletAccount })));
335
+ this.#flush();
336
+ };
337
+ #flush = ({ changes } = {}) => {
338
+ const balances = cloneBalances(this.#balances);
339
+ if (Object.keys(balances).length === 0)
340
+ return;
341
+ this.#logger.log('balances updated');
342
+ void this.#balancesAtom.set({ balances, changes });
343
+ };
344
+ stop = () => {
345
+ this.#subscriptions.forEach((unsubscribe) => unsubscribe());
346
+ this.#subscriptions = [];
347
+ };
348
+ }
349
+ const createBalances = (opts) => new Balances(opts);
350
+ const balancesDefinition = {
351
+ id: 'balances',
352
+ type: 'module',
353
+ factory: createBalances,
354
+ dependencies: [
355
+ 'assetsAtom',
356
+ 'availableAssetNamesAtom',
357
+ 'enabledWalletAccountsAtom',
358
+ 'config',
359
+ 'logger',
360
+ 'balancesAtom',
361
+ 'blockchainMetadataAtom',
362
+ 'feeDataAtom',
363
+ 'errorTracking',
364
+ ],
365
+ public: true,
366
+ };
367
+ export default balancesDefinition;
@@ -0,0 +1,16 @@
1
+ import type { BalancesModule } from './types.js';
2
+ import type { BalancesAtom, BalancesByAssetSource } from '../types.js';
3
+ declare const mockableBalancesDefinition: {
4
+ readonly id: "balances";
5
+ readonly type: "module";
6
+ readonly factory: (opts: Options) => BalancesModule;
7
+ readonly dependencies: readonly ["config", "balancesAtom"];
8
+ readonly public: true;
9
+ };
10
+ type Options = {
11
+ config: {
12
+ importReport: BalancesByAssetSource;
13
+ };
14
+ balancesAtom: BalancesAtom;
15
+ };
16
+ export default mockableBalancesDefinition;
@@ -0,0 +1,21 @@
1
+ class Balances {
2
+ #balancesAtom;
3
+ #configBalances;
4
+ constructor({ config, balancesAtom }) {
5
+ this.#balancesAtom = balancesAtom;
6
+ this.#configBalances = config.importReport;
7
+ }
8
+ load = async () => {
9
+ void this.#balancesAtom.set({ balances: this.#configBalances });
10
+ };
11
+ stop = () => { };
12
+ }
13
+ const createBalances = (opts) => new Balances(opts);
14
+ const mockableBalancesDefinition = {
15
+ id: 'balances',
16
+ type: 'module',
17
+ factory: createBalances,
18
+ dependencies: ['config', 'balancesAtom'],
19
+ public: true,
20
+ };
21
+ export default mockableBalancesDefinition;
@@ -0,0 +1,80 @@
1
+ import type { Atom, ReadonlyAtom, Logger } from '@exodus/atoms';
2
+ import type { TxSet, WalletAccount } from '@exodus/models';
3
+ import type { BaseAsset } from '@exodus/asset-types';
4
+ import type { BalancesAtomValue, BalancesByAssetSource } from '../types.js';
5
+ import type NumberUnit from '@exodus/currency';
6
+ export type FieldBalances = Record<string, NumberUnit>;
7
+ export type BalanceFieldConfig = {
8
+ name: string;
9
+ legacyName?: string;
10
+ default?: string;
11
+ };
12
+ export type BalancesConfig = {
13
+ balanceFieldsConfig: BalanceFieldConfig[];
14
+ balanceFields: string[];
15
+ assetsToTrackForBalances: {
16
+ assetName: string;
17
+ analyticsName: string;
18
+ }[];
19
+ importReport?: BalancesByAssetSource;
20
+ };
21
+ export type AccountState = object;
22
+ export type FeeData = Record<string, unknown>;
23
+ export type Metadata = {
24
+ accountState?: AccountState | null;
25
+ txLogsByAssetName: Record<string, TxSet>;
26
+ feeData?: FeeData;
27
+ };
28
+ export type Source = {
29
+ walletAccount: string;
30
+ assetName: string;
31
+ };
32
+ export type SourceUpdate = Source & Partial<Metadata>;
33
+ export type MetadataChanges = {
34
+ accountStateChanges?: Record<string, Record<string, AccountState>>;
35
+ txLogChanges?: Record<string, Record<string, TxSet>>;
36
+ };
37
+ export type BalanceAsset = Pick<BaseAsset, 'name' | 'currency'> & {
38
+ isCombined?: boolean;
39
+ baseAsset: BalanceAsset;
40
+ api: {
41
+ features?: {
42
+ balancesRequireFeeData?: boolean;
43
+ };
44
+ createAccountState?: () => {
45
+ create(): AccountState;
46
+ };
47
+ getBalances(params: {
48
+ asset: BalanceAsset;
49
+ accountState?: AccountState | null;
50
+ txLog?: TxSet;
51
+ feeData?: FeeData;
52
+ }): FieldBalances | undefined;
53
+ };
54
+ };
55
+ export type Options = {
56
+ assetsAtom: ReadonlyAtom<{
57
+ value: Record<string, BalanceAsset>;
58
+ }>;
59
+ availableAssetNamesAtom: ReadonlyAtom<string[]>;
60
+ enabledWalletAccountsAtom: ReadonlyAtom<Record<string, WalletAccount>>;
61
+ config?: Partial<BalancesConfig>;
62
+ logger: Pick<Logger, 'log' | 'warn'>;
63
+ balancesAtom: Atom<BalancesAtomValue>;
64
+ blockchainMetadataAtom: ReadonlyAtom<MetadataChanges & {
65
+ value: {
66
+ txs: Record<string, Record<string, TxSet>>;
67
+ accountStates: Record<string, Record<string, AccountState>>;
68
+ };
69
+ }>;
70
+ feeDataAtom: ReadonlyAtom<Record<string, FeeData>>;
71
+ errorTracking: {
72
+ track(params: {
73
+ error: Error;
74
+ }): void;
75
+ };
76
+ };
77
+ export interface BalancesModule {
78
+ load(): Promise<void>;
79
+ stop(): void;
80
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import type { HasBalanceAtom } from '../types.js';
2
+ import type { ReadonlyAtom } from '@exodus/atoms';
3
+ import type { BalancesConfig } from '../module/types.js';
4
+ import type NumberUnit from '@exodus/currency';
5
+ declare const createBalancesAnalyticsPlugin: ({ analytics, hasBalanceAtom, assetsWithBalanceCountAtom, assetsTotalWalletAmountsAtom, config: { assetsToTrackForBalances }, }: {
6
+ analytics: {
7
+ requireDefaultEventProperties(keys: string[]): void;
8
+ setDefaultEventProperties(properties: Record<string, boolean | number>): void;
9
+ };
10
+ hasBalanceAtom: Pick<HasBalanceAtom, "get" | "observe">;
11
+ assetsWithBalanceCountAtom: ReadonlyAtom<number>;
12
+ assetsTotalWalletAmountsAtom: ReadonlyAtom<Map<string, NumberUnit>>;
13
+ config: Pick<BalancesConfig, "assetsToTrackForBalances">;
14
+ }) => {
15
+ onStart: () => void;
16
+ onStop: () => void;
17
+ };
18
+ export default createBalancesAnalyticsPlugin;
@@ -0,0 +1,30 @@
1
+ const createBalancesAnalyticsPlugin = ({ analytics, hasBalanceAtom, assetsWithBalanceCountAtom, assetsTotalWalletAmountsAtom, config: { assetsToTrackForBalances }, }) => {
2
+ let subscriptions = [];
3
+ const getAssetAmountFromNumberUnit = (unit) => {
4
+ if (!unit)
5
+ return 0;
6
+ return unit.toDefaultNumber();
7
+ };
8
+ const onStart = () => {
9
+ analytics.requireDefaultEventProperties(['hasBalance', 'numberOfAssets']);
10
+ const unsubscribeBalanceAtom = hasBalanceAtom.observe((hasBalance) => analytics.setDefaultEventProperties({ hasBalance }));
11
+ const unsubscribeBalanceCountAtom = assetsWithBalanceCountAtom.observe((numberOfAssets) => analytics.setDefaultEventProperties({ numberOfAssets }));
12
+ const unsubscribeWalletAmountsAtom = assetsTotalWalletAmountsAtom.observe(async (balances) => {
13
+ const assetBalances = assetsToTrackForBalances.reduce((acc, obj) => {
14
+ acc[obj.analyticsName] = getAssetAmountFromNumberUnit(balances.get(obj.assetName));
15
+ return acc;
16
+ }, Object.create(null));
17
+ analytics.setDefaultEventProperties(assetBalances);
18
+ });
19
+ subscriptions = [
20
+ unsubscribeBalanceAtom,
21
+ unsubscribeBalanceCountAtom,
22
+ unsubscribeWalletAmountsAtom,
23
+ ];
24
+ };
25
+ const onStop = () => {
26
+ subscriptions.forEach((unsubscribe) => unsubscribe());
27
+ };
28
+ return { onStart, onStop };
29
+ };
30
+ export default createBalancesAnalyticsPlugin;
@@ -0,0 +1,35 @@
1
+ export declare const balancesLifecyclePluginDefinition: {
2
+ readonly id: "balancesLifecyclePlugin";
3
+ readonly type: "plugin";
4
+ readonly factory: ({ balancesAtom, port, balances, hasBalanceAtom, }: {
5
+ balancesAtom: import("../types.js").BalancesAtom;
6
+ port: import("@exodus/atoms").Port<import("../types.js").BalancesAtomValue | boolean | undefined>;
7
+ balances: import("../module/types.js").BalancesModule;
8
+ hasBalanceAtom: import("../types.js").HasBalanceAtom;
9
+ }) => {
10
+ onLoad: () => void;
11
+ onStop: () => void;
12
+ onClear: () => Promise<void>;
13
+ };
14
+ readonly dependencies: readonly ["balancesAtom", "port", "balances", "hasBalanceAtom"];
15
+ readonly public: true;
16
+ };
17
+ export declare const balancesAnalyticsPluginDefinition: {
18
+ readonly id: "balancesAnalyticsPlugin";
19
+ readonly type: "plugin";
20
+ readonly factory: ({ analytics, hasBalanceAtom, assetsWithBalanceCountAtom, assetsTotalWalletAmountsAtom, config: { assetsToTrackForBalances }, }: {
21
+ analytics: {
22
+ requireDefaultEventProperties(keys: string[]): void;
23
+ setDefaultEventProperties(properties: Record<string, boolean | number>): void;
24
+ };
25
+ hasBalanceAtom: Pick<import("../types.js").HasBalanceAtom, "get" | "observe">;
26
+ assetsWithBalanceCountAtom: import("@exodus/atoms").ReadonlyAtom<number>;
27
+ assetsTotalWalletAmountsAtom: import("@exodus/atoms").ReadonlyAtom<Map<string, import("@exodus/currency").default>>;
28
+ config: Pick<import("../module/types.js").BalancesConfig, "assetsToTrackForBalances">;
29
+ }) => {
30
+ onStart: () => void;
31
+ onStop: () => void;
32
+ };
33
+ readonly dependencies: readonly ["analytics", "hasBalanceAtom", "assetsWithBalanceCountAtom", "assetsTotalWalletAmountsAtom", "config?"];
34
+ readonly public: true;
35
+ };
@@ -0,0 +1,22 @@
1
+ import createBalancesAnalyticsPlugin from './analytics.js';
2
+ import createBalancesLifecyclePlugin from './lifecycle.js';
3
+ export const balancesLifecyclePluginDefinition = {
4
+ id: 'balancesLifecyclePlugin',
5
+ type: 'plugin',
6
+ factory: createBalancesLifecyclePlugin,
7
+ dependencies: ['balancesAtom', 'port', 'balances', 'hasBalanceAtom'],
8
+ public: true,
9
+ };
10
+ export const balancesAnalyticsPluginDefinition = {
11
+ id: 'balancesAnalyticsPlugin',
12
+ type: 'plugin',
13
+ factory: createBalancesAnalyticsPlugin,
14
+ dependencies: [
15
+ 'analytics',
16
+ 'hasBalanceAtom',
17
+ 'assetsWithBalanceCountAtom',
18
+ 'assetsTotalWalletAmountsAtom',
19
+ 'config?',
20
+ ],
21
+ public: true,
22
+ };
@@ -0,0 +1,14 @@
1
+ import type { BalancesModule } from '../module/types.js';
2
+ import type { HasBalanceAtom, BalancesAtom, BalancesAtomValue } from '../types.js';
3
+ import type { Port } from '@exodus/atoms';
4
+ declare const createBalancesLifecyclePlugin: ({ balancesAtom, port, balances, hasBalanceAtom, }: {
5
+ balancesAtom: BalancesAtom;
6
+ port: Port<BalancesAtomValue | boolean | undefined>;
7
+ balances: BalancesModule;
8
+ hasBalanceAtom: HasBalanceAtom;
9
+ }) => {
10
+ onLoad: () => void;
11
+ onStop: () => void;
12
+ onClear: () => Promise<void>;
13
+ };
14
+ export default createBalancesLifecyclePlugin;
@@ -0,0 +1,32 @@
1
+ import { createAtomObserver } from '@exodus/atoms';
2
+ const createBalancesLifecyclePlugin = ({ balancesAtom, port, balances, hasBalanceAtom, }) => {
3
+ const balancesAtomObserver = createAtomObserver({
4
+ port,
5
+ atom: balancesAtom,
6
+ event: 'balances',
7
+ });
8
+ const hasBalanceAtomObserver = createAtomObserver({
9
+ port,
10
+ atom: hasBalanceAtom,
11
+ event: 'hasBalance',
12
+ });
13
+ const onLoad = () => {
14
+ hasBalanceAtomObserver.start();
15
+ balancesAtomObserver.start();
16
+ void balances.load();
17
+ };
18
+ const onStop = () => {
19
+ balancesAtomObserver.unregister();
20
+ hasBalanceAtomObserver.unregister();
21
+ balances.stop();
22
+ };
23
+ const onClear = async () => {
24
+ await hasBalanceAtom.set(undefined);
25
+ };
26
+ return {
27
+ onLoad,
28
+ onStop,
29
+ onClear,
30
+ };
31
+ };
32
+ export default createBalancesLifecyclePlugin;
@@ -0,0 +1,2 @@
1
+ declare const _default: "balances";
2
+ export default _default;
@@ -0,0 +1 @@
1
+ export default 'balances';