@talismn/balances-react 0.0.0-pr589-20230302073822 → 0.0.0-pr595-20230305120713

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.
@@ -2,169 +2,23 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var react = require('react');
6
- var jsxRuntime = require('react/jsx-runtime');
7
5
  var balances = require('@talismn/balances');
8
- var tokenRates = require('@talismn/token-rates');
6
+ var chainConnector = require('@talismn/chain-connector');
7
+ var chainConnectorEvm = require('@talismn/chain-connector-evm');
9
8
  var dexieReactHooks = require('dexie-react-hooks');
9
+ var react = require('react');
10
10
  var reactUse = require('react-use');
11
- var chaindataProviderExtension = require('@talismn/chaindata-provider-extension');
12
11
  var anylogger = require('anylogger');
13
- var rxjs = require('rxjs');
14
- var chainConnector = require('@talismn/chain-connector');
15
- var chainConnectorEvm = require('@talismn/chain-connector-evm');
12
+ var chaindataProviderExtension = require('@talismn/chaindata-provider-extension');
13
+ var tokenRates = require('@talismn/token-rates');
16
14
 
17
15
  function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
18
16
 
19
17
  var anylogger__default = /*#__PURE__*/_interopDefault(anylogger);
20
18
 
21
- const provideContext = useProviderContext => {
22
- // automatic typing based on our hook's return type
23
-
24
- const Context = /*#__PURE__*/react.createContext({
25
- __provideContextInternalDefaultValue: true
26
- });
27
- const Provider = ({
28
- children,
29
- ...props
30
- }) => {
31
- const ctx = useProviderContext(props);
32
- return /*#__PURE__*/jsxRuntime.jsx(Context.Provider, {
33
- value: ctx,
34
- children: children
35
- });
36
- };
37
- const useProvidedContext = () => {
38
- const context = react.useContext(Context);
39
- if (typeof context === "object" && context && "__provideContextInternalDefaultValue" in context) throw new Error("This hook requires a provider to be present above it in the tree");
40
- return context;
41
- };
42
- const result = [Provider, useProvidedContext];
43
- return result;
44
- };
45
-
46
- const useAllAddressesProvider = () => react.useState([]);
47
- const [AllAddressesProvider, useAllAddresses] = provideContext(useAllAddressesProvider);
48
-
49
- const useBalanceModulesProvider = ({
50
- balanceModules
51
- }) => balanceModules;
52
- const [BalanceModulesProvider, useBalanceModules] = provideContext(useBalanceModulesProvider);
53
-
54
- function useChaindataProvider(options = {}) {
55
- const [chaindata, setChaindata] = react.useState();
56
- react.useEffect(() => {
57
- setChaindata(new chaindataProviderExtension.ChaindataProviderExtension({
58
- onfinalityApiKey: options.onfinalityApiKey
59
- }));
60
- }, [options.onfinalityApiKey]);
61
- return chaindata;
62
- }
63
- const [ChaindataProvider, useChaindata] = provideContext(useChaindataProvider);
64
-
65
- const filterNoTestnet = ({
66
- isTestnet
67
- }) => isTestnet === false;
68
- const DEFAULT_VALUE = {
69
- chainsWithTestnets: [],
70
- chainsWithoutTestnets: [],
71
- evmNetworksWithTestnets: [],
72
- evmNetworksWithoutTestnets: [],
73
- tokensWithTestnets: [],
74
- tokensWithoutTestnets: [],
75
- chainsWithTestnetsMap: {},
76
- chainsWithoutTestnetsMap: {},
77
- evmNetworksWithTestnetsMap: {},
78
- evmNetworksWithoutTestnetsMap: {},
79
- tokensWithTestnetsMap: {},
80
- tokensWithoutTestnetsMap: {},
81
- tokenRatesMap: {},
82
- balances: []
83
- };
84
- const consolidateDbCache = (chainsMap, evmNetworksMap, tokensMap, tokenRates, allBalances) => {
85
- if (!chainsMap || !evmNetworksMap || !tokensMap || !tokenRates || !allBalances) return DEFAULT_VALUE;
86
-
87
- // BEGIN: temp hack to indicate that
88
- // - EVM GLMR is a mirror of substrate GLMR
89
- // - EVM MOVR is a mirror of substrate MOVR
90
- // - EVM DEV is a mirror of substrate DEV
91
- // - EVM ACA is a mirror of substrate ACA
92
- const mirrorTokenIds = {
93
- "1284-evm-native-glmr": "moonbeam-substrate-native-glmr",
94
- "1285-evm-native-movr": "moonriver-substrate-native-movr",
95
- "1287-evm-native-dev": "moonbase-alpha-testnet-substrate-native-dev",
96
- "787-evm-native-aca": "acala-substrate-native-aca"
97
- };
98
- Object.entries(mirrorTokenIds).filter(([mirrorToken]) => tokensMap[mirrorToken]).forEach(([mirrorToken, mirrorOf]) => tokensMap[mirrorToken].mirrorOf = mirrorOf);
99
- // END: temp hack
100
-
101
- const chainsWithTestnets = Object.values(chainsMap);
102
- const chainsWithoutTestnets = chainsWithTestnets.filter(filterNoTestnet);
103
- const chainsWithoutTestnetsMap = Object.fromEntries(chainsWithoutTestnets.map(network => [network.id, network]));
104
- const evmNetworksWithTestnets = Object.values(evmNetworksMap);
105
- const evmNetworksWithoutTestnets = evmNetworksWithTestnets.filter(filterNoTestnet);
106
- const evmNetworksWithoutTestnetsMap = Object.fromEntries(evmNetworksWithoutTestnets.map(network => [network.id, network]));
107
-
108
- // ensure that we have corresponding network for each token
109
- const tokensWithTestnets = Object.values(tokensMap).filter(token => token.chain && chainsMap[token.chain.id] || token.evmNetwork && evmNetworksMap[token.evmNetwork.id]);
110
- const tokensWithoutTestnets = tokensWithTestnets.filter(filterNoTestnet).filter(token => token.chain && chainsWithoutTestnetsMap[token.chain.id] || token.evmNetwork && evmNetworksWithoutTestnetsMap[token.evmNetwork.id]);
111
- const tokensWithTestnetsMap = Object.fromEntries(tokensWithTestnets.map(token => [token.id, token]));
112
- const tokensWithoutTestnetsMap = Object.fromEntries(tokensWithoutTestnets.map(token => [token.id, token]));
113
- const tokenRatesMap = Object.fromEntries(tokenRates.map(({
114
- tokenId,
115
- rates
116
- }) => [tokenId, rates]));
117
-
118
- // return only balances for which we have a token
119
- const balances = allBalances.filter(b => tokensWithTestnetsMap[b.tokenId]);
120
- return {
121
- chainsWithTestnets,
122
- chainsWithoutTestnets,
123
- evmNetworksWithTestnets,
124
- evmNetworksWithoutTestnets,
125
- tokensWithTestnets,
126
- tokensWithoutTestnets,
127
- chainsWithTestnetsMap: chainsMap,
128
- chainsWithoutTestnetsMap,
129
- evmNetworksWithTestnetsMap: evmNetworksMap,
130
- evmNetworksWithoutTestnetsMap,
131
- tokensWithTestnetsMap,
132
- tokensWithoutTestnetsMap,
133
- tokenRatesMap,
134
- balances
135
- };
136
- };
137
- const useDbCacheProvider = ({
138
- useTestnets = false
139
- }) => {
140
- const chaindataProvider = useChaindata();
141
- const chainList = dexieReactHooks.useLiveQuery(() => chaindataProvider === null || chaindataProvider === void 0 ? void 0 : chaindataProvider.chains(), [chaindataProvider]);
142
- const evmNetworkList = dexieReactHooks.useLiveQuery(() => chaindataProvider === null || chaindataProvider === void 0 ? void 0 : chaindataProvider.evmNetworks(), [chaindataProvider]);
143
- const tokenList = dexieReactHooks.useLiveQuery(() => chaindataProvider === null || chaindataProvider === void 0 ? void 0 : chaindataProvider.tokens(), [chaindataProvider]);
144
- const tokenRates$1 = dexieReactHooks.useLiveQuery(() => tokenRates.db.tokenRates.toArray(), []);
145
- const rawBalances = dexieReactHooks.useLiveQuery(() => balances.db.balances.toArray(), []);
146
- const [dbData, setDbData] = react.useState(DEFAULT_VALUE);
147
-
148
- // debounce every 500ms to prevent hammering UI with updates
149
- reactUse.useDebounce(() => {
150
- setDbData(consolidateDbCache(chainList, evmNetworkList, tokenList, tokenRates$1, rawBalances));
151
- }, 500, [chainList, evmNetworkList, tokenList, rawBalances, tokenRates$1, useTestnets]);
152
- const refInitialized = react.useRef(false);
153
-
154
- // force an update as soon as all datasources are fetched, so UI can display data ASAP
155
- react.useEffect(() => {
156
- if (!refInitialized.current && chainList && evmNetworkList && tokenList && tokenRates$1 && rawBalances) {
157
- setDbData(consolidateDbCache(chainList, evmNetworkList, tokenList, tokenRates$1, rawBalances));
158
- refInitialized.current = true;
159
- }
160
- }, [chainList, evmNetworkList, rawBalances, tokenList, tokenRates$1, useTestnets]);
161
- return dbData;
162
- };
163
- const [DbCacheProvider, useDbCache] = provideContext(useDbCacheProvider);
164
-
165
19
  var packageJson = {
166
20
  name: "@talismn/balances-react",
167
- version: "0.0.0-pr589-20230302073822",
21
+ version: "0.0.0-pr595-20230305120713",
168
22
  author: "Talisman",
169
23
  homepage: "https://talisman.xyz",
170
24
  license: "UNLICENSED",
@@ -199,8 +53,7 @@ var packageJson = {
199
53
  anylogger: "^1.0.11",
200
54
  dexie: "^3.2.3",
201
55
  "dexie-react-hooks": "^1.1.1",
202
- "react-use": "^17.4.0",
203
- rxjs: "^7.8.0"
56
+ "react-use": "^17.4.0"
204
57
  },
205
58
  devDependencies: {
206
59
  "@talismn/eslint-config": "workspace:^",
@@ -227,312 +80,139 @@ var packageJson = {
227
80
 
228
81
  var log = anylogger__default["default"](packageJson.name);
229
82
 
230
- /**
231
- * Creates a subscription function that can be used to subscribe to a multicast observable created from an upstream source.
232
- *
233
- * An example of when this is useful is when we want to subscribe to some data from multiple components, but we only want
234
- * to actively keep that data hydrated when at least one component is subscribed to it.
235
- *
236
- * When the first component subscribes, the `upstream` function will be called. It should then set up a subscription and return a teardown function.
237
- * When subsequent components subscribe, they will be added to the existing subscription.
238
- * When the last component unsubscribes, the teardown function returned from the `upstream` function will be called.
239
- *
240
- * @param upstream A function that takes a "next" callback function as an argument, and returns either an unsubscribe function or void.
241
- * @returns A subscription function that can be used to subscribe to the multicast observable.
242
- */
243
- const useMulticastSubscription = upstream => {
244
- const subscribe = react.useMemo(() => createMulticastSubscription(upstream), [upstream]);
245
- return subscribe;
246
- };
247
- const createMulticastSubscription = upstream => {
248
- // Create an upstream observable using the provided function.
249
- const upstreamObservable = new rxjs.Observable(subscriber => {
250
- const unsubscribe = upstream(val => subscriber.next(val));
251
- return () => {
252
- typeof unsubscribe === "function" && unsubscribe();
253
- };
254
- });
255
-
256
- // Create a multicast observable from the upstream observable, using the shareReplay operator.
257
- const multicastObservable = rxjs.defer(() => upstreamObservable).pipe(rxjs.shareReplay({
258
- bufferSize: 1,
259
- refCount: true
260
- }));
261
-
262
- // Create a subscription function that subscribes to the multicast observable and returns an unsubscribe function.
263
- const subscribe = callback => {
264
- const subscription = multicastObservable.subscribe(callback);
265
- const unsubscribe = () => subscription.unsubscribe();
266
- return unsubscribe;
267
- };
268
- return subscribe;
269
- };
83
+ // TODO: Allow user to call useChaindata from multiple places
84
+ function useChaindata(options = {}) {
85
+ const [chaindataProvider, setChaindataProvider] = react.useState(null);
270
86
 
271
- function useChainConnectorsProvider(options) {
272
- // chaindata dependency
273
- const chaindata = useChaindata();
274
-
275
- // substrate connector
276
- const [substrate, setSubstrate] = react.useState();
87
+ // this number is incremented each time the chaindataProvider has fetched new data
88
+ const [generation, setGeneration] = react.useState(0);
277
89
  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, {
90
+ const chaindataProvider = new chaindataProviderExtension.ChaindataProviderExtension({
287
91
  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);
299
-
300
- /**
301
- * This hook is responsible for fetching the data used for balances and inserting it into the db.
302
- */
303
- const useDbCacheSubscription = subscribeTo => {
304
- const [subscribeHydrateChains, subscribeHydrateEvmNetworks, subscribeHydrateTokens, subscribeTokenRates, subscribeBalances] = useSubscriptions();
305
- react.useEffect(() => {
306
- switch (subscribeTo) {
307
- case "chains":
308
- return subscribeHydrateChains();
309
- case "evmNetworks":
310
- return subscribeHydrateEvmNetworks();
311
- case "tokens":
312
- return subscribeHydrateTokens();
313
- case "tokenRates":
314
- return subscribeTokenRates();
315
- case "balances":
316
- return subscribeBalances();
317
- }
318
- }, [subscribeTo, subscribeHydrateChains, subscribeHydrateEvmNetworks, subscribeHydrateTokens, subscribeTokenRates, subscribeBalances]);
319
- };
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
-
92
+ });
93
+ let shouldHydrate = true;
94
+ const timer = 300_000; // 300_000ms = 300s = 5 minutes
329
95
  const hydrate = async () => {
330
- if (!active) return;
96
+ if (!shouldHydrate) return;
331
97
  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);
98
+ const updated = await chaindataProvider.hydrate();
99
+ if (updated) setGeneration(generation => (generation + 1) % Number.MAX_SAFE_INTEGER);
100
+ setTimeout(hydrate, timer);
336
101
  } catch (error) {
337
102
  const retryTimeout = 5_000; // 5_000ms = 5 seconds
338
103
  log.error(`Failed to fetch chaindata, retrying in ${Math.round(retryTimeout / 1000)} seconds`, error);
339
104
  setTimeout(hydrate, retryTimeout);
340
105
  }
341
106
  };
107
+ setChaindataProvider(chaindataProvider);
342
108
  hydrate();
343
109
  return () => {
344
- active = false;
110
+ shouldHydrate = false;
345
111
  };
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;
112
+ }, [options.onfinalityApiKey]);
113
+ if (chaindataProvider) chaindataProvider.generation = generation;
114
+ return chaindataProvider;
392
115
  }
393
- function useSubscribeBalances() {
394
- const balanceModules = useBalanceModules();
395
- const chaindataProvider = useChaindata();
396
- const chainConnectors = useChainConnectors();
397
- 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(({
400
- 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;
116
+ function useChains(chaindata) {
117
+ const [chains, setChains] = react.useState();
118
+ react.useEffect(() => {
119
+ if (!chaindata) return;
120
+ const thisGeneration = chaindata.generation;
121
+ chaindata.chains().then(chains => {
122
+ if (thisGeneration !== chaindata.generation) return;
123
+ setChains(chains);
451
124
  });
452
- const unsubscribeAll = () => unsubs.forEach(unsub => unsub());
453
- return unsubscribeAll;
454
- }, [addressesByToken, balanceModules, chainConnectors, chaindataProvider, tokens]);
455
- const subscribe = useMulticastSubscription(createSubscription);
456
- return subscribe;
125
+ }, [chaindata, chaindata === null || chaindata === void 0 ? void 0 : chaindata.generation]);
126
+ return chains || {};
457
127
  }
458
-
459
- function useChains(withTestnets) {
460
- // keep db data up to date
461
- useDbCacheSubscription("chains");
462
- const {
463
- chainsWithTestnetsMap,
464
- chainsWithoutTestnetsMap
465
- } = useDbCache();
466
- return withTestnets ? chainsWithTestnetsMap : chainsWithoutTestnetsMap;
467
- }
468
- function useChain(chainId, withTestnets) {
469
- const chains = useChains(withTestnets);
470
- return chainId ? chains[chainId] : undefined;
128
+ function useChain(chaindata, chainId) {
129
+ const [chain, setChain] = react.useState();
130
+ react.useEffect(() => {
131
+ if (chaindata === null) return;
132
+ if (!chainId) return;
133
+ chaindata.getChain(chainId).then(setChain);
134
+ }, [chainId, chaindata, chaindata === null || chaindata === void 0 ? void 0 : chaindata.generation]);
135
+ return chain;
471
136
  }
472
-
473
- function useEvmNetworks(withTestnets) {
474
- // keep db data up to date
475
- useDbCacheSubscription("evmNetworks");
476
- const {
477
- evmNetworksWithTestnetsMap,
478
- evmNetworksWithoutTestnetsMap
479
- } = useDbCache();
480
- return withTestnets ? evmNetworksWithTestnetsMap : evmNetworksWithoutTestnetsMap;
137
+ function useEvmNetworks(chaindata) {
138
+ const [evmNetworks, setEvmNetworks] = react.useState();
139
+ react.useEffect(() => {
140
+ if (!chaindata) return;
141
+ const thisGeneration = chaindata.generation;
142
+ chaindata.evmNetworks().then(evmNetworks => {
143
+ if (thisGeneration !== chaindata.generation) return;
144
+ setEvmNetworks(evmNetworks);
145
+ });
146
+ }, [chaindata, chaindata === null || chaindata === void 0 ? void 0 : chaindata.generation]);
147
+ return evmNetworks || {};
481
148
  }
482
- function useEvmNetwork(evmNetworkId, withTestnets) {
483
- const evmNetworks = useEvmNetworks(withTestnets);
484
- return evmNetworkId ? evmNetworks[evmNetworkId] : undefined;
149
+ function useEvmNetwork(chaindata, evmNetworkId) {
150
+ const [evmNetwork, setEvmNetwork] = react.useState();
151
+ react.useEffect(() => {
152
+ if (chaindata === null) return;
153
+ if (!evmNetworkId) return;
154
+ chaindata.getEvmNetwork(evmNetworkId).then(setEvmNetwork);
155
+ }, [chaindata, chaindata === null || chaindata === void 0 ? void 0 : chaindata.generation, evmNetworkId]);
156
+ return evmNetwork;
485
157
  }
486
-
487
- function useTokenRates() {
488
- // keep db data up to date
489
- useDbCacheSubscription("tokenRates");
490
- const {
491
- tokenRatesMap
492
- } = useDbCache();
493
- return tokenRatesMap;
158
+ function useTokens(chaindata) {
159
+ const [tokens, setTokens] = react.useState();
160
+ react.useEffect(() => {
161
+ if (!chaindata) return;
162
+ const thisGeneration = chaindata.generation;
163
+ chaindata.tokens().then(tokens => {
164
+ if (thisGeneration !== chaindata.generation) return;
165
+ setTokens(tokens);
166
+ });
167
+ }, [chaindata, chaindata === null || chaindata === void 0 ? void 0 : chaindata.generation]);
168
+ return tokens || {};
494
169
  }
495
- function useTokenRate(tokenId) {
496
- const tokenRates = useTokenRates();
497
- return tokenId ? tokenRates[tokenId] : undefined;
170
+ function useToken(chaindata, tokenId) {
171
+ const [token, setToken] = react.useState();
172
+ react.useEffect(() => {
173
+ if (chaindata === null) return;
174
+ if (!tokenId) return;
175
+ chaindata.getToken(tokenId).then(setToken);
176
+ }, [chaindata, chaindata === null || chaindata === void 0 ? void 0 : chaindata.generation, tokenId]);
177
+ return token;
498
178
  }
499
179
 
500
- function useTokens(withTestnets) {
501
- // keep db data up to date
502
- useDbCacheSubscription("tokens");
503
- 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
- }
180
+ function useTokenRates(tokens) {
181
+ const generation = react.useRef(0);
182
+ const [tokenRates$1, setTokenRates] = react.useState();
183
+ react.useEffect(() => {
184
+ if (!tokens) return;
185
+ if (Object.keys(tokens).length < 1) return;
513
186
 
514
- const useBalancesHydrate = withTestnets => {
515
- const chains = useChains(withTestnets);
516
- const evmNetworks = useEvmNetworks(withTestnets);
517
- const tokens = useTokens(withTestnets);
518
- const tokenRates = useTokenRates();
519
- return react.useMemo(() => ({
520
- chains,
521
- evmNetworks,
522
- tokens,
523
- tokenRates
524
- }), [chains, evmNetworks, tokens, tokenRates]);
525
- };
187
+ // when we make a new request, we want to ignore any old requests which haven't yet completed
188
+ // otherwise we risk replacing the most recent data with older data
189
+ generation.current = (generation.current + 1) % Number.MAX_SAFE_INTEGER;
190
+ const thisGeneration = generation.current;
191
+ tokenRates.fetchTokenRates(tokens).then(tokenRates => {
192
+ if (thisGeneration !== generation.current) return;
193
+ setTokenRates(tokenRates);
194
+ });
195
+ }, [tokens]);
196
+ return tokenRates$1 || {};
197
+ }
526
198
 
527
- function useBalances(addressesByToken) {
528
- // keep db data up to date
529
- useDbCacheSubscription("balances");
530
- const balanceModules = useBalanceModules();
531
- const {
532
- balances: balances$1
533
- } = useDbCache();
534
- const hydrate = useBalancesHydrate();
535
- return react.useMemo(() => new balances.Balances(balances$1.filter(balance => {
199
+ // TODO: Add the equivalent functionalty of `useDbCache` directly to this library.
200
+ //
201
+ // How it will work:
202
+ //
203
+ // useChains/useEvmNetworks/useTokens/useTokenRates will all make use of a
204
+ // useCachedDb hook, which internally subscribes to all of the db tables
205
+ // for everything, and then filters the subscribed data based on what params
206
+ // the caller of useChains/useTokens/etc has provided.
207
+ function useBalances(
208
+ // TODO: Make this array of BalanceModules more type-safe
209
+ balanceModules, chaindataProvider, addressesByToken, options = {}) {
210
+ useBalancesSubscriptions(balanceModules, chaindataProvider, addressesByToken, options);
211
+ const chains = useChains(chaindataProvider);
212
+ const evmNetworks = useEvmNetworks(chaindataProvider);
213
+ const tokens = useTokens(chaindataProvider);
214
+ const tokenRates = useTokenRates(tokens);
215
+ const balances$1 = dexieReactHooks.useLiveQuery(async () => new balances.Balances(await balances.db.balances.filter(balance => {
536
216
  // check that this balance is included in our queried balance modules
537
217
  if (!balanceModules.map(({
538
218
  type
@@ -549,58 +229,145 @@ function useBalances(addressesByToken) {
549
229
 
550
230
  // keep this balance
551
231
  return true;
552
- }),
232
+ }).toArray(),
553
233
  // hydrate balance chains, evmNetworks, tokens and tokenRates
554
- hydrate), [balances$1, hydrate, balanceModules, addressesByToken]);
234
+ {
235
+ chains,
236
+ evmNetworks,
237
+ tokens,
238
+ tokenRates
239
+ }), [balanceModules, addressesByToken, chains, evmNetworks, tokens, tokenRates]);
240
+
241
+ // debounce every 100ms to prevent hammering UI with updates
242
+ const [debouncedBalances, setDebouncedBalances] = react.useState(balances$1);
243
+ reactUse.useDebounce(() => balances$1 && setDebouncedBalances(balances$1), 100, [balances$1]);
244
+ return debouncedBalances;
555
245
  }
556
246
 
557
- const BalancesProvider = ({
558
- balanceModules,
559
- onfinalityApiKey,
560
- useTestnets,
561
- children
562
- }) => /*#__PURE__*/jsxRuntime.jsx(ChaindataProvider, {
563
- onfinalityApiKey: onfinalityApiKey,
564
- children: /*#__PURE__*/jsxRuntime.jsx(ChainConnectorsProvider, {
565
- 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, {
572
- children: children
573
- })
574
- })
575
- })
576
- })
577
- })
578
- });
247
+ // TODO: Turn into react context
248
+ const subscriptions = {};
249
+
250
+ // This hook is responsible for allowing us to call useBalances
251
+ // from multiple components, without setting up unnecessary
252
+ // balance subscriptions
253
+ function useBalancesSubscriptions(
254
+ // TODO: Make this array of BalanceModules more type-safe
255
+ balanceModules, chaindataProvider, addressesByToken, options = {}) {
256
+ // const subscriptions = useRef<
257
+ // Record<string, { unsub: Promise<() => void>; refcount: number; generation: number }>
258
+ // >({})
259
+
260
+ const addSubscription = (key, balanceModule, chainConnectors, chaindataProvider, addressesByToken) => {
261
+ // create subscription if it doesn't already exist
262
+ if (!subscriptions[key] || subscriptions[key].refcount === 0) {
263
+ var _subscriptions$key;
264
+ const generation = ((((_subscriptions$key = subscriptions[key]) === null || _subscriptions$key === void 0 ? void 0 : _subscriptions$key.generation) || 0) + 1) % Number.MAX_SAFE_INTEGER;
265
+ const unsub = balances.balances(balanceModule, chainConnectors, chaindataProvider, addressesByToken, (error, balances$1) => {
266
+ if (error) return log.error(`Failed to fetch ${balanceModule.type} balances`, error);
267
+ if (!balances$1) return;
268
+
269
+ // ignore balances from old subscriptions which are still in the process of unsubscribing
270
+ if (subscriptions[key].generation !== generation) return;
271
+ const putBalances = Object.entries(balances$1.toJSON()).map(([id, balance]) => ({
272
+ id,
273
+ ...balance
274
+ }));
275
+ balances.db.transaction("rw", balances.db.balances, async () => await balances.db.balances.bulkPut(putBalances));
276
+ });
277
+ subscriptions[key] = {
278
+ unsub,
279
+ refcount: 0,
280
+ generation
281
+ };
282
+ }
283
+
284
+ // bump up the refcount by 1
285
+ subscriptions[key].refcount += 1;
286
+ };
287
+ const removeSubscription = (key, balanceModule, addressesByToken) => {
288
+ // ignore dead subscriptions
289
+ if (!subscriptions[key] || subscriptions[key].refcount === 0) return;
290
+
291
+ // drop the refcount by one
292
+ subscriptions[key].refcount -= 1;
293
+
294
+ // unsubscribe if refcount is now 0 (nobody wants this subcription anymore)
295
+ if (subscriptions[key].refcount < 1) {
296
+ // remove subscription
297
+ subscriptions[key].unsub.then(unsub => unsub());
298
+ delete subscriptions[key];
299
+
300
+ // set this subscription's balances in the store to status: cache
301
+ balances.db.transaction("rw", balances.db.balances, async () => await balances.db.balances.filter(balance => {
302
+ if (balance.source !== balanceModule.type) return false;
303
+ if (!Object.keys(addressesByToken).includes(balance.tokenId)) return false;
304
+ if (!addressesByToken[balance.tokenId].includes(balance.address)) return false;
305
+ return true;
306
+ }).modify({
307
+ status: "cache"
308
+ }));
309
+ }
310
+ };
311
+ const chainConnector = useChainConnector(chaindataProvider);
312
+ const chainConnectorEvm = useChainConnectorEvm(chaindataProvider, options);
313
+ const tokens = useTokens(chaindataProvider);
314
+ react.useEffect(() => {
315
+ if (chainConnector === null) return;
316
+ if (chainConnectorEvm === null) return;
317
+ if (chaindataProvider === null) return;
318
+ if (addressesByToken === null) return;
319
+ const unsubs = balanceModules.map(balanceModule => {
320
+ const subscriptionKey = `${balanceModule.type}-${JSON.stringify(addressesByToken)}`;
321
+
322
+ // filter out tokens to only include those which this module knows how to fetch balances for
323
+ const moduleTokenIds = Object.values(tokens).filter(({
324
+ type
325
+ }) => type === balanceModule.type).map(({
326
+ id
327
+ }) => id);
328
+ const addressesByModuleToken = Object.fromEntries(Object.entries(addressesByToken).filter(([tokenId]) => moduleTokenIds.includes(tokenId)));
329
+
330
+ // add balance subscription for this module
331
+ addSubscription(subscriptionKey, balanceModule, {
332
+ substrate: chainConnector,
333
+ evm: chainConnectorEvm
334
+ }, chaindataProvider, addressesByModuleToken);
335
+
336
+ // return an unsub method, to be called when this effect unmounts
337
+ return () => removeSubscription(subscriptionKey, balanceModule, addressesByToken);
338
+ });
339
+ const unsubAll = () => unsubs.forEach(unsub => unsub());
340
+ return unsubAll;
341
+ }, [addressesByToken, balanceModules, chainConnector, chainConnectorEvm, chaindataProvider, tokens]);
342
+ }
343
+
344
+ // TODO: Allow advanced users of this library to provide their own chain connector
345
+ function useChainConnector(chaindataProvider) {
346
+ const [chainConnector$1, setChainConnector] = react.useState(null);
347
+ react.useEffect(() => {
348
+ if (chaindataProvider === null) return;
349
+ setChainConnector(new chainConnector.ChainConnector(chaindataProvider));
350
+ }, [chaindataProvider]);
351
+ return chainConnector$1;
352
+ }
353
+ // TODO: Allow advanced users of this library to provide their own chain connector
354
+ function useChainConnectorEvm(chaindataProvider, options = {}) {
355
+ const [chainConnectorEvm$1, setChainConnectorEvm] = react.useState(null);
356
+ react.useEffect(() => {
357
+ if (chaindataProvider === null) return;
358
+ setChainConnectorEvm(new chainConnectorEvm.ChainConnectorEvm(chaindataProvider, {
359
+ onfinalityApiKey: options.onfinalityApiKey
360
+ }));
361
+ }, [chaindataProvider, options.onfinalityApiKey]);
362
+ return chainConnectorEvm$1;
363
+ }
579
364
 
580
- exports.AllAddressesProvider = AllAddressesProvider;
581
- exports.BalanceModulesProvider = BalanceModulesProvider;
582
- exports.BalancesProvider = BalancesProvider;
583
- exports.ChainConnectorsProvider = ChainConnectorsProvider;
584
- exports.ChaindataProvider = ChaindataProvider;
585
- exports.DbCacheProvider = DbCacheProvider;
586
- exports.SubscriptionsProvider = SubscriptionsProvider;
587
- exports.createMulticastSubscription = createMulticastSubscription;
588
- exports.provideContext = provideContext;
589
- exports.useAllAddresses = useAllAddresses;
590
- exports.useBalanceModules = useBalanceModules;
591
365
  exports.useBalances = useBalances;
592
- exports.useBalancesHydrate = useBalancesHydrate;
593
366
  exports.useChain = useChain;
594
- exports.useChainConnectors = useChainConnectors;
595
367
  exports.useChaindata = useChaindata;
596
368
  exports.useChains = useChains;
597
- exports.useDbCache = useDbCache;
598
- exports.useDbCacheSubscription = useDbCacheSubscription;
599
369
  exports.useEvmNetwork = useEvmNetwork;
600
370
  exports.useEvmNetworks = useEvmNetworks;
601
- exports.useMulticastSubscription = useMulticastSubscription;
602
- exports.useSubscriptions = useSubscriptions;
603
371
  exports.useToken = useToken;
604
- exports.useTokenRate = useTokenRate;
605
372
  exports.useTokenRates = useTokenRates;
606
373
  exports.useTokens = useTokens;