@talismn/balances-react 0.9.12 → 1.0.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.
@@ -1,25 +1,22 @@
1
- import { atom, useSetAtom, useAtom, useAtomValue } from 'jotai';
1
+ import { atom, useSetAtom, useAtomValue } from 'jotai';
2
2
  import { useMemo, useEffect } from 'react';
3
- import { defaultBalanceModules, configureStore, decompress, compress, MiniMetadataUpdater, EvmTokenFetcher, hydrateChaindataAndMiniMetadata, updateCustomMiniMetadata, updateEvmTokens, db, Balances, getBalanceId, balances } from '@talismn/balances';
4
- export { evmErc20TokenId, evmNativeTokenId, subAssetTokenId, subEquilibriumTokenId, subNativeTokenId, subPsp22TokenId, subTokensTokenId } from '@talismn/balances';
5
- import { DEFAULT_COINSAPI_CONFIG, db as db$1, fetchTokenRates, ALL_CURRENCY_IDS } from '@talismn/token-rates';
3
+ import { DEFAULT_COINSAPI_CONFIG, db, fetchTokenRates, ALL_CURRENCY_IDS } from '@talismn/token-rates';
6
4
  import { jsx, Fragment } from 'react/jsx-runtime';
7
- import { firstThenDebounce, isTruthy, isEthereumAddress } from '@talismn/util';
5
+ import { ChaindataProvider } from '@talismn/chaindata-provider';
6
+ export { evmErc20TokenId, evmNativeTokenId, subAssetTokenId, subNativeTokenId, subPsp22TokenId, subTokensTokenId } from '@talismn/chaindata-provider';
7
+ import { BalancesProvider as BalancesProvider$1, Balances } from '@talismn/balances';
8
8
  import { atomEffect } from 'jotai-effect';
9
- import { atomWithObservable } from 'jotai/utils';
10
- import { isEqual as isEqual$1 } from 'lodash';
11
- import { Observable, distinctUntilChanged, map, combineLatest, BehaviorSubject, debounceTime, firstValueFrom } from 'rxjs';
12
- import anylogger from 'anylogger';
9
+ import { keyBy, fromPairs } from 'lodash-es';
13
10
  import { ChainConnector } from '@talismn/chain-connector';
14
11
  import { ChainConnectorEvm } from '@talismn/chain-connector-evm';
15
12
  import { connectionMetaDb } from '@talismn/connection-meta';
16
- import { ChaindataProvider } from '@talismn/chaindata-provider';
17
- import { cryptoWaitReady } from '@polkadot/util-crypto';
13
+ import { firstThenDebounce, isTruthy, isAbortError } from '@talismn/util';
14
+ import { atomWithObservable } from 'jotai/utils';
15
+ import { combineLatest, Observable, map } from 'rxjs';
18
16
  import { liveQuery } from 'dexie';
19
- import isEqual from 'lodash/isEqual';
17
+ import anylogger from 'anylogger';
18
+ import { cryptoWaitReady } from '@polkadot/util-crypto';
20
19
 
21
- const balanceModuleCreatorsAtom = atom(defaultBalanceModules);
22
- const onfinalityApiKeyAtom = atom(undefined);
23
20
  const innerCoinsApiConfigAtom = atom(DEFAULT_COINSAPI_CONFIG);
24
21
  const coinsApiConfigAtom = atom(get => get(innerCoinsApiConfigAtom), (_get, set, options) => set(innerCoinsApiConfigAtom, {
25
22
  apiUrl: options.apiUrl ?? DEFAULT_COINSAPI_CONFIG.apiUrl
@@ -31,137 +28,58 @@ const enabledTokensAtom = atom(undefined);
31
28
  /** Sets the list of addresses for which token balances will be fetched by the balances subscription */
32
29
  const allAddressesAtom = atom([]);
33
30
 
34
- var packageJson = {
35
- name: "@talismn/balances-react"};
36
-
37
- var log = anylogger(packageJson.name);
38
-
39
- /**
40
- // Persistence backend for indexedDB
41
- // Add a new backend by implementing the BalancesPersistBackend interface
42
- // configureStore can be called with a different indexedDB table
43
- */
44
- const {
45
- persistData,
46
- retrieveData
47
- } = configureStore();
48
-
49
- /**
50
- // Persistence backend for localStorage
51
- */
52
- const localStoragePersist = async balances => {
53
- const storedBalances = balances.map(b => {
54
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
55
- const {
56
- status,
57
- ...rest
58
- } = b;
59
- return rest;
60
- });
61
- const deflated = compress(storedBalances);
62
- localStorage.setItem("talismanBalances", deflated.toString());
63
- };
64
- const localStorageRetrieve = async () => {
65
- const deflated = localStorage.getItem("talismanBalances");
66
- if (deflated) {
67
- // deflated will be a long string of numbers separated by commas
68
- const deflatedArray = deflated.split(",").map(n => parseInt(n, 10));
69
- const deflatedBytes = new Uint8Array(deflatedArray.length);
70
- deflatedArray.forEach((n, i) => deflatedBytes[i] = n);
71
- return decompress(deflatedBytes).map(b => ({
72
- ...b,
73
- status: "cache"
74
- }));
75
- }
76
- return [];
77
- };
78
- const localStorageBalancesPersistBackend = {
79
- persist: localStoragePersist,
80
- retrieve: localStorageRetrieve
81
- };
82
-
83
- const cryptoWaitReadyAtom = atom(async () => await cryptoWaitReady());
84
-
85
- const chaindataProviderAtom = atom(get => {
86
- // runs a timer to keep chaindata hydrated
87
- get(chaindataHydrateAtomEffect);
88
- return new ChaindataProvider({
89
- onfinalityApiKey: get(onfinalityApiKeyAtom)
90
- });
31
+ const chaindataProviderAtom = atom(() => {
32
+ return new ChaindataProvider({});
91
33
  });
92
- const miniMetadataHydratedAtom = atom(false);
93
34
 
94
- /** This atomEffect keeps chaindata hydrated (i.e. up to date with the GitHub repo) */
95
- const chaindataHydrateAtomEffect = atomEffect((get, set) => {
35
+ const chainConnectorsAtom = atom(get => {
96
36
  const chaindataProvider = get(chaindataProviderAtom);
97
- const miniMetadataUpdater = get(miniMetadataUpdaterAtom);
98
- const evmTokenFetcher = get(evmTokenFetcherAtom);
99
- const loopMs = 300_000; // 300_000ms = 300s = 5 minutes
100
- const retryTimeout = 5_000; // 5_000ms = 5 seconds
101
-
102
- let timeout = null;
103
- const hydrate = async () => {
104
- try {
105
- await get(cryptoWaitReadyAtom);
106
- await hydrateChaindataAndMiniMetadata(chaindataProvider, miniMetadataUpdater);
107
- await updateCustomMiniMetadata(chaindataProvider, miniMetadataUpdater);
108
- await updateEvmTokens(chaindataProvider, evmTokenFetcher);
109
- set(miniMetadataHydratedAtom, true);
110
- timeout = setTimeout(hydrate, loopMs);
111
- } catch (error) {
112
- log.error(`Failed to hydrate chaindata, retrying in ${Math.round(retryTimeout / 1000)} seconds`, error);
113
- timeout = setTimeout(hydrate, retryTimeout);
114
- }
37
+ const substrate = new ChainConnector(chaindataProvider, connectionMetaDb);
38
+ const evm = new ChainConnectorEvm(chaindataProvider);
39
+ return {
40
+ substrate,
41
+ evm
115
42
  };
116
-
117
- // launch the loop
118
- hydrate();
119
-
120
- // return an unsub function to shut down the loop
121
- return () => timeout && clearTimeout(timeout);
122
43
  });
123
44
 
124
- /** MiniMetadataUpdater is a class used for hydrating chaindata */
125
- const miniMetadataUpdaterAtom = atom(get => {
126
- const chainConnectors = get(chainConnectorsAtom);
127
- const chaindataProvider = get(chaindataProviderAtom);
128
- const balanceModules = get(balanceModulesAtom);
129
- return new MiniMetadataUpdater(chainConnectors, chaindataProvider, balanceModules);
45
+ const balancesProviderAtom = atom(get => {
46
+ return new BalancesProvider$1(get(chaindataProviderAtom), get(chainConnectorsAtom)) // TODO pass storage
47
+ ;
130
48
  });
131
49
 
132
- /** EvmTokenFetcher is a class used for hydrating chaindata */
133
- const evmTokenFetcherAtom = atom(get => {
134
- const chaindataProvider = get(chaindataProviderAtom);
135
- const balanceModules = get(balanceModulesAtom);
136
- return new EvmTokenFetcher(chaindataProvider, balanceModules);
50
+ const chaindataAtom = atomWithObservable(get => {
51
+ return combineLatest({
52
+ networks: get(chaindataProviderAtom).networks$,
53
+ tokens: get(chaindataProviderAtom).tokens$
54
+ }).pipe(firstThenDebounce(1_000));
137
55
  });
138
-
139
- const chainConnectorsAtom = atom(get => {
140
- const onfinalityApiKey = get(onfinalityApiKeyAtom);
141
- const chaindataProvider = get(chaindataProviderAtom);
142
- const substrate = new ChainConnector(chaindataProvider, connectionMetaDb);
143
- const evm = new ChainConnectorEvm(chaindataProvider, {
144
- onfinalityApiKey
145
- });
56
+ const filteredChaindataAtom = atom(async get => {
57
+ const enabledNetworkIds = get(enabledChainsAtom);
58
+ const enabledTokenIds = get(enabledTokensAtom);
59
+ const enableTestnets = get(enableTestnetsAtom);
60
+ const chaindata = await get(chaindataAtom);
61
+ const networks = chaindata.networks.filter(n => (enabledNetworkIds?.includes(n.id) || n.isDefault) && (enableTestnets || !n.isTestnet));
62
+ const networkById = keyBy(networks, n => n.id);
63
+ const tokens = chaindata.tokens.filter(token => (enabledTokenIds?.includes(token.id) || token.isDefault) && networkById[token.networkId]);
146
64
  return {
147
- substrate,
148
- evm
65
+ networks,
66
+ tokens
149
67
  };
150
68
  });
151
-
152
- const balanceModulesAtom = atom(get => {
153
- const balanceModuleCreators = get(balanceModuleCreatorsAtom);
154
- const chainConnectors = get(chainConnectorsAtom);
155
- const chaindataProvider = get(chaindataProviderAtom);
156
- if (!chainConnectors.substrate) return [];
157
- if (!chainConnectors.evm) return [];
158
- if (!chaindataProvider) return [];
159
- return balanceModuleCreators.map(mod => mod({
160
- chainConnectors,
161
- chaindataProvider
162
- }));
69
+ const tokensAtom = atom(async get => {
70
+ const chaindata = await get(filteredChaindataAtom);
71
+ return chaindata.tokens;
72
+ });
73
+ const networksAtom = atom(async get => {
74
+ const chaindata = await get(filteredChaindataAtom);
75
+ return chaindata.networks;
163
76
  });
164
77
 
78
+ var packageJson = {
79
+ name: "@talismn/balances-react"};
80
+
81
+ var log = anylogger(packageJson.name);
82
+
165
83
  /**
166
84
  * Converts a dexie Observable into an rxjs Observable.
167
85
  */
@@ -175,48 +93,6 @@ function dexieToRxjs(o) {
175
93
  });
176
94
  }
177
95
 
178
- const chainsAtom = atom(async get => (await get(chaindataAtom)).chains);
179
- const chainsByIdAtom = atom(async get => (await get(chaindataAtom)).chainsById);
180
- const chainsByGenesisHashAtom = atom(async get => (await get(chaindataAtom)).chainsByGenesisHash);
181
- const evmNetworksAtom = atom(async get => (await get(chaindataAtom)).evmNetworks);
182
- const evmNetworksByIdAtom = atom(async get => (await get(chaindataAtom)).evmNetworksById);
183
- const tokensAtom = atom(async get => (await get(chaindataAtom)).tokens);
184
- const tokensByIdAtom = atom(async get => (await get(chaindataAtom)).tokensById);
185
- const miniMetadatasAtom = atom(async get => (await get(chaindataAtom)).miniMetadatas);
186
- const chaindataAtom = atomWithObservable(get => {
187
- const enableTestnets = get(enableTestnetsAtom);
188
- const filterTestnets = items => enableTestnets ? items : items.filter(({
189
- isTestnet
190
- }) => !isTestnet);
191
- const filterMapTestnets = items => enableTestnets ? items : Object.fromEntries(Object.entries(items).filter(([, {
192
- isTestnet
193
- }]) => !isTestnet));
194
- const filterEnabledTokens = tokens => tokens.filter(token => token.isDefault || "isCustom" in token && token.isCustom);
195
- const filterMapEnabledTokens = tokensById => Object.fromEntries(Object.entries(tokensById).filter(([, token]) => token.isDefault || "isCustom" in token && token.isCustom));
196
- const distinctUntilIsEqual = distinctUntilChanged((a, b) => isEqual(a, b));
197
- const chains = get(chaindataProviderAtom).chainsObservable.pipe(distinctUntilIsEqual, map(filterTestnets), distinctUntilIsEqual);
198
- const chainsById = get(chaindataProviderAtom).chainsByIdObservable.pipe(distinctUntilIsEqual, map(filterMapTestnets), distinctUntilIsEqual);
199
- const chainsByGenesisHash = get(chaindataProviderAtom).chainsByGenesisHashObservable.pipe(distinctUntilIsEqual, map(filterMapTestnets), distinctUntilIsEqual);
200
- const evmNetworks = get(chaindataProviderAtom).evmNetworksObservable.pipe(distinctUntilIsEqual, map(filterTestnets), distinctUntilIsEqual);
201
- const evmNetworksById = get(chaindataProviderAtom).evmNetworksByIdObservable.pipe(distinctUntilIsEqual, map(filterMapTestnets), distinctUntilIsEqual);
202
- const tokens = get(chaindataProviderAtom).tokensObservable.pipe(distinctUntilIsEqual, map(filterTestnets), map(filterEnabledTokens), distinctUntilIsEqual);
203
- const tokensById = get(chaindataProviderAtom).tokensByIdObservable.pipe(distinctUntilIsEqual, map(filterMapTestnets), map(filterMapEnabledTokens), distinctUntilIsEqual);
204
- const miniMetadatasObservable = dexieToRxjs(liveQuery(() => db.miniMetadatas.toArray()));
205
- const miniMetadatas = combineLatest([miniMetadatasObservable.pipe(distinctUntilIsEqual), chainsById]).pipe(map(([miniMetadatas, chainsById]) => miniMetadatas.filter(m => chainsById[m.chainId])), distinctUntilIsEqual);
206
- return combineLatest({
207
- chains,
208
- chainsById,
209
- chainsByGenesisHash,
210
- evmNetworks,
211
- evmNetworksById,
212
- tokens,
213
- tokensById,
214
- miniMetadatas
215
- }).pipe(
216
- // debounce to prevent hammering UI with updates
217
- firstThenDebounce(1_000), distinctUntilIsEqual);
218
- });
219
-
220
96
  const tokenRatesAtom = atom(async get => {
221
97
  // runs a timer to keep tokenRates up to date
222
98
  get(tokenRatesFetcherAtomEffect);
@@ -229,7 +105,7 @@ const tokenRatesDbAtom = atomWithObservable(() => {
229
105
  }) => [tokenId, rates]));
230
106
 
231
107
  // retrieve fetched tokenRates from the db
232
- return dexieToRxjs(liveQuery(() => db$1.tokenRates.toArray())).pipe(map(dbRatesToMap));
108
+ return dexieToRxjs(liveQuery(() => db.tokenRates.toArray())).pipe(map(dbRatesToMap));
233
109
  });
234
110
  const tokenRatesFetcherAtomEffect = atomEffect(get => {
235
111
  // lets us tear down the existing timer when the effect is restarted
@@ -237,9 +113,9 @@ const tokenRatesFetcherAtomEffect = atomEffect(get => {
237
113
 
238
114
  // we have to get these synchronously so that jotai knows to restart our timer when they change
239
115
  const coinsApiConfig = get(coinsApiConfigAtom);
240
- const tokensByIdPromise = get(tokensByIdAtom);
116
+ const tokensPromise = get(tokensAtom);
241
117
  (async () => {
242
- const tokensById = await tokensByIdPromise;
118
+ const tokensById = keyBy(await tokensPromise, "id");
243
119
  const tokenIds = Object.keys(tokensById);
244
120
  const loopMs = 300_000; // 300_000ms = 300s = 5 minutes
245
121
  const retryTimeout = 5_000; // 5_000ms = 5 seconds
@@ -253,15 +129,15 @@ const tokenRatesFetcherAtomEffect = atomEffect(get => {
253
129
  rates
254
130
  }));
255
131
  if (abort.signal.aborted) return; // don't insert into db if aborted
256
- await db$1.transaction("rw", db$1.tokenRates, async () => {
132
+ await db.transaction("rw", db.tokenRates, async () => {
257
133
  // override all tokenRates
258
- await db$1.tokenRates.bulkPut(putTokenRates);
134
+ await db.tokenRates.bulkPut(putTokenRates);
259
135
 
260
136
  // delete tokenRates for tokens which no longer exist
261
137
  const validTokenIds = new Set(tokenIds);
262
- const tokenRatesIds = await db$1.tokenRates.toCollection().primaryKeys();
138
+ const tokenRatesIds = await db.tokenRates.toCollection().primaryKeys();
263
139
  const deleteIds = tokenRatesIds.filter(id => !validTokenIds.has(id));
264
- if (deleteIds.length > 0) await db$1.tokenRates.bulkDelete(deleteIds);
140
+ if (deleteIds.length > 0) await db.tokenRates.bulkDelete(deleteIds);
265
141
  });
266
142
  if (abort.signal.aborted) return; // don't schedule next loop if aborted
267
143
  setTimeout(hydrate, loopMs);
@@ -269,7 +145,7 @@ const tokenRatesFetcherAtomEffect = atomEffect(get => {
269
145
  const retrying = !abort.signal.aborted;
270
146
  const messageParts = ["Failed to fetch tokenRates", retrying && `retrying in ${Math.round(retryTimeout / 1000)} seconds`, !retrying && `giving up (timer no longer needed)`].filter(isTruthy);
271
147
  log.error(messageParts.join(", "), error);
272
- if (abort.signal.aborted) return; // don't schedule retry if aborted
148
+ if (isAbortError(error)) return; // don't schedule retry if aborted
273
149
  setTimeout(hydrate, retryTimeout);
274
150
  }
275
151
  };
@@ -280,218 +156,42 @@ const tokenRatesFetcherAtomEffect = atomEffect(get => {
280
156
  return () => abort.abort("Unsubscribed");
281
157
  });
282
158
 
283
- const allBalancesAtom = atom(async get => {
284
- // set up our subscription to fetch balances from the various blockchains
285
- get(balancesSubscriptionAtomEffect);
286
- const [balances, hydrateData] = await Promise.all([get(balancesObservableAtom), get(balancesHydrateDataAtom)]);
287
- return new Balances(Object.values(balances).filter(balance => !!hydrateData?.tokens?.[balance.tokenId]),
288
- // hydrate balance chains, evmNetworks, tokens and tokenRates
289
- hydrateData);
159
+ const addressesByTokenIdAtom = atom(async get => {
160
+ const [tokens, addresses] = await Promise.all([get(tokensAtom), get(allAddressesAtom)]);
161
+ return fromPairs(tokens.map(token => [token.id, addresses]));
290
162
  });
291
- const balancesObservable = new BehaviorSubject({});
292
- const balancesObservableAtom = atomWithObservable(() => balancesObservable);
293
- const balancesPersistBackendAtom = atom(localStorageBalancesPersistBackend);
294
- const hydrateBalancesObservableAtom = atom(async get => {
295
- const persistBackend = get(balancesPersistBackendAtom);
296
- const balances = await persistBackend.retrieve();
297
- balancesObservable.next(Object.fromEntries(balances.map(b => [getBalanceId(b), {
298
- ...b,
299
- status: "cache"
300
- }])));
163
+ const rawBalancesAtom = atom([]);
164
+ const subscribeBalancesAtom = atomEffect((get, set) => {
165
+ const unsub = (async () => {
166
+ const balancesProvider = get(balancesProviderAtom);
167
+ const addressesByTokenId = await get(addressesByTokenIdAtom);
168
+ const sub = balancesProvider.getBalances$(addressesByTokenId).subscribe(balances => {
169
+ set(rawBalancesAtom, balances.balances);
170
+ });
171
+ return () => {
172
+ return sub.unsubscribe();
173
+ };
174
+ })();
175
+ return () => {
176
+ unsub.then(unsubscribe => unsubscribe());
177
+ };
301
178
  });
302
179
  const balancesHydrateDataAtom = atom(async get => {
303
- const [{
304
- chainsById,
305
- evmNetworksById,
306
- tokensById
307
- }, tokenRates] = await Promise.all([get(chaindataAtom), get(tokenRatesAtom)]);
180
+ const [chaindata, tokenRates] = await Promise.all([get(chaindataAtom), get(tokenRatesAtom)]);
181
+ const networksById = keyBy(chaindata.networks, "id");
182
+ const tokensById = keyBy(chaindata.tokens, "id");
308
183
  return {
309
- chains: chainsById,
310
- evmNetworks: evmNetworksById,
184
+ networks: networksById,
311
185
  tokens: tokensById,
312
186
  tokenRates
313
187
  };
314
188
  });
315
- const balancesSubscriptionAtomEffect = atomEffect(get => {
316
- // lets us tear down the existing subscriptions when the atomEffect is restarted
317
- const abort = new AbortController();
318
-
319
- // we have to specify these synchronously, otherwise jotai won't know
320
- // that it needs to restart our subscriptions when they change
321
- const atomDependencies = Promise.all([get(cryptoWaitReadyAtom), get(balanceModulesAtom), get(miniMetadataHydratedAtom), get(allAddressesAtom), get(chainsAtom), get(chainsByIdAtom), get(evmNetworksAtom), get(evmNetworksByIdAtom), get(tokensAtom), get(tokensByIdAtom), get(miniMetadatasAtom), get(enabledChainsAtom), get(enabledTokensAtom), get(hydrateBalancesObservableAtom)]);
322
- const persistBackend = get(balancesPersistBackendAtom);
323
- const unsubsPromise = (async () => {
324
- const [_cryptoReady, balanceModules, miniMetadataHydrated, allAddresses, chains, chainsById, evmNetworks, evmNetworksById, tokens, tokensById, _miniMetadatas, enabledChainsConfig, enabledTokensConfig] = await atomDependencies;
325
- if (!miniMetadataHydrated) return;
326
- if (abort.signal.aborted) return;
327
-
328
- // persist data every thirty seconds
329
- balancesObservable.pipe(debounceTime(10000)).subscribe(balancesUpdate => {
330
- persistBackend.persist(Object.values(balancesUpdate));
331
- });
332
- const updateBalances = async balancesUpdates => {
333
- if (abort.signal.aborted) return;
334
- const updatesWithIds = new Balances(balancesUpdates);
335
- const existing = balancesObservable.value;
336
-
337
- // update initialising set here - before filtering out zero balances
338
- // while this may include stale balances, the important thing is that the balance is no longer "initialising"
339
- // balancesUpdates.forEach((b) => this.#initialising.delete(getBalanceId(b)))
340
-
341
- const newlyZeroBalances = [];
342
- const changedBalances = Object.fromEntries(updatesWithIds.each.filter(newB => {
343
- const isZero = newB.total.tokens === "0";
344
- // Keep new balances which are not zeros
345
- const existingB = existing[newB.id];
346
- if (!existingB && !isZero) return true;
347
- const hasChanged = !isEqual$1(existingB, newB.toJSON());
348
- // Collect balances now confirmed to be zero separately, so they can be filtered out from the main set
349
- if (existingB && hasChanged && isZero) newlyZeroBalances.push(newB.id);
350
- // Keep changed balances, which are not known zeros
351
- return hasChanged && !isZero;
352
- }).map(b => [b.id, b.toJSON()]));
353
- if (Object.keys(changedBalances).length === 0 && newlyZeroBalances.length === 0) return;
354
- const nonZeroBalances = newlyZeroBalances.length > 0 ? Object.fromEntries(Object.entries(existing).filter(([id]) => !newlyZeroBalances.includes(id))) : existing;
355
- const newBalancesState = {
356
- ...nonZeroBalances,
357
- ...changedBalances
358
- };
359
- if (Object.keys(newBalancesState).length === 0) return;
360
- balancesObservable.next(newBalancesState);
361
- };
362
- const deleteBalances = async balancesFilter => {
363
- if (abort.signal.aborted) return;
364
- const balancesToKeep = Object.fromEntries(new Balances(Object.values(await get(balancesObservableAtom))).each.filter(b => !balancesFilter(b)).map(b => [b.id, b.toJSON()]));
365
- balancesObservable.next(balancesToKeep);
366
- };
367
- const enabledChainIds = enabledChainsConfig?.map(genesisHash => chains.find(chain => chain.genesisHash === genesisHash)?.id);
368
- const enabledChainsFilter = enabledChainIds ? token => token.chain && enabledChainIds?.includes(token.chain.id) : () => true;
369
- const enabledTokensFilter = enabledTokensConfig ? token => enabledTokensConfig.includes(token.id) : () => true;
370
- const enabledTokenIds = tokens.filter(enabledChainsFilter).filter(enabledTokensFilter).map(({
371
- id
372
- }) => id);
373
- if (enabledTokenIds.length < 1 || allAddresses.length < 1) return;
374
- const addressesByTokenByModule = {};
375
- enabledTokenIds.flatMap(tokenId => tokensById[tokenId]).forEach(token => {
376
- // filter out tokens on chains/evmNetworks which have no rpcs
377
- const hasRpcs = token.chain?.id && (chainsById[token.chain.id]?.rpcs?.length ?? 0) > 0 || token.evmNetwork?.id && (evmNetworksById[token.evmNetwork.id]?.rpcs?.length ?? 0) > 0;
378
- if (!hasRpcs) return;
379
- if (!addressesByTokenByModule[token.type]) addressesByTokenByModule[token.type] = {};
380
- addressesByTokenByModule[token.type][token.id] = allAddresses.filter(address => {
381
- // for each address, fetch balances only from compatible chains
382
- return isEthereumAddress(address) ? token.evmNetwork?.id || chainsById[token.chain?.id ?? ""]?.account === "secp256k1" : token.chain?.id && chainsById[token.chain?.id ?? ""]?.account !== "secp256k1";
383
- });
384
- });
385
-
386
- // Delete invalid cached balances
387
- const chainIds = new Set(chains.map(chain => chain.id));
388
- const evmNetworkIds = new Set(evmNetworks.map(evmNetwork => evmNetwork.id));
389
- await deleteBalances(balance => {
390
- // delete cached balances for accounts which don't exist anymore
391
- if (!balance.address || !allAddresses.includes(balance.address)) return true;
392
-
393
- // delete cached balances when chain/evmNetwork doesn't exist
394
- if (balance.chainId === undefined && balance.evmNetworkId === undefined) return true;
395
- if (balance.chainId !== undefined && !chainIds.has(balance.chainId)) return true;
396
- if (balance.evmNetworkId !== undefined && !evmNetworkIds.has(balance.evmNetworkId)) return true;
397
-
398
- // delete cached balance when token doesn't exist / is disabled
399
- if (!enabledTokenIds.includes(balance.tokenId)) return true;
400
-
401
- // delete cached balance when module doesn't exist
402
- if (!balanceModules.find(module => module.type === balance.source)) return true;
403
-
404
- // delete cached balance for accounts on incompatible chains
405
- // (substrate accounts shouldn't have evm balances)
406
- // (evm accounts shouldn't have substrate balances (unless the chain uses secp256k1 accounts))
407
- const chain = balance.chainId && chains.find(({
408
- id
409
- }) => id === balance.chainId) || null;
410
- const hasChain = balance.chainId && chainIds.has(balance.chainId);
411
- const hasEvmNetwork = balance.evmNetworkId && evmNetworkIds.has(balance.evmNetworkId);
412
- const chainUsesSecp256k1Accounts = chain?.account === "secp256k1";
413
- if (!isEthereumAddress(balance.address)) {
414
- if (!hasChain) return true;
415
- if (chainUsesSecp256k1Accounts) return true;
416
- }
417
- if (isEthereumAddress(balance.address)) {
418
- if (!hasEvmNetwork && !chainUsesSecp256k1Accounts) return true;
419
- }
420
-
421
- // keep balance
422
- return false;
423
- });
424
- if (abort.signal.aborted) return;
425
-
426
- // after 30 seconds, change the status of all balances still initializing to stale
427
- setTimeout(() => {
428
- if (abort.signal.aborted) return;
429
- const staleObservable = balancesObservable.pipe(map(val => Object.values(val).filter(({
430
- status
431
- }) => status === "cache").map(balance => ({
432
- ...balance,
433
- status: "stale"
434
- }))));
435
- firstValueFrom(staleObservable).then(v => {
436
- if (v.length) updateBalances(v);
437
- });
438
- }, 30_000);
439
- return balanceModules.map(balanceModule => {
440
- const unsub = balances(balanceModule, addressesByTokenByModule[balanceModule.type] ?? {}, (error, balances) => {
441
- // log errors
442
- if (error) {
443
- if (error?.type === "STALE_RPC_ERROR" || error?.type === "WEBSOCKET_ALLOCATION_EXHAUSTED_ERROR") {
444
- const addressesByModuleToken = addressesByTokenByModule[balanceModule.type] ?? {};
445
- const staleObservable = balancesObservable.pipe(map(val => Object.values(val).filter(balance => {
446
- const {
447
- tokenId,
448
- address,
449
- source
450
- } = balance;
451
- const chainComparison = error.chainId ? "chainId" in balance && error.chainId === balance.chainId : error.evmNetworkId ? "evmNetworkId" in balance && error.evmNetworkId === balance.evmNetworkId : true;
452
- return chainComparison && addressesByModuleToken[tokenId]?.includes(address) && source === balanceModule.type;
453
- }).map(balance => ({
454
- ...balance,
455
- status: "stale"
456
- }))));
457
- firstValueFrom(staleObservable).then(v => {
458
- if (v.length) updateBalances(v);
459
- });
460
- }
461
- return log.error(`Failed to fetch ${balanceModule.type} balances`, error);
462
- }
463
-
464
- // ignore empty balance responses
465
- if (!balances) return;
466
- // ignore balances from old subscriptions which are still in the process of unsubscribing
467
- if (abort.signal.aborted) return;
468
-
469
- // good balances
470
- if (balances) {
471
- if ("status" in balances) {
472
- // For modules using the new SubscriptionResultWithStatus pattern
473
- //TODO fix initialisin
474
- // if (result.status === "initialising") this.#initialising.add(balanceModule.type)
475
- // else this.#initialising.delete(balanceModule.type)
476
- updateBalances(balances.data);
477
- } else {
478
- // add good ones to initialisedBalances
479
- updateBalances(Object.values(balances.toJSON()));
480
- }
481
- }
482
- });
483
- return () => unsub.then(unsubscribe => unsubscribe());
484
- });
485
- })();
486
-
487
- // close the existing subscriptions when our effect unmounts
488
- // (wait 2 seconds before actually unsubscribing, to allow the websocket to be reused in that time)
489
- const unsubscribe = () => unsubsPromise.then(unsubs => {
490
- persistBackend.persist(Object.values(balancesObservable.value));
491
- unsubs?.forEach(unsub => unsub());
492
- });
493
- abort.signal.addEventListener("abort", () => setTimeout(unsubscribe, 2_000));
494
- return () => abort.abort("Unsubscribed");
189
+ const balancesAtom = atom(async get => {
190
+ // subscribe to balancesProvider getBalance with addressesByTokenIdAtom as param
191
+ get(subscribeBalancesAtom);
192
+ const hydrate = await get(balancesHydrateDataAtom);
193
+ const rawBalances = get(rawBalancesAtom);
194
+ return new Balances(rawBalances, hydrate);
495
195
  });
496
196
 
497
197
  const useSetBalancesAddresses = addresses => {
@@ -508,10 +208,8 @@ const useSetBalancesAddresses = addresses => {
508
208
  * @returns a Balances object containing the current balances state.
509
209
  */
510
210
 
511
- const useBalances = persistBackend => {
512
- const [, setPersistBackend] = useAtom(balancesPersistBackendAtom);
513
- if (persistBackend) setPersistBackend(persistBackend);
514
- return useAtomValue(allBalancesAtom);
211
+ const useBalances = () => {
212
+ return useAtomValue(balancesAtom);
515
213
  };
516
214
 
517
215
  // TODO: Extract to shared definition between extension and @talismn/balances-react
@@ -542,41 +240,47 @@ const useBalancesStatus = balances => useMemo(() => {
542
240
  status: "live"
543
241
  };
544
242
  }, [balances]);
545
- const getStaleChains = balances => [...new Set(balances.sorted.filter(b => b.status === "stale").map(b => b.chain?.name ?? b.chainId ?? "Unknown"))];
243
+ const getStaleChains = balances => [...new Set(balances.each.filter(b => b.status === "stale").map(b => b.network?.name ?? b.networkId ?? "Unknown"))];
546
244
 
547
245
  const useChainConnectors = () => useAtomValue(chainConnectorsAtom);
548
246
 
549
247
  const useChaindataProvider = () => useAtomValue(chaindataProviderAtom);
550
248
  const useChaindata = () => useAtomValue(chaindataAtom);
551
- const useChains = () => useAtomValue(chainsByIdAtom);
552
- const useChainsByGenesisHash = () => useAtomValue(chainsByGenesisHashAtom);
553
- const useEvmNetworks = () => useAtomValue(evmNetworksByIdAtom);
554
- const useTokens = () => useAtomValue(tokensByIdAtom);
555
- const useMiniMetadatas = () => useAtomValue(miniMetadatasAtom);
556
- const useChain = chainId => useChains()[chainId ?? ""] ?? undefined;
557
- const useEvmNetwork = evmNetworkId => useEvmNetworks()[evmNetworkId ?? ""] ?? undefined;
558
- const useToken = tokenId => useTokens()[tokenId ?? ""] ?? undefined;
249
+ const useNetworks = () => useChaindata().networks;
250
+ const useNetworksById = () => {
251
+ const {
252
+ networks
253
+ } = useChaindata();
254
+ return useMemo(() => keyBy(networks, n => n.id), [networks]);
255
+ };
256
+ const useNetwork = networkId => {
257
+ const networksById = useNetworksById();
258
+ return networksById[networkId ?? ""] ?? null;
259
+ };
260
+ const useTokens = () => useChaindata().tokens;
261
+ const useTokensById = () => {
262
+ const {
263
+ tokens
264
+ } = useChaindata();
265
+ return useMemo(() => keyBy(tokens, t => t.id), [tokens]);
266
+ };
267
+ const useToken = tokenId => {
268
+ const tokensById = useTokensById();
269
+ return tokensById[tokenId ?? ""] ?? null;
270
+ };
559
271
 
560
272
  const useTokenRates = () => useAtomValue(tokenRatesAtom);
561
273
  const useTokenRate = tokenId => useTokenRates()[tokenId ?? ""] ?? undefined;
562
274
 
275
+ const cryptoWaitReadyAtom = atom(async () => await cryptoWaitReady());
276
+
563
277
  const BalancesProvider = ({
564
- balanceModules,
565
- onfinalityApiKey,
566
278
  coinsApiUrl,
567
279
  withTestnets,
568
280
  enabledChains,
569
281
  enabledTokens,
570
282
  children
571
283
  }) => {
572
- const setBalanceModules = useSetAtom(balanceModuleCreatorsAtom);
573
- useEffect(() => {
574
- if (balanceModules !== undefined) setBalanceModules(balanceModules);
575
- }, [balanceModules, setBalanceModules]);
576
- const setOnfinalityApiKey = useSetAtom(onfinalityApiKeyAtom);
577
- useEffect(() => {
578
- setOnfinalityApiKey(onfinalityApiKey);
579
- }, [onfinalityApiKey, setOnfinalityApiKey]);
580
284
  const setCoinsApiConfig = useSetAtom(coinsApiConfigAtom);
581
285
  useEffect(() => {
582
286
  setCoinsApiConfig({
@@ -600,4 +304,4 @@ const BalancesProvider = ({
600
304
  });
601
305
  };
602
306
 
603
- export { BalancesProvider, allAddressesAtom, allBalancesAtom, balanceModuleCreatorsAtom, balanceModulesAtom, balancesPersistBackendAtom, chainConnectorsAtom, chaindataAtom, chaindataProviderAtom, chainsAtom, chainsByGenesisHashAtom, chainsByIdAtom, coinsApiConfigAtom, cryptoWaitReadyAtom, enableTestnetsAtom, enabledChainsAtom, enabledTokensAtom, evmNetworksAtom, evmNetworksByIdAtom, getStaleChains, miniMetadataHydratedAtom, miniMetadatasAtom, onfinalityApiKeyAtom, tokenRatesAtom, tokensAtom, tokensByIdAtom, useBalances, useBalancesStatus, useChain, useChainConnectors, useChaindata, useChaindataProvider, useChains, useChainsByGenesisHash, useEvmNetwork, useEvmNetworks, useMiniMetadatas, useSetBalancesAddresses, useToken, useTokenRate, useTokenRates, useTokens };
307
+ export { BalancesProvider, allAddressesAtom, balancesAtom, chainConnectorsAtom, chaindataAtom, chaindataProviderAtom, coinsApiConfigAtom, cryptoWaitReadyAtom, enableTestnetsAtom, enabledChainsAtom, enabledTokensAtom, getStaleChains, networksAtom, tokenRatesAtom, tokensAtom, useBalances, useBalancesStatus, useChainConnectors, useChaindata, useChaindataProvider, useNetwork, useNetworks, useNetworksById, useSetBalancesAddresses, useToken, useTokenRate, useTokenRates, useTokens, useTokensById };