@talismn/balances-react 0.0.0-pr447-20230214040957 → 0.0.0-pr447-20230222115452

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,16 +1,162 @@
1
- import { Balances, db, balances } from '@talismn/balances';
2
- import { ChainConnector } from '@talismn/chain-connector';
3
- import { ChainConnectorEvm } from '@talismn/chain-connector-evm';
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';
4
5
  import { useLiveQuery } from 'dexie-react-hooks';
5
- import { useState, useEffect, useRef } from 'react';
6
6
  import { useDebounce } from 'react-use';
7
- import anylogger from 'anylogger';
8
7
  import { ChaindataProviderExtension } from '@talismn/chaindata-provider-extension';
9
- import { fetchTokenRates } from '@talismn/token-rates';
8
+ 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 === null || chaindataProvider === void 0 ? void 0 : chaindataProvider.chains(), [chaindataProvider]);
134
+ const evmNetworkList = useLiveQuery(() => chaindataProvider === null || chaindataProvider === void 0 ? void 0 : chaindataProvider.evmNetworks(), [chaindataProvider]);
135
+ const tokenList = useLiveQuery(() => chaindataProvider === null || chaindataProvider === void 0 ? void 0 : 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);
10
156
 
11
157
  var packageJson = {
12
158
  name: "@talismn/balances-react",
13
- version: "0.0.0-pr447-20230214040957",
159
+ version: "0.0.0-pr447-20230222115452",
14
160
  author: "Talisman",
15
161
  homepage: "https://talisman.xyz",
16
162
  license: "UNLICENSED",
@@ -43,9 +189,10 @@ var packageJson = {
43
189
  "@talismn/chaindata-provider-extension": "workspace:^",
44
190
  "@talismn/token-rates": "workspace:^",
45
191
  anylogger: "^1.0.11",
46
- dexie: "^3.2.2",
192
+ dexie: "^3.2.3",
47
193
  "dexie-react-hooks": "^1.1.1",
48
- "react-use": "^17.4.0"
194
+ "react-use": "^17.4.0",
195
+ rxjs: "^7.8.0"
49
196
  },
50
197
  devDependencies: {
51
198
  "@talismn/eslint-config": "workspace:^",
@@ -72,138 +219,312 @@ var packageJson = {
72
219
 
73
220
  var log = anylogger(packageJson.name);
74
221
 
75
- // TODO: Allow user to call useChaindata from multiple places
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
+ });
76
247
 
77
- function useChaindata() {
78
- const [chaindataProvider, setChaindataProvider] = useState(null);
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
+ }));
79
253
 
80
- // this number is incremented each time the chaindataProvider has fetched new data
81
- const [generation, setGeneration] = useState(0);
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();
276
+ useEffect(() => {
277
+ if (!chaindata) return;
278
+ setEvm(new ChainConnectorEvm(chaindata, {
279
+ 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();
82
297
  useEffect(() => {
83
- const chaindataProvider = new ChaindataProviderExtension();
84
- let shouldHydrate = true;
85
- const timer = 300_000; // 300_000ms = 300s = 5 minutes
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
+
86
321
  const hydrate = async () => {
87
- if (!shouldHydrate) return;
322
+ if (!active) return;
88
323
  try {
89
- const updated = await chaindataProvider.hydrate();
90
- if (updated) setGeneration(generation => (generation + 1) % Number.MAX_SAFE_INTEGER);
91
- setTimeout(hydrate, timer);
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);
92
328
  } catch (error) {
93
329
  const retryTimeout = 5_000; // 5_000ms = 5 seconds
94
330
  log.error(`Failed to fetch chaindata, retrying in ${Math.round(retryTimeout / 1000)} seconds`, error);
95
331
  setTimeout(hydrate, retryTimeout);
96
332
  }
97
333
  };
98
- setChaindataProvider(chaindataProvider);
99
334
  hydrate();
100
335
  return () => {
101
- shouldHydrate = false;
336
+ active = false;
102
337
  };
103
- }, []);
104
- if (chaindataProvider) chaindataProvider.generation = generation;
105
- return chaindataProvider;
106
- }
107
- function useChains(chaindata) {
108
- const [chains, setChains] = useState();
109
- useEffect(() => {
110
- if (!chaindata) return;
111
- const thisGeneration = chaindata.generation;
112
- chaindata.chains().then(chains => {
113
- if (thisGeneration !== chaindata.generation) return;
114
- setChains(chains);
115
- });
116
- }, [chaindata === null || chaindata === void 0 ? void 0 : chaindata.generation]);
117
- return chains || {};
338
+ }, [chaindata, type]);
339
+ const subscribe = useMulticastSubscription(createSubscription);
340
+ return subscribe;
118
341
  }
119
- function useChain(chaindata, chainId) {
120
- const [chain, setChain] = useState();
121
- useEffect(() => {
122
- if (chaindata === null) return;
123
- if (!chainId) return;
124
- chaindata.getChain(chainId).then(setChain);
125
- }, [chaindata === null || chaindata === void 0 ? void 0 : chaindata.generation]);
126
- return chain;
342
+ function useSubscribeTokenRates() {
343
+ const chaindataProvider = useChaindata();
344
+ const tokens = useLiveQuery(() => chaindataProvider === null || chaindataProvider === void 0 ? void 0 : 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;
380
+ };
381
+ }, [chaindataProvider, tokens]);
382
+ const subscribe = useMulticastSubscription(createSubscription);
383
+ return subscribe;
127
384
  }
128
- function useEvmNetworks(chaindata) {
129
- const [evmNetworks, setEvmNetworks] = useState();
130
- useEffect(() => {
131
- if (!chaindata) return;
132
- const thisGeneration = chaindata.generation;
133
- chaindata.evmNetworks().then(evmNetworks => {
134
- if (thisGeneration !== chaindata.generation) return;
135
- setEvmNetworks(evmNetworks);
385
+ function useSubscribeBalances() {
386
+ const balanceModules = useBalanceModules();
387
+ const chaindataProvider = useChaindata();
388
+ const chainConnectors = useChainConnectors();
389
+ const [allAddresses] = useAllAddresses();
390
+ const tokens = useLiveQuery(() => chaindataProvider === null || chaindataProvider === void 0 ? void 0 : 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;
136
443
  });
137
- }, [chaindata === null || chaindata === void 0 ? void 0 : chaindata.generation]);
138
- return evmNetworks || {};
444
+ const unsubscribeAll = () => unsubs.forEach(unsub => unsub());
445
+ return unsubscribeAll;
446
+ }, [addressesByToken, balanceModules, chainConnectors, chaindataProvider, tokens]);
447
+ const subscribe = useMulticastSubscription(createSubscription);
448
+ return subscribe;
139
449
  }
140
- function useEvmNetwork(chaindata, evmNetworkId) {
141
- const [evmNetwork, setEvmNetwork] = useState();
142
- useEffect(() => {
143
- if (chaindata === null) return;
144
- if (!evmNetworkId) return;
145
- chaindata.getEvmNetwork(evmNetworkId).then(setEvmNetwork);
146
- }, [chaindata === null || chaindata === void 0 ? void 0 : chaindata.generation]);
147
- return evmNetwork;
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;
148
459
  }
149
- function useTokens(chaindata) {
150
- const [tokens, setTokens] = useState();
151
- useEffect(() => {
152
- if (!chaindata) return;
153
- const thisGeneration = chaindata.generation;
154
- chaindata.tokens().then(tokens => {
155
- if (thisGeneration !== chaindata.generation) return;
156
- setTokens(tokens);
157
- });
158
- }, [chaindata === null || chaindata === void 0 ? void 0 : chaindata.generation]);
159
- return tokens || {};
460
+ function useChain(chainId, withTestnets) {
461
+ const chains = useChains(withTestnets);
462
+ return chainId ? chains[chainId] : undefined;
160
463
  }
161
- function useToken(chaindata, tokenId) {
162
- const [token, setToken] = useState();
163
- useEffect(() => {
164
- if (chaindata === null) return;
165
- if (!tokenId) return;
166
- chaindata.getToken(tokenId).then(setToken);
167
- }, [chaindata === null || chaindata === void 0 ? void 0 : chaindata.generation]);
168
- return token;
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;
473
+ }
474
+ function useEvmNetwork(evmNetworkId, withTestnets) {
475
+ const evmNetworks = useEvmNetworks(withTestnets);
476
+ return evmNetworkId ? evmNetworks[evmNetworkId] : undefined;
169
477
  }
170
478
 
171
- function useTokenRates(tokens) {
172
- const generation = useRef(0);
173
- const [tokenRates, setTokenRates] = useState();
174
- useEffect(() => {
175
- if (!tokens) return;
176
- if (Object.keys(tokens).length < 1) return;
479
+ function useTokenRates() {
480
+ // keep db data up to date
481
+ useDbCacheSubscription("tokenRates");
482
+ const {
483
+ tokenRatesMap
484
+ } = useDbCache();
485
+ return tokenRatesMap;
486
+ }
487
+ function useTokenRate(tokenId) {
488
+ const tokenRates = useTokenRates();
489
+ return tokenId ? tokenRates[tokenId] : undefined;
490
+ }
177
491
 
178
- // when we make a new request, we want to ignore any old requests which haven't yet completed
179
- // otherwise we risk replacing the most recent data with older data
180
- generation.current = (generation.current + 1) % Number.MAX_SAFE_INTEGER;
181
- const thisGeneration = generation.current;
182
- fetchTokenRates(tokens).then(tokenRates => {
183
- if (thisGeneration !== generation.current) return;
184
- setTokenRates(tokenRates);
185
- });
186
- }, [tokens]);
187
- return tokenRates || {};
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;
188
504
  }
189
505
 
190
- // TODO: Add the equivalent functionalty of `useDbCache` directly to this library.
191
- //
192
- // How it will work:
193
- //
194
- // useChains/useEvmNetworks/useTokens/useTokenRates will all make use of a
195
- // useCachedDb hook, which internally subscribes to all of the db tables
196
- // for everything, and then filters the subscribed data based on what params
197
- // the caller of useChains/useTokens/etc has provided.
198
- function useBalances(
199
- // TODO: Make this array of BalanceModules more type-safe
200
- balanceModules, chaindataProvider, addressesByToken) {
201
- useBalancesSubscriptions(balanceModules, chaindataProvider, addressesByToken);
202
- const chains = useChains(chaindataProvider);
203
- const evmNetworks = useEvmNetworks(chaindataProvider);
204
- const tokens = useTokens(chaindataProvider);
205
- const tokenRates = useTokenRates(tokens);
206
- const balances = useLiveQuery(async () => new Balances(await db.balances.filter(balance => {
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
+ };
518
+
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 => {
207
528
  // check that this balance is included in our queried balance modules
208
529
  if (!balanceModules.map(({
209
530
  type
@@ -220,135 +541,32 @@ balanceModules, chaindataProvider, addressesByToken) {
220
541
 
221
542
  // keep this balance
222
543
  return true;
223
- }).toArray(),
544
+ }),
224
545
  // hydrate balance chains, evmNetworks, tokens and tokenRates
225
- {
226
- chains,
227
- evmNetworks,
228
- tokens,
229
- tokenRates
230
- }), [balanceModules, addressesByToken, chains, evmNetworks, tokens, tokenRates]);
231
-
232
- // debounce every 100ms to prevent hammering UI with updates
233
- const [debouncedBalances, setDebouncedBalances] = useState(balances);
234
- useDebounce(() => balances && setDebouncedBalances(balances), 100, [balances]);
235
- return debouncedBalances;
546
+ hydrate), [balances, hydrate, balanceModules, addressesByToken]);
236
547
  }
237
548
 
238
- // TODO: Turn into react context
239
- const subscriptions = {};
240
-
241
- // This hook is responsible for allowing us to call useBalances
242
- // from multiple components, without setting up unnecessary
243
- // balance subscriptions
244
- function useBalancesSubscriptions(
245
- // TODO: Make this array of BalanceModules more type-safe
246
- balanceModules, chaindataProvider, addressesByToken) {
247
- // const subscriptions = useRef<
248
- // Record<string, { unsub: Promise<() => void>; refcount: number; generation: number }>
249
- // >({})
250
-
251
- const addSubscription = (key, balanceModule, chainConnectors, chaindataProvider, addressesByToken) => {
252
- // create subscription if it doesn't already exist
253
- if (!subscriptions[key] || subscriptions[key].refcount === 0) {
254
- var _subscriptions$key;
255
- const generation = ((((_subscriptions$key = subscriptions[key]) === null || _subscriptions$key === void 0 ? void 0 : _subscriptions$key.generation) || 0) + 1) % Number.MAX_SAFE_INTEGER;
256
- const unsub = balances(balanceModule, chainConnectors, chaindataProvider, addressesByToken, (error, balances) => {
257
- if (error) return log.error(`Failed to fetch ${balanceModule.type} balances`, error);
258
- if (!balances) return;
259
-
260
- // ignore balances from old subscriptions which are still in the process of unsubscribing
261
- if (subscriptions[key].generation !== generation) return;
262
- const putBalances = Object.entries(balances.toJSON()).map(([id, balance]) => ({
263
- id,
264
- ...balance
265
- }));
266
- db.transaction("rw", db.balances, async () => await db.balances.bulkPut(putBalances));
267
- });
268
- subscriptions[key] = {
269
- unsub,
270
- refcount: 0,
271
- generation
272
- };
273
- }
274
-
275
- // bump up the refcount by 1
276
- subscriptions[key].refcount += 1;
277
- };
278
- const removeSubscription = (key, balanceModule, addressesByToken) => {
279
- // ignore dead subscriptions
280
- if (!subscriptions[key] || subscriptions[key].refcount === 0) return;
281
-
282
- // drop the refcount by one
283
- subscriptions[key].refcount -= 1;
284
-
285
- // unsubscribe if refcount is now 0 (nobody wants this subcription anymore)
286
- if (subscriptions[key].refcount < 1) {
287
- // remove subscription
288
- subscriptions[key].unsub.then(unsub => unsub());
289
- delete subscriptions[key];
290
-
291
- // set this subscription's balances in the store to status: cache
292
- db.transaction("rw", db.balances, async () => await db.balances.filter(balance => {
293
- if (balance.source !== balanceModule.type) return false;
294
- if (!Object.keys(addressesByToken).includes(balance.tokenId)) return false;
295
- if (!addressesByToken[balance.tokenId].includes(balance.address)) return false;
296
- return true;
297
- }).modify({
298
- status: "cache"
299
- }));
300
- }
301
- };
302
- const chainConnector = useChainConnector(chaindataProvider);
303
- const chainConnectorEvm = useChainConnectorEvm(chaindataProvider);
304
- const tokens = useTokens(chaindataProvider);
305
- useEffect(() => {
306
- if (chainConnector === null) return;
307
- if (chainConnectorEvm === null) return;
308
- if (chaindataProvider === null) return;
309
- if (addressesByToken === null) return;
310
- const unsubs = balanceModules.map(balanceModule => {
311
- const subscriptionKey = `${balanceModule.type}-${JSON.stringify(addressesByToken)}`;
312
-
313
- // filter out tokens to only include those which this module knows how to fetch balances for
314
- const moduleTokenIds = Object.values(tokens).filter(({
315
- type
316
- }) => type === balanceModule.type).map(({
317
- id
318
- }) => id);
319
- const addressesByModuleToken = Object.fromEntries(Object.entries(addressesByToken).filter(([tokenId]) => moduleTokenIds.includes(tokenId)));
320
-
321
- // add balance subscription for this module
322
- addSubscription(subscriptionKey, balanceModule, {
323
- substrate: chainConnector,
324
- evm: chainConnectorEvm
325
- }, chaindataProvider, addressesByModuleToken);
326
-
327
- // return an unsub method, to be called when this effect unmounts
328
- return () => removeSubscription(subscriptionKey, balanceModule, addressesByToken);
329
- });
330
- const unsubAll = () => unsubs.forEach(unsub => unsub());
331
- return unsubAll;
332
- }, [addressesByToken, chainConnector, chainConnectorEvm, chaindataProvider, tokens]);
333
- }
334
-
335
- // TODO: Allow advanced users of this library to provide their own chain connector
336
- function useChainConnector(chaindataProvider) {
337
- const [chainConnector, setChainConnector] = useState(null);
338
- useEffect(() => {
339
- if (chaindataProvider === null) return;
340
- setChainConnector(new ChainConnector(chaindataProvider));
341
- }, [chaindataProvider]);
342
- return chainConnector;
343
- }
344
- // TODO: Allow advanced users of this library to provide their own chain connector
345
- function useChainConnectorEvm(chaindataProvider) {
346
- const [chainConnectorEvm, setChainConnectorEvm] = useState(null);
347
- useEffect(() => {
348
- if (chaindataProvider === null) return;
349
- setChainConnectorEvm(new ChainConnectorEvm(chaindataProvider));
350
- }, [chaindataProvider]);
351
- return chainConnectorEvm;
352
- }
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
+ });
353
571
 
354
- export { useBalances, useChain, useChaindata, useChains, useEvmNetwork, useEvmNetworks, useToken, useTokenRates, useTokens };
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 };