@talismn/balances-react 0.0.0-pr573-20230227134247 → 0.0.0-pr577-20230228061431

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
+ __provideContextInternalDefaultValue: 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 && "__provideContextInternalDefaultValue" 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-pr573-20230227134247",
159
+ version: "0.0.0-pr577-20230228061431",
14
160
  author: "Talisman",
15
161
  homepage: "https://talisman.xyz",
16
162
  license: "UNLICENSED",
@@ -45,7 +191,8 @@ var packageJson = {
45
191
  anylogger: "^1.0.11",
46
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,139 +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
76
- function useChaindata(options = {}) {
77
- const [chaindataProvider, setChaindataProvider] = useState(null);
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
+ }));
78
253
 
79
- // this number is incremented each time the chaindataProvider has fetched new data
80
- 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();
81
276
  useEffect(() => {
82
- const chaindataProvider = new ChaindataProviderExtension({
277
+ if (!chaindata) return;
278
+ setEvm(new ChainConnectorEvm(chaindata, {
83
279
  onfinalityApiKey: options.onfinalityApiKey
84
- });
85
- let shouldHydrate = true;
86
- const timer = 300_000; // 300_000ms = 300s = 5 minutes
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
+
87
321
  const hydrate = async () => {
88
- if (!shouldHydrate) return;
322
+ if (!active) return;
89
323
  try {
90
- const updated = await chaindataProvider.hydrate();
91
- if (updated) setGeneration(generation => (generation + 1) % Number.MAX_SAFE_INTEGER);
92
- 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);
93
328
  } catch (error) {
94
329
  const retryTimeout = 5_000; // 5_000ms = 5 seconds
95
330
  log.error(`Failed to fetch chaindata, retrying in ${Math.round(retryTimeout / 1000)} seconds`, error);
96
331
  setTimeout(hydrate, retryTimeout);
97
332
  }
98
333
  };
99
- setChaindataProvider(chaindataProvider);
100
334
  hydrate();
101
335
  return () => {
102
- shouldHydrate = false;
336
+ active = false;
103
337
  };
104
- }, [options.onfinalityApiKey]);
105
- if (chaindataProvider) chaindataProvider.generation = generation;
106
- return chaindataProvider;
338
+ }, [chaindata, type]);
339
+ const subscribe = useMulticastSubscription(createSubscription);
340
+ return subscribe;
107
341
  }
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);
116
- });
117
- }, [chaindata, chaindata === null || chaindata === void 0 ? void 0 : chaindata.generation]);
118
- return chains || {};
119
- }
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;
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;
128
384
  }
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);
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;
137
443
  });
138
- }, [chaindata, chaindata === null || chaindata === void 0 ? void 0 : chaindata.generation]);
139
- 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;
140
449
  }
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;
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;
149
459
  }
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 || {};
460
+ function useChain(chainId, withTestnets) {
461
+ const chains = useChains(withTestnets);
462
+ return chainId ? chains[chainId] : undefined;
161
463
  }
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;
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;
170
477
  }
171
478
 
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;
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
+ }
178
491
 
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 || {};
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;
189
504
  }
190
505
 
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 => {
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 => {
208
528
  // check that this balance is included in our queried balance modules
209
529
  if (!balanceModules.map(({
210
530
  type
@@ -221,137 +541,32 @@ balanceModules, chaindataProvider, addressesByToken, options = {}) {
221
541
 
222
542
  // keep this balance
223
543
  return true;
224
- }).toArray(),
544
+ }),
225
545
  // hydrate balance chains, evmNetworks, tokens and tokenRates
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;
546
+ hydrate), [balances, hydrate, balanceModules, addressesByToken]);
237
547
  }
238
548
 
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
- }
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
+ });
356
571
 
357
- 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 };