@exodus/balances 13.15.1 → 13.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [13.15.3](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/balances@13.15.2...@exodus/balances@13.15.3) (2026-03-11)
7
+
8
+ ### Performance
9
+
10
+ - perf(balances): optimize clone and incremental asset computation (#15480)
11
+
12
+ ## [13.15.2](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/balances@13.15.1...@exodus/balances@13.15.2) (2026-03-03)
13
+
14
+ ### Bug Fixes
15
+
16
+ - fix(balances): return empty object instead of undefined from createBalances selector (#15366)
17
+
6
18
  ## [13.15.1](https://github.com/ExodusMovement/exodus-hydra/compare/@exodus/balances@13.15.0...@exodus/balances@13.15.1) (2026-02-02)
7
19
 
8
20
  **Note:** Version bump only for package @exodus/balances
package/module/index.js CHANGED
@@ -1,34 +1,43 @@
1
1
  import { safeString } from '@exodus/safe-string'
2
- import { isNumberUnit } from '@exodus/currency'
3
2
  import { TxSet } from '@exodus/models'
4
- // eslint-disable-next-line no-restricted-imports -- TODO: Fix this the next time the file is edited.
5
- import lodash from 'lodash'
6
3
  import { difference as getChanges } from '@exodus/atoms'
7
4
  import { difference, pick, set, mapValues } from '@exodus/basic-utils'
8
5
  import makeConcurrent from 'make-concurrent'
9
6
  import { processAssetBalances, validateBalances } from './balances-utils.js'
10
7
  import defaultConfig from '../default-config.js'
11
8
 
12
- const { isEmpty, merge, zipObject } = lodash
9
+ const zipObject = (keys, values) => {
10
+ const result = Object.create(null)
11
+ for (const [i, key] of keys.entries()) {
12
+ result[key] = values[i]
13
+ }
14
+
15
+ return result
16
+ }
13
17
 
14
18
  export const cloneBalances = (balances) => {
15
- if (isNumberUnit(balances)) {
16
- return balances
17
- }
19
+ const result = Object.create(null)
20
+ for (const walletAccount of Object.keys(balances)) {
21
+ const accountBalances = balances[walletAccount]
22
+ const clonedAccount = Object.create(null)
23
+ for (const assetName of Object.keys(accountBalances)) {
24
+ clonedAccount[assetName] = { ...accountBalances[assetName] }
25
+ }
18
26
 
19
- const cloned = Object.create(Object.getPrototypeOf(balances))
20
- for (const key in balances) {
21
- cloned[key] = cloneBalances(balances[key])
27
+ result[walletAccount] = clonedAccount
22
28
  }
23
29
 
24
- return cloned
30
+ return result
25
31
  }
26
32
 
27
- const zipNonNull = (keys, values) =>
28
- zipObject(
29
- keys.filter((key, i) => values[i] != null),
30
- values.filter((value) => value != null)
31
- )
33
+ const zipNonNull = (keys, values) => {
34
+ const result = Object.create(null)
35
+ for (const [i, key] of keys.entries()) {
36
+ if (values[i] != null) result[key] = values[i]
37
+ }
38
+
39
+ return result
40
+ }
32
41
 
33
42
  class Balances {
34
43
  #logger
@@ -134,7 +143,8 @@ class Balances {
134
143
  getChanges(this.#availableAssetNamesAtom).observe(({ previous = [], current }) => {
135
144
  this.#availableAssetNames = current
136
145
  const added = difference(current, previous)
137
- if (added.length > 0) this.#onAssetsChanged(added)
146
+ const removed = difference(previous, current)
147
+ if (added.length > 0 || removed.length > 0) this.#onAssetsChanged({ added, removed })
138
148
  })
139
149
  )
140
150
 
@@ -247,7 +257,7 @@ class Balances {
247
257
  )
248
258
 
249
259
  const balances = results.map(({ value }) => value).filter(Boolean)
250
- return merge(Object.create(null), ...balances)
260
+ return Object.assign(Object.create(null), ...balances)
251
261
  }
252
262
 
253
263
  #load = async ({ walletAccount }) => {
@@ -277,21 +287,48 @@ class Balances {
277
287
  }
278
288
  }
279
289
 
280
- #onAssetsChanged = async (added) => {
281
- if (
282
- added.every((name) =>
283
- this.#walletAccounts.every((walletAccount) => !!this.#balances?.[walletAccount]?.[name])
284
- )
285
- ) {
290
+ #onAssetsChanged = async ({ added, removed }) => {
291
+ if (removed.length > 0) {
292
+ for (const walletAccount of this.#walletAccounts) {
293
+ for (const assetName of removed) {
294
+ delete this.#balances[walletAccount]?.[assetName]
295
+ }
296
+ }
297
+ }
298
+
299
+ const newSupportedAssets = added.filter(this.#isSupportedAsset)
300
+ const missingAssets = newSupportedAssets.filter(
301
+ (name) =>
302
+ !this.#walletAccounts.every((walletAccount) => !!this.#balances?.[walletAccount]?.[name])
303
+ )
304
+
305
+ if (missingAssets.length === 0) {
306
+ if (removed.length > 0) this.#flush()
286
307
  return
287
308
  }
288
309
 
289
- this.#logger.log('recomputing based on new assets list')
290
- this.#recompute()
310
+ this.#logger.log('computing balances for newly available assets', missingAssets)
311
+ await Promise.all(
312
+ this.#walletAccounts.map(async (walletAccount) => {
313
+ const balances = await Promise.all(
314
+ missingAssets.map((assetName) =>
315
+ this.#getBalancesForAssetSource({ assetName, walletAccount })
316
+ )
317
+ )
318
+ if (this.#balances[walletAccount]) {
319
+ Object.assign(this.#balances[walletAccount], ...balances)
320
+ }
321
+ })
322
+ )
323
+
324
+ this.#flush()
291
325
  }
292
326
 
293
- #isSupportedAsset = (assetName) =>
294
- this.#availableAssetNames.includes(assetName) && !this.#getAsset(assetName).isCombined
327
+ #isSupportedAsset = (assetName) => {
328
+ if (!this.#availableAssetNames.includes(assetName)) return false
329
+ const asset = this.#getAsset(assetName)
330
+ return asset && !asset.isCombined
331
+ }
295
332
 
296
333
  #getSupportedAssetNames = () => Object.keys(this.#getAssets()).filter(this.#isSupportedAsset)
297
334
 
@@ -406,7 +443,7 @@ class Balances {
406
443
  }
407
444
  }
408
445
 
409
- if (!isEmpty(changes)) this.#flush({ changes })
446
+ if (Object.keys(changes).length > 0) this.#flush({ changes })
410
447
  }
411
448
 
412
449
  #recompute = async ({ walletAccounts = this.#walletAccounts } = {}) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exodus/balances",
3
- "version": "13.15.1",
3
+ "version": "13.15.3",
4
4
  "description": "Tracks crypto balances across enabled wallet accounts.",
5
5
  "author": "Exodus Movement, Inc.",
6
6
  "license": "MIT",
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@exodus/atoms": "^10.0.0",
37
- "@exodus/basic-utils": "^3.0.1",
37
+ "@exodus/basic-utils": "^3.7.3",
38
38
  "@exodus/core-selectors": "^4.0.0",
39
39
  "@exodus/currency": "^5.0.2",
40
40
  "@exodus/errors": "^3.0.0",
@@ -54,12 +54,12 @@
54
54
  "devDependencies": {
55
55
  "@exodus/assets": "^11.0.0",
56
56
  "@exodus/assets-base": "^10.0.0",
57
- "@exodus/assets-feature": "^8.8.0",
58
- "@exodus/available-assets": "^8.10.1",
57
+ "@exodus/assets-feature": "^9.0.1",
58
+ "@exodus/available-assets": "^8.11.0",
59
59
  "@exodus/bitcoin-meta": "^2.0.0",
60
60
  "@exodus/blockchain-metadata": "^17.1.0",
61
61
  "@exodus/ethereum-meta": "^2.0.0",
62
- "@exodus/fee-data-monitors": "^5.0.4",
62
+ "@exodus/fee-data-monitors": "^5.0.5",
63
63
  "@exodus/storage-memory": "^2.3.0",
64
64
  "@exodus/wallet-accounts": "^20.2.1",
65
65
  "events": "^3.3.0",
@@ -69,5 +69,5 @@
69
69
  "access": "public",
70
70
  "provenance": false
71
71
  },
72
- "gitHead": "e7b1b3f204fccaecff69f8fc89642bfc8e232ec3"
72
+ "gitHead": "3a35709ec4589d6898aa8f1025b510a65cb747f7"
73
73
  }
@@ -5,10 +5,15 @@ import { MY_STATE } from '@exodus/redux-dependency-injection'
5
5
 
6
6
  const { memoize } = lodash // eslint-disable-line @exodus/basic-utils/prefer-basic-utils
7
7
 
8
+ const EMPTY_BALANCES = Object.create(null)
9
+
8
10
  const selectorFactory = (selfSelector) =>
9
11
  memoize(
10
12
  ({ assetName, walletAccount }) =>
11
- createSelector(selfSelector, (balances) => balances[walletAccount]?.data[assetName]),
13
+ createSelector(
14
+ selfSelector,
15
+ (balances) => balances[walletAccount]?.data[assetName] ?? EMPTY_BALANCES
16
+ ),
12
17
  ({ assetName, walletAccount }) => [assetName, walletAccount].join('-')
13
18
  )
14
19