@talismn/balances-react 0.0.0-pr596-20230306005809 → 0.0.0-pr599-20230306082809

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.
@@ -9,6 +9,7 @@ var tokenRates = require('@talismn/token-rates');
9
9
  var dexieReactHooks = require('dexie-react-hooks');
10
10
  var reactUse = require('react-use');
11
11
  var chaindataProviderExtension = require('@talismn/chaindata-provider-extension');
12
+ var md5 = require('blueimp-md5');
12
13
  var anylogger = require('anylogger');
13
14
  var rxjs = require('rxjs');
14
15
  var chainConnector = require('@talismn/chain-connector');
@@ -16,6 +17,7 @@ var chainConnectorEvm = require('@talismn/chain-connector-evm');
16
17
 
17
18
  function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
18
19
 
20
+ var md5__default = /*#__PURE__*/_interopDefault(md5);
19
21
  var anylogger__default = /*#__PURE__*/_interopDefault(anylogger);
20
22
 
21
23
  const provideContext = useProviderContext => {
@@ -52,13 +54,15 @@ const useBalanceModulesProvider = ({
52
54
  const [BalanceModulesProvider, useBalanceModules] = provideContext(useBalanceModulesProvider);
53
55
 
54
56
  function useChaindataProvider(options = {}) {
55
- const [chaindata, setChaindata] = react.useState();
57
+ const [onfinalityApiKey, setOnfinalityApiKey] = react.useState(options.onfinalityApiKey);
58
+
59
+ // make sure we recreate provider only when the onfinalityApiKey changes
56
60
  react.useEffect(() => {
57
- setChaindata(new chaindataProviderExtension.ChaindataProviderExtension({
58
- onfinalityApiKey: options.onfinalityApiKey
59
- }));
60
- }, [options.onfinalityApiKey]);
61
- return chaindata;
61
+ if (options.onfinalityApiKey !== onfinalityApiKey) setOnfinalityApiKey(options.onfinalityApiKey);
62
+ }, [options.onfinalityApiKey, onfinalityApiKey]);
63
+ return react.useMemo(() => new chaindataProviderExtension.ChaindataProviderExtension({
64
+ onfinalityApiKey
65
+ }), [onfinalityApiKey]);
62
66
  }
63
67
  const [ChaindataProvider, useChaindata] = provideContext(useChaindataProvider);
64
68
 
@@ -134,9 +138,7 @@ const consolidateDbCache = (chainsMap, evmNetworksMap, tokensMap, tokenRates, al
134
138
  balances
135
139
  };
136
140
  };
137
- const useDbCacheProvider = ({
138
- useTestnets = false
139
- }) => {
141
+ const useDbCacheProvider = () => {
140
142
  const chaindataProvider = useChaindata();
141
143
  const chainList = dexieReactHooks.useLiveQuery(() => chaindataProvider === null || chaindataProvider === void 0 ? void 0 : chaindataProvider.chains(), [chaindataProvider]);
142
144
  const evmNetworkList = dexieReactHooks.useLiveQuery(() => chaindataProvider === null || chaindataProvider === void 0 ? void 0 : chaindataProvider.evmNetworks(), [chaindataProvider]);
@@ -148,7 +150,7 @@ const useDbCacheProvider = ({
148
150
  // debounce every 500ms to prevent hammering UI with updates
149
151
  reactUse.useDebounce(() => {
150
152
  setDbData(consolidateDbCache(chainList, evmNetworkList, tokenList, tokenRates$1, rawBalances));
151
- }, 500, [chainList, evmNetworkList, tokenList, rawBalances, tokenRates$1, useTestnets]);
153
+ }, 500, [chainList, evmNetworkList, tokenList, rawBalances, tokenRates$1]);
152
154
  const refInitialized = react.useRef(false);
153
155
 
154
156
  // force an update as soon as all datasources are fetched, so UI can display data ASAP
@@ -157,14 +159,14 @@ const useDbCacheProvider = ({
157
159
  setDbData(consolidateDbCache(chainList, evmNetworkList, tokenList, tokenRates$1, rawBalances));
158
160
  refInitialized.current = true;
159
161
  }
160
- }, [chainList, evmNetworkList, rawBalances, tokenList, tokenRates$1, useTestnets]);
162
+ }, [chainList, evmNetworkList, rawBalances, tokenList, tokenRates$1]);
161
163
  return dbData;
162
164
  };
163
165
  const [DbCacheProvider, useDbCache] = provideContext(useDbCacheProvider);
164
166
 
165
167
  var packageJson = {
166
168
  name: "@talismn/balances-react",
167
- version: "0.0.0-pr596-20230306005809",
169
+ version: "0.0.0-pr599-20230306082809",
168
170
  author: "Talisman",
169
171
  homepage: "https://talisman.xyz",
170
172
  license: "UNLICENSED",
@@ -197,6 +199,7 @@ var packageJson = {
197
199
  "@talismn/chaindata-provider-extension": "workspace:^",
198
200
  "@talismn/token-rates": "workspace:^",
199
201
  anylogger: "^1.0.11",
202
+ "blueimp-md5": "2.19.0",
200
203
  dexie: "^3.2.3",
201
204
  "dexie-react-hooks": "^1.1.1",
202
205
  "react-use": "^17.4.0",
@@ -227,6 +230,95 @@ var packageJson = {
227
230
 
228
231
  var log = anylogger__default["default"](packageJson.name);
229
232
 
233
+ // global data store containing all subscriptions
234
+ const subscriptions = {};
235
+
236
+ /**
237
+ * This hook ensures a subscription is created only once, and unsubscribe automatically as soon as there is no consumer to the hook
238
+ * @param key key that is unique to the subscription's parameters
239
+ * @param subscribe // subscribe function that will be shared by all consumers of the key
240
+ */
241
+ const useSharedSubscription = (key, subscribe) => {
242
+ // create the rxJS subject if it doesn't exist
243
+ if (!subscriptions[key]) subscriptions[key] = {
244
+ subject: new rxjs.Subject()
245
+ };
246
+ react.useEffect(() => {
247
+ // subscribe to subject.
248
+ // it won't change but we need to count subscribers, to unsubscribe main subscription when no more observers
249
+ const s = subscriptions[key].subject.subscribe();
250
+ return () => {
251
+ // unsubscribe from our local observable updates to prevent memory leaks
252
+ s.unsubscribe();
253
+ const {
254
+ subject,
255
+ unsubscribe
256
+ } = subscriptions[key];
257
+ if (!subject.observed && unsubscribe) {
258
+ log.debug(`[useSharedSubscription] unsubscribing ${key}`);
259
+
260
+ // unsubscribe from backend updates to prevent unnecessary network connections
261
+ unsubscribe();
262
+ delete subscriptions[key].unsubscribe;
263
+ }
264
+ };
265
+ }, [key]);
266
+
267
+ // Initialize subscription
268
+ react.useEffect(() => {
269
+ const {
270
+ unsubscribe
271
+ } = subscriptions[key];
272
+ // launch the subscription if it's a new key
273
+ if (!unsubscribe) {
274
+ const cb = subscribe();
275
+ log.debug(`[useSharedSubscription] subscribing ${key}`);
276
+ if (cb) subscriptions[key].unsubscribe = cb;
277
+ // this error should only happen when developping a new hook, let it bubble up
278
+ else throw new Error(`${key} subscribe did not return an unsubscribe callback`);
279
+ }
280
+ }, [key, subscribe]);
281
+ };
282
+
283
+ function useChainConnectorsProvider(options) {
284
+ const [onfinalityApiKey, setOnfinalityApiKey] = react.useState(options.onfinalityApiKey);
285
+
286
+ // make sure we recreate provider only when the onfinalityApiKey changes
287
+ react.useEffect(() => {
288
+ if (options.onfinalityApiKey !== onfinalityApiKey) setOnfinalityApiKey(options.onfinalityApiKey);
289
+ }, [options.onfinalityApiKey, onfinalityApiKey]);
290
+
291
+ // chaindata dependency
292
+ const chaindata = useChaindata();
293
+
294
+ // substrate connector
295
+ const substrate = react.useMemo(() => new chainConnector.ChainConnector(chaindata), [chaindata]);
296
+
297
+ // evm connector
298
+ const evm = react.useMemo(() => new chainConnectorEvm.ChainConnectorEvm(chaindata, {
299
+ onfinalityApiKey
300
+ }), [chaindata, onfinalityApiKey]);
301
+ return react.useMemo(() => ({
302
+ substrate,
303
+ evm
304
+ }), [substrate, evm]);
305
+ }
306
+ const [ChainConnectorsProvider, useChainConnectors] = provideContext(useChainConnectorsProvider);
307
+
308
+ function useTokens(withTestnets) {
309
+ // keep db data up to date
310
+ useDbCacheSubscription("tokens");
311
+ const {
312
+ tokensWithTestnetsMap,
313
+ tokensWithoutTestnetsMap
314
+ } = useDbCache();
315
+ return withTestnets ? tokensWithTestnetsMap : tokensWithoutTestnetsMap;
316
+ }
317
+ function useToken(tokenId, withTestnets) {
318
+ const tokens = useTokens(withTestnets);
319
+ return tokenId ? tokens[tokenId] : undefined;
320
+ }
321
+
230
322
  /**
231
323
  * Creates a subscription function that can be used to subscribe to a multicast observable created from an upstream source.
232
324
  *
@@ -268,193 +360,193 @@ const createMulticastSubscription = upstream => {
268
360
  return subscribe;
269
361
  };
270
362
 
271
- function useChainConnectorsProvider(options) {
272
- // chaindata dependency
273
- const chaindata = useChaindata();
274
-
275
- // substrate connector
276
- const [substrate, setSubstrate] = react.useState();
277
- react.useEffect(() => {
278
- if (!chaindata) return;
279
- setSubstrate(new chainConnector.ChainConnector(chaindata));
280
- }, [chaindata]);
281
-
282
- // evm connector
283
- const [evm, setEvm] = react.useState();
284
- react.useEffect(() => {
285
- if (!chaindata) return;
286
- setEvm(new chainConnectorEvm.ChainConnectorEvm(chaindata, {
287
- onfinalityApiKey: options.onfinalityApiKey
288
- }));
289
- }, [chaindata, options.onfinalityApiKey]);
290
- return react.useMemo(() => ({
291
- substrate,
292
- evm
293
- }), [substrate, evm]);
294
- }
295
- const [ChainConnectorsProvider, useChainConnectors] = provideContext(useChainConnectorsProvider);
296
-
297
- const useSubscriptionsProvider = () => [useSubscribeChaindataHydrate("chains"), useSubscribeChaindataHydrate("evmNetworks"), useSubscribeChaindataHydrate("tokens"), useSubscribeTokenRates(), useSubscribeBalances()];
298
- const [SubscriptionsProvider, useSubscriptions] = provideContext(useSubscriptionsProvider);
363
+ const useWithTestnetsProvider = ({
364
+ withTestnets
365
+ }) => {
366
+ return {
367
+ withTestnets
368
+ };
369
+ };
370
+ const [WithTestnetsProvider, useWithTestnets] = provideContext(useWithTestnetsProvider);
299
371
 
300
372
  /**
301
373
  * This hook is responsible for fetching the data used for balances and inserting it into the db.
302
374
  */
303
375
  const useDbCacheSubscription = subscribeTo => {
304
- const [subscribeHydrateChains, subscribeHydrateEvmNetworks, subscribeHydrateTokens, subscribeTokenRates, subscribeBalances] = useSubscriptions();
305
- react.useEffect(() => {
376
+ const provider = useChaindata();
377
+
378
+ // can't handle balances & tokenRates here as they have other dependencies, it would trigger to many subscriptions
379
+ const subscribe = react.useCallback(() => {
306
380
  switch (subscribeTo) {
307
381
  case "chains":
308
- return subscribeHydrateChains();
382
+ return subscribeChainDataHydrate(provider, "chains");
309
383
  case "evmNetworks":
310
- return subscribeHydrateEvmNetworks();
384
+ return subscribeChainDataHydrate(provider, "evmNetworks");
311
385
  case "tokens":
312
- return subscribeHydrateTokens();
313
- case "tokenRates":
314
- return subscribeTokenRates();
315
- case "balances":
316
- return subscribeBalances();
386
+ return subscribeChainDataHydrate(provider, "tokens");
317
387
  }
318
- }, [subscribeTo, subscribeHydrateChains, subscribeHydrateEvmNetworks, subscribeHydrateTokens, subscribeTokenRates, subscribeBalances]);
388
+ }, [provider, subscribeTo]);
389
+ useSharedSubscription(subscribeTo, subscribe);
319
390
  };
320
- function useSubscribeChaindataHydrate(type) {
321
- const chaindata =
322
- // cheeky hack to give us access to the hydrate methods
323
- useChaindata();
324
- const createSubscription = react.useCallback(() => {
325
- if (!chaindata) return;
326
- let active = true;
327
- const interval = 300_000; // 300_000ms = 300s = 5 minutes
328
-
329
- const hydrate = async () => {
330
- if (!active) return;
331
- try {
332
- if (type === "chains") await chaindata.hydrateChains();
333
- if (type === "evmNetworks") await chaindata.hydrateEvmNetworks();
334
- if (type === "tokens") await chaindata.hydrateTokens();
335
- setTimeout(hydrate, interval);
336
- } catch (error) {
337
- const retryTimeout = 5_000; // 5_000ms = 5 seconds
338
- log.error(`Failed to fetch chaindata, retrying in ${Math.round(retryTimeout / 1000)} seconds`, error);
339
- setTimeout(hydrate, retryTimeout);
340
- }
341
- };
342
- hydrate();
343
- return () => {
344
- active = false;
345
- };
346
- }, [chaindata, type]);
347
- const subscribe = useMulticastSubscription(createSubscription);
348
- return subscribe;
349
- }
350
- function useSubscribeTokenRates() {
351
- const chaindataProvider = useChaindata();
352
- const tokens = dexieReactHooks.useLiveQuery(() => chaindataProvider === null || chaindataProvider === void 0 ? void 0 : chaindataProvider.tokens(), [chaindataProvider]);
353
- const generationRef = react.useRef(0);
354
- const createSubscription = react.useCallback(() => {
355
- if (!chaindataProvider) return;
356
- if (!tokens) return;
357
- if (Object.keys(tokens).length < 1) return;
358
-
359
- // when we make a new request, we want to ignore any old requests which haven't yet completed
360
- // otherwise we risk replacing the most recent data with older data
361
- const generation = (generationRef.current + 1) % Number.MAX_SAFE_INTEGER;
362
- generationRef.current = generation;
363
- let active = true;
364
- const REFRESH_INTERVAL = 300_000; // 300_000ms = 5 minutes
365
- const RETRY_INTERVAL = 5_000; // 5_000ms = 5 seconds
366
-
367
- const hydrate = async () => {
368
- if (!active) return;
369
- if (generationRef.current !== generation) return;
370
- try {
371
- const tokenRates$1 = await tokenRates.fetchTokenRates(tokens);
372
- if (!active) return;
373
- if (generationRef.current !== generation) return;
374
- const putTokenRates = Object.entries(tokenRates$1).map(([tokenId, rates]) => ({
375
- tokenId,
376
- rates
377
- }));
378
- tokenRates.db.transaction("rw", tokenRates.db.tokenRates, async () => await tokenRates.db.tokenRates.bulkPut(putTokenRates));
379
- setTimeout(hydrate, REFRESH_INTERVAL);
380
- } catch (error) {
381
- log.error(`Failed to fetch tokenRates, retrying in ${Math.round(RETRY_INTERVAL / 1000)} seconds`, error);
382
- setTimeout(hydrate, RETRY_INTERVAL);
383
- }
384
- };
385
- hydrate();
386
- return () => {
387
- active = false;
388
- };
389
- }, [chaindataProvider, tokens]);
390
- const subscribe = useMulticastSubscription(createSubscription);
391
- return subscribe;
391
+
392
+ /**
393
+ * This hook is responsible for fetching the data used for token rates and inserting it into the db.
394
+ */
395
+ function useDbCacheTokenRatesSubscription() {
396
+ const {
397
+ withTestnets
398
+ } = useWithTestnets();
399
+ const tokens = useTokens(withTestnets);
400
+ const subscriptionKey = react.useMemo(
401
+ // not super sexy but we need key to change based on this stuff
402
+ () => {
403
+ const key = Object.values(tokens ?? {}).map(({
404
+ id
405
+ }) => id).sort().join();
406
+ return `tokenRates-${md5__default["default"](key)}`;
407
+ }, [tokens]);
408
+ const subscription = react.useCallback(() => {
409
+ if (!Object.values(tokens ?? {}).length) return () => {};
410
+ return subscribeTokenRates(tokens);
411
+ }, [tokens]);
412
+ useSharedSubscription(subscriptionKey, subscription);
392
413
  }
393
- function useSubscribeBalances() {
414
+
415
+ /**
416
+ * This hook is responsible for fetching the data used for balances and inserting it into the db.
417
+ */
418
+ function useDbCacheBalancesSubscription() {
419
+ const {
420
+ withTestnets
421
+ } = useWithTestnets();
394
422
  const balanceModules = useBalanceModules();
395
423
  const chaindataProvider = useChaindata();
396
424
  const chainConnectors = useChainConnectors();
397
425
  const [allAddresses] = useAllAddresses();
398
- const tokens = dexieReactHooks.useLiveQuery(() => chaindataProvider === null || chaindataProvider === void 0 ? void 0 : chaindataProvider.tokens(), [chaindataProvider]);
399
- const tokenIds = react.useMemo(() => Object.values(tokens ?? {}).map(({
426
+ const tokens = useTokens(withTestnets);
427
+ const subscriptionKey = react.useMemo(
428
+ // not super sexy but we need key to change based on this stuff
429
+ () => {
430
+ const key = allAddresses.sort().join().concat(...Object.values(tokens ?? {}).map(({
431
+ id
432
+ }) => id).sort()).concat(`evm:${!!chainConnectors.evm}`, `sub:${!!chainConnectors.substrate}`, ...balanceModules.map(m => m.type).sort(), `cd:${!!chaindataProvider}`);
433
+ return `balances-${md5__default["default"](key)}`;
434
+ }, [allAddresses, balanceModules, chainConnectors, chaindataProvider, tokens]);
435
+ const subscription = react.useCallback(() => {
436
+ if (!Object.values(tokens ?? {}).length || !allAddresses.length) return () => {};
437
+ return subscribeBalances(tokens ?? {}, allAddresses, chainConnectors, chaindataProvider, balanceModules);
438
+ }, [allAddresses, balanceModules, chainConnectors, chaindataProvider, tokens]);
439
+ useSharedSubscription(subscriptionKey, subscription);
440
+ }
441
+ const subscribeChainDataHydrate = (provider, type) => {
442
+ const chaindata = provider;
443
+ const delay = 300_000; // 300_000ms = 300s = 5 minutes
444
+
445
+ let timeout = null;
446
+ const hydrate = async () => {
447
+ try {
448
+ if (type === "chains") await chaindata.hydrateChains();
449
+ if (type === "evmNetworks") await chaindata.hydrateEvmNetworks();
450
+ if (type === "tokens") await chaindata.hydrateTokens();
451
+ timeout = setTimeout(hydrate, delay);
452
+ } catch (error) {
453
+ const retryTimeout = 5_000; // 5_000ms = 5 seconds
454
+ log.error(`Failed to fetch chaindata, retrying in ${Math.round(retryTimeout / 1000)} seconds`, error);
455
+ timeout = setTimeout(hydrate, retryTimeout);
456
+ }
457
+ };
458
+
459
+ // launch the loop
460
+ hydrate();
461
+ return () => {
462
+ if (timeout) clearTimeout(timeout);
463
+ };
464
+ };
465
+ const subscribeTokenRates = tokens => {
466
+ const REFRESH_INTERVAL = 300_000; // 6 minutes
467
+ const RETRY_INTERVAL = 5_000; // 5 sec
468
+
469
+ let timeout = null;
470
+ const refreshTokenRates = async () => {
471
+ try {
472
+ if (timeout) clearTimeout(timeout);
473
+ const tokenRates$1 = await tokenRates.fetchTokenRates(tokens);
474
+ const putTokenRates = Object.entries(tokenRates$1).map(([tokenId, rates]) => ({
475
+ tokenId,
476
+ rates
477
+ }));
478
+ tokenRates.db.transaction("rw", tokenRates.db.tokenRates, async () => await tokenRates.db.tokenRates.bulkPut(putTokenRates));
479
+ timeout = setTimeout(() => {
480
+ refreshTokenRates();
481
+ }, REFRESH_INTERVAL);
482
+ } catch (error) {
483
+ log.error(`Failed to fetch tokenRates, retrying in ${Math.round(RETRY_INTERVAL / 1000)} seconds`, error);
484
+ setTimeout(async () => {
485
+ refreshTokenRates();
486
+ }, RETRY_INTERVAL);
487
+ }
488
+ };
489
+
490
+ // launch the loop
491
+ refreshTokenRates();
492
+ return () => {
493
+ if (timeout) clearTimeout(timeout);
494
+ };
495
+ };
496
+ const subscribeBalances = (tokens, addresses, chainConnectors, provider, balanceModules) => {
497
+ const tokenIds = Object.values(tokens).map(({
400
498
  id
401
- }) => id), [tokens]);
402
- const addressesByToken = react.useMemo(() => Object.fromEntries(tokenIds.map(tokenId => [tokenId, allAddresses])), [allAddresses, tokenIds]);
403
- const generationRef = react.useRef(0);
404
- const createSubscription = react.useCallback(() => {
405
- if (!chainConnectors.substrate) return;
406
- if (!chainConnectors.evm) return;
407
- if (!chaindataProvider) return;
408
- const generation = (generationRef.current + 1) % Number.MAX_SAFE_INTEGER;
409
- generationRef.current = generation;
410
- const unsubs = balanceModules.map(balanceModule => {
411
- // filter out tokens to only include those which this module knows how to fetch balances for
412
- const moduleTokenIds = Object.values(tokens ?? {}).filter(({
413
- type
414
- }) => type === balanceModule.type).map(({
415
- id
416
- }) => id);
417
- const addressesByModuleToken = Object.fromEntries(Object.entries(addressesByToken).filter(([tokenId]) => moduleTokenIds.includes(tokenId)));
418
- const subscribe = createMulticastSubscription(next => {
419
- const unsub = balances.balances(balanceModule, chainConnectors, chaindataProvider, addressesByModuleToken, (error, balances) => {
420
- // log errors
421
- if (error) return log.error(`Failed to fetch ${balanceModule.type} balances`, error);
422
- // ignore empty balance responses
423
- if (!balances) return;
424
- // ignore balances from old subscriptions which are still in the process of unsubscribing
425
- if (generationRef.current !== generation) return;
426
- next(balances);
427
- });
428
- return () => {
429
- // unsubscribe from upstream
430
- unsub.then(unsubscribe => unsubscribe());
431
-
432
- // set this subscription's balances in the store to status: cache
433
- balances.db.transaction("rw", balances.db.balances, async () => await balances.db.balances.filter(balance => {
434
- if (balance.source !== balanceModule.type) return false;
435
- if (!Object.keys(addressesByModuleToken).includes(balance.tokenId)) return false;
436
- if (!addressesByModuleToken[balance.tokenId].includes(balance.address)) return false;
437
- return true;
438
- }).modify({
439
- status: "cache"
440
- }));
441
- };
442
- });
443
- const unsubscribe = subscribe(balances$1 => {
444
- const putBalances = Object.entries(balances$1.toJSON()).map(([id, balance]) => ({
445
- id,
446
- ...balance
447
- }));
448
- balances.db.transaction("rw", balances.db.balances, async () => await balances.db.balances.bulkPut(putBalances));
449
- });
450
- return unsubscribe;
499
+ }) => id);
500
+ const addressesByToken = Object.fromEntries(tokenIds.map(tokenId => [tokenId, addresses]));
501
+ const updateDb = balances$1 => {
502
+ const putBalances = Object.entries(balances$1.toJSON()).map(([id, balance]) => ({
503
+ id,
504
+ ...balance
505
+ }));
506
+ balances.db.transaction("rw", balances.db.balances, async () => await balances.db.balances.bulkPut(putBalances));
507
+ };
508
+ let unsubscribed = false;
509
+
510
+ // eslint-disable-next-line no-console
511
+ log.log("subscribing to balance changes for %d tokens and %d addresses", tokenIds.length, addresses.length);
512
+ const unsubs = balanceModules.map(async balanceModule => {
513
+ // filter out tokens to only include those which this module knows how to fetch balances for
514
+ const moduleTokenIds = Object.values(tokens ?? {}).filter(({
515
+ type
516
+ }) => type === balanceModule.type).map(({
517
+ id
518
+ }) => id);
519
+ const addressesByModuleToken = Object.fromEntries(Object.entries(addressesByToken).filter(([tokenId]) => moduleTokenIds.includes(tokenId)));
520
+ const unsub = balances.balances(balanceModule, chainConnectors, provider, addressesByModuleToken, (error, balances) => {
521
+ // log errors
522
+ if (error) return log.error(`Failed to fetch ${balanceModule.type} balances`, error);
523
+ // ignore empty balance responses
524
+ if (!balances) return;
525
+ // ignore balances from old subscriptions which are still in the process of unsubscribing
526
+ if (unsubscribed) return;
527
+ updateDb(balances);
451
528
  });
452
- const unsubscribeAll = () => unsubs.forEach(unsub => unsub());
453
- return unsubscribeAll;
454
- }, [addressesByToken, balanceModules, chainConnectors, chaindataProvider, tokens]);
455
- const subscribe = useMulticastSubscription(createSubscription);
456
- return subscribe;
457
- }
529
+ return () => {
530
+ // wait 2 seconds before actually unsubscribing, allowing for websocket to be reused
531
+ unsub.then(unsubscribe => {
532
+ setTimeout(unsubscribe, 2_000);
533
+ });
534
+ balances.db.transaction("rw", balances.db.balances, async () => await balances.db.balances.filter(balance => {
535
+ if (balance.source !== balanceModule.type) return false;
536
+ if (!Object.keys(addressesByModuleToken).includes(balance.tokenId)) return false;
537
+ if (!addressesByModuleToken[balance.tokenId].includes(balance.address)) return false;
538
+ return true;
539
+ }).modify({
540
+ status: "cache"
541
+ }));
542
+ };
543
+ });
544
+ const unsubscribeAll = () => {
545
+ unsubscribed = true;
546
+ unsubs.forEach(unsub => unsub.then(unsubscribe => unsubscribe()));
547
+ };
548
+ return unsubscribeAll;
549
+ };
458
550
 
459
551
  function useChains(withTestnets) {
460
552
  // keep db data up to date
@@ -486,7 +578,7 @@ function useEvmNetwork(evmNetworkId, withTestnets) {
486
578
 
487
579
  function useTokenRates() {
488
580
  // keep db data up to date
489
- useDbCacheSubscription("tokenRates");
581
+ useDbCacheTokenRatesSubscription();
490
582
  const {
491
583
  tokenRatesMap
492
584
  } = useDbCache();
@@ -497,21 +589,10 @@ function useTokenRate(tokenId) {
497
589
  return tokenId ? tokenRates[tokenId] : undefined;
498
590
  }
499
591
 
500
- function useTokens(withTestnets) {
501
- // keep db data up to date
502
- useDbCacheSubscription("tokens");
592
+ const useBalancesHydrate = () => {
503
593
  const {
504
- tokensWithTestnetsMap,
505
- tokensWithoutTestnetsMap
506
- } = useDbCache();
507
- return withTestnets ? tokensWithTestnetsMap : tokensWithoutTestnetsMap;
508
- }
509
- function useToken(tokenId, withTestnets) {
510
- const tokens = useTokens(withTestnets);
511
- return tokenId ? tokens[tokenId] : undefined;
512
- }
513
-
514
- const useBalancesHydrate = withTestnets => {
594
+ withTestnets
595
+ } = useWithTestnets();
515
596
  const chains = useChains(withTestnets);
516
597
  const evmNetworks = useEvmNetworks(withTestnets);
517
598
  const tokens = useTokens(withTestnets);
@@ -526,7 +607,7 @@ const useBalancesHydrate = withTestnets => {
526
607
 
527
608
  function useBalances(addressesByToken) {
528
609
  // keep db data up to date
529
- useDbCacheSubscription("balances");
610
+ useDbCacheBalancesSubscription();
530
611
  const balanceModules = useBalanceModules();
531
612
  const {
532
613
  balances: balances$1
@@ -557,18 +638,18 @@ function useBalances(addressesByToken) {
557
638
  const BalancesProvider = ({
558
639
  balanceModules,
559
640
  onfinalityApiKey,
560
- useTestnets,
641
+ withTestnets,
561
642
  children
562
- }) => /*#__PURE__*/jsxRuntime.jsx(ChaindataProvider, {
563
- onfinalityApiKey: onfinalityApiKey,
564
- children: /*#__PURE__*/jsxRuntime.jsx(ChainConnectorsProvider, {
643
+ }) => /*#__PURE__*/jsxRuntime.jsx(WithTestnetsProvider, {
644
+ withTestnets: withTestnets,
645
+ children: /*#__PURE__*/jsxRuntime.jsx(ChaindataProvider, {
565
646
  onfinalityApiKey: onfinalityApiKey,
566
- children: /*#__PURE__*/jsxRuntime.jsx(AllAddressesProvider, {
567
- children: /*#__PURE__*/jsxRuntime.jsx(BalanceModulesProvider, {
568
- balanceModules: balanceModules,
569
- children: /*#__PURE__*/jsxRuntime.jsx(DbCacheProvider, {
570
- useTestnets: useTestnets,
571
- children: /*#__PURE__*/jsxRuntime.jsx(SubscriptionsProvider, {
647
+ children: /*#__PURE__*/jsxRuntime.jsx(ChainConnectorsProvider, {
648
+ onfinalityApiKey: onfinalityApiKey,
649
+ children: /*#__PURE__*/jsxRuntime.jsx(AllAddressesProvider, {
650
+ children: /*#__PURE__*/jsxRuntime.jsx(BalanceModulesProvider, {
651
+ balanceModules: balanceModules,
652
+ children: /*#__PURE__*/jsxRuntime.jsx(DbCacheProvider, {
572
653
  children: children
573
654
  })
574
655
  })
@@ -583,7 +664,7 @@ exports.BalancesProvider = BalancesProvider;
583
664
  exports.ChainConnectorsProvider = ChainConnectorsProvider;
584
665
  exports.ChaindataProvider = ChaindataProvider;
585
666
  exports.DbCacheProvider = DbCacheProvider;
586
- exports.SubscriptionsProvider = SubscriptionsProvider;
667
+ exports.WithTestnetsProvider = WithTestnetsProvider;
587
668
  exports.createMulticastSubscription = createMulticastSubscription;
588
669
  exports.provideContext = provideContext;
589
670
  exports.useAllAddresses = useAllAddresses;
@@ -595,12 +676,14 @@ exports.useChainConnectors = useChainConnectors;
595
676
  exports.useChaindata = useChaindata;
596
677
  exports.useChains = useChains;
597
678
  exports.useDbCache = useDbCache;
679
+ exports.useDbCacheBalancesSubscription = useDbCacheBalancesSubscription;
598
680
  exports.useDbCacheSubscription = useDbCacheSubscription;
681
+ exports.useDbCacheTokenRatesSubscription = useDbCacheTokenRatesSubscription;
599
682
  exports.useEvmNetwork = useEvmNetwork;
600
683
  exports.useEvmNetworks = useEvmNetworks;
601
684
  exports.useMulticastSubscription = useMulticastSubscription;
602
- exports.useSubscriptions = useSubscriptions;
603
685
  exports.useToken = useToken;
604
686
  exports.useTokenRate = useTokenRate;
605
687
  exports.useTokenRates = useTokenRates;
606
688
  exports.useTokens = useTokens;
689
+ exports.useWithTestnets = useWithTestnets;