@talismn/balances-react 0.0.0-pr2080-20250710073919 → 0.0.0-pr2091-20250715125148

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