@talismn/balances-react 0.0.0-pr563-20230222051735 → 0.0.0-pr573-20230227134247

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