@talismn/balances-react 0.0.0-pr557-20230216040942 → 0.0.0-pr563-20230221230003

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,184 @@
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, balances, Balances } from '@talismn/balances';
4
4
  import { useLiveQuery } from 'dexie-react-hooks';
5
- import { useState, useEffect, useRef } from 'react';
6
5
  import { useDebounce } from 'react-use';
7
- import anylogger from 'anylogger';
8
6
  import { ChaindataProviderExtension } from '@talismn/chaindata-provider-extension';
9
7
  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
+ function useTokenRates(tokens) {
58
+ const generation = useRef(0);
59
+ const [tokenRates, setTokenRates] = useState({});
60
+ useEffect(() => {
61
+ if (!tokens) return;
62
+ if (Object.keys(tokens).length < 1) return;
63
+
64
+ // when we make a new request, we want to ignore any old requests which haven't yet completed
65
+ // otherwise we risk replacing the most recent data with older data
66
+ generation.current = (generation.current + 1) % Number.MAX_SAFE_INTEGER;
67
+ const thisGeneration = generation.current;
68
+ fetchTokenRates(tokens).then(tokenRates => {
69
+ if (thisGeneration !== generation.current) return;
70
+ setTokenRates(tokenRates);
71
+ });
72
+ }, [tokens]);
73
+ return tokenRates;
74
+ }
75
+
76
+ const filterNoTestnet = ({
77
+ isTestnet
78
+ }) => isTestnet === false;
79
+ const DEFAULT_VALUE = {
80
+ chainsWithTestnets: [],
81
+ chainsWithoutTestnets: [],
82
+ evmNetworksWithTestnets: [],
83
+ evmNetworksWithoutTestnets: [],
84
+ tokensWithTestnets: [],
85
+ tokensWithoutTestnets: [],
86
+ chainsWithTestnetsMap: {},
87
+ chainsWithoutTestnetsMap: {},
88
+ evmNetworksWithTestnetsMap: {},
89
+ evmNetworksWithoutTestnetsMap: {},
90
+ tokensWithTestnetsMap: {},
91
+ tokensWithoutTestnetsMap: {},
92
+ balances: [],
93
+ tokenRatesMap: {}
94
+ };
95
+ const consolidateDbCache = (chainsMap, evmNetworksMap, tokensMap, allBalances, tokenRates) => {
96
+ if (!chainsMap || !evmNetworksMap || !tokensMap || !allBalances
97
+ // TODO: Store tokenRates in a DB so that we don't have to wait for tokens before we can begin to fetch tokenRates
98
+ /* || !tokenRates */) return DEFAULT_VALUE;
99
+
100
+ // BEGIN: temp hack to indicate that
101
+ // - EVM GLMR is a mirror of substrate GLMR
102
+ // - EVM MOVR is a mirror of substrate MOVR
103
+ // - EVM DEV is a mirror of substrate DEV
104
+ // - EVM ACA is a mirror of substrate ACA
105
+ const mirrorTokenIds = {
106
+ "1284-evm-native-glmr": "moonbeam-substrate-native-glmr",
107
+ "1285-evm-native-movr": "moonriver-substrate-native-movr",
108
+ "1287-evm-native-dev": "moonbase-alpha-testnet-substrate-native-dev",
109
+ "787-evm-native-aca": "acala-substrate-native-aca"
110
+ };
111
+ Object.entries(mirrorTokenIds).filter(([mirrorToken]) => tokensMap[mirrorToken]).forEach(([mirrorToken, mirrorOf]) => tokensMap[mirrorToken].mirrorOf = mirrorOf);
112
+ // END: temp hack
113
+
114
+ const chainsWithTestnets = Object.values(chainsMap);
115
+ const chainsWithoutTestnets = chainsWithTestnets.filter(filterNoTestnet);
116
+ const chainsWithoutTestnetsMap = Object.fromEntries(chainsWithoutTestnets.map(network => [network.id, network]));
117
+ const evmNetworksWithTestnets = Object.values(evmNetworksMap);
118
+ const evmNetworksWithoutTestnets = evmNetworksWithTestnets.filter(filterNoTestnet);
119
+ const evmNetworksWithoutTestnetsMap = Object.fromEntries(evmNetworksWithoutTestnets.map(network => [network.id, network]));
120
+
121
+ // ensure that we have corresponding network for each token
122
+ const tokensWithTestnets = Object.values(tokensMap).filter(token => token.chain && chainsMap[token.chain.id] || token.evmNetwork && evmNetworksMap[token.evmNetwork.id]);
123
+ const tokensWithoutTestnets = tokensWithTestnets.filter(filterNoTestnet).filter(token => token.chain && chainsWithoutTestnetsMap[token.chain.id] || token.evmNetwork && evmNetworksWithoutTestnetsMap[token.evmNetwork.id]);
124
+ const tokensWithTestnetsMap = Object.fromEntries(tokensWithTestnets.map(token => [token.id, token]));
125
+ const tokensWithoutTestnetsMap = Object.fromEntries(tokensWithoutTestnets.map(token => [token.id, token]));
126
+
127
+ // return only balances for which we have a token
128
+ const balances = allBalances.filter(b => tokensWithTestnetsMap[b.tokenId]);
129
+ return {
130
+ chainsWithTestnets,
131
+ chainsWithoutTestnets,
132
+ evmNetworksWithTestnets,
133
+ evmNetworksWithoutTestnets,
134
+ tokensWithTestnets,
135
+ tokensWithoutTestnets,
136
+ chainsWithTestnetsMap: chainsMap,
137
+ chainsWithoutTestnetsMap,
138
+ evmNetworksWithTestnetsMap: evmNetworksMap,
139
+ evmNetworksWithoutTestnetsMap,
140
+ tokensWithTestnetsMap,
141
+ tokensWithoutTestnetsMap,
142
+ balances,
143
+ tokenRatesMap: tokenRates ?? {}
144
+ };
145
+ };
146
+ const useDbCacheProvider = ({
147
+ useTestnets = false
148
+ }) => {
149
+ const chaindataProvider = useChaindata();
150
+ const chainList = useLiveQuery(() => chaindataProvider?.chains(), [chaindataProvider]);
151
+ const evmNetworkList = useLiveQuery(() => chaindataProvider?.evmNetworks(), [chaindataProvider]);
152
+ const tokenList = useLiveQuery(() => chaindataProvider?.tokens(), [chaindataProvider]);
153
+ const rawBalances = useLiveQuery(() => db.balances.toArray(), []);
154
+
155
+ // TODO: Store in a DB so that we don't have to wait for tokens before we can begin to fetch tokenRates
156
+ const tokenRates = useTokenRates();
157
+ const [dbData, setDbData] = useState(DEFAULT_VALUE);
158
+
159
+ // debounce every 500ms to prevent hammering UI with updates
160
+ useDebounce(() => {
161
+ setDbData(consolidateDbCache(chainList, evmNetworkList, tokenList, rawBalances, tokenRates));
162
+ }, 500, [chainList, evmNetworkList, tokenList, rawBalances, tokenRates, useTestnets]);
163
+ const refInitialized = useRef(false);
164
+
165
+ // force an update as soon as all datasources are fetched, so UI can display data ASAP
166
+ useEffect(() => {
167
+ if (!refInitialized.current && chainList && evmNetworkList && tokenList && rawBalances
168
+ // TODO: Store tokenRates in a DB so that we don't have to wait for tokens before we can begin to fetch tokenRates
169
+ // && tokenRates
170
+ ) {
171
+ setDbData(consolidateDbCache(chainList, evmNetworkList, tokenList, rawBalances, tokenRates));
172
+ refInitialized.current = true;
173
+ }
174
+ }, [chainList, evmNetworkList, rawBalances, tokenList, tokenRates, useTestnets]);
175
+ return dbData;
176
+ };
177
+ const [DbCacheProvider, useDbCache] = provideContext(useDbCacheProvider);
10
178
 
11
179
  var packageJson = {
12
180
  name: "@talismn/balances-react",
13
- version: "0.0.0-pr557-20230216040942",
181
+ version: "0.0.0-pr563-20230221230003",
14
182
  author: "Talisman",
15
183
  homepage: "https://talisman.xyz",
16
184
  license: "UNLICENSED",
@@ -45,7 +213,8 @@ var packageJson = {
45
213
  anylogger: "^1.0.11",
46
214
  dexie: "^3.2.2",
47
215
  "dexie-react-hooks": "^1.1.1",
48
- "react-use": "^17.4.0"
216
+ "react-use": "^17.4.0",
217
+ rxjs: "^7.8.0"
49
218
  },
50
219
  devDependencies: {
51
220
  "@talismn/eslint-config": "workspace:^",
@@ -72,139 +241,259 @@ var packageJson = {
72
241
 
73
242
  var log = anylogger(packageJson.name);
74
243
 
75
- // TODO: Allow user to call useChaindata from multiple places
76
- function useChaindata(options = {}) {
77
- const [chaindataProvider, setChaindataProvider] = useState(null);
244
+ /**
245
+ * Creates a subscription function that can be used to subscribe to a multicast observable created from an upstream source.
246
+ *
247
+ * An example of when this is useful is when we want to subscribe to some data from multiple components, but we only want
248
+ * to actively keep that data hydrated when at least one component is subscribed to it.
249
+ *
250
+ * When the first component subscribes, the `upstream` function will be called. It should then set up a subscription and return a teardown function.
251
+ * When subsequent components subscribe, they will be added to the existing subscription.
252
+ * When the last component unsubscribes, the teardown function returned from the `upstream` function will be called.
253
+ *
254
+ * @param upstream A function that takes a "next" callback function as an argument, and returns either an unsubscribe function or void.
255
+ * @returns A subscription function that can be used to subscribe to the multicast observable.
256
+ */
257
+ const useMulticastSubscription = upstream => {
258
+ const subscribe = useMemo(() => createMulticastSubscription(upstream), [upstream]);
259
+ return subscribe;
260
+ };
261
+ const createMulticastSubscription = upstream => {
262
+ // Create an upstream observable using the provided function.
263
+ const upstreamObservable = new Observable(subscriber => {
264
+ const unsubscribe = upstream(val => subscriber.next(val));
265
+ return () => {
266
+ typeof unsubscribe === "function" && unsubscribe();
267
+ };
268
+ });
269
+
270
+ // Create a multicast observable from the upstream observable, using the shareReplay operator.
271
+ const multicastObservable = defer(() => upstreamObservable).pipe(shareReplay({
272
+ bufferSize: 1,
273
+ refCount: true
274
+ }));
275
+
276
+ // Create a subscription function that subscribes to the multicast observable and returns an unsubscribe function.
277
+ const subscribe = callback => {
278
+ const subscription = multicastObservable.subscribe(callback);
279
+ const unsubscribe = () => subscription.unsubscribe();
280
+ return unsubscribe;
281
+ };
282
+ return subscribe;
283
+ };
284
+
285
+ function useChainConnectorsProvider(options) {
286
+ // chaindata dependency
287
+ const chaindata = useChaindata();
288
+
289
+ // substrate connector
290
+ const [substrate, setSubstrate] = useState();
291
+ useEffect(() => {
292
+ if (!chaindata) return;
293
+ setSubstrate(new ChainConnector(chaindata));
294
+ }, [chaindata]);
78
295
 
79
- // this number is incremented each time the chaindataProvider has fetched new data
80
- const [generation, setGeneration] = useState(0);
296
+ // evm connector
297
+ const [evm, setEvm] = useState();
81
298
  useEffect(() => {
82
- const chaindataProvider = new ChaindataProviderExtension({
299
+ if (!chaindata) return;
300
+ setEvm(new ChainConnectorEvm(chaindata, {
83
301
  onfinalityApiKey: options.onfinalityApiKey
84
- });
85
- let shouldHydrate = true;
86
- const timer = 300_000; // 300_000ms = 300s = 5 minutes
302
+ }));
303
+ }, [chaindata, options.onfinalityApiKey]);
304
+ return useMemo(() => ({
305
+ substrate,
306
+ evm
307
+ }), [substrate, evm]);
308
+ }
309
+ const [ChainConnectorsProvider, useChainConnectors] = provideContext(useChainConnectorsProvider);
310
+
311
+ const useSubscriptionsProvider = () => [useSubscribeChaindataHydrate("chains"), useSubscribeChaindataHydrate("evmNetworks"), useSubscribeChaindataHydrate("tokens"), useSubscribeBalances()];
312
+ const [SubscriptionsProvider, useSubscriptions] = provideContext(useSubscriptionsProvider);
313
+
314
+ /**
315
+ * This hook is responsible for fetching the data used for balances and inserting it into the db.
316
+ */
317
+ const useDbCacheSubscription = subscribeTo => {
318
+ const [subscribeHydrateChains, subscribeHydrateEvmNetworks, subscribeHydrateTokens, subscribeBalances] = useSubscriptions();
319
+ useEffect(() => {
320
+ switch (subscribeTo) {
321
+ case "chains":
322
+ return subscribeHydrateChains();
323
+ case "evmNetworks":
324
+ return subscribeHydrateEvmNetworks();
325
+ case "tokens":
326
+ return subscribeHydrateTokens();
327
+ case "balances":
328
+ return subscribeBalances();
329
+ }
330
+ }, [subscribeBalances, subscribeHydrateChains, subscribeHydrateEvmNetworks, subscribeHydrateTokens, subscribeTo]);
331
+ };
332
+ function useSubscribeChaindataHydrate(type) {
333
+ const chaindata =
334
+ // cheeky hack to give us access to the hydrate methods
335
+ useChaindata();
336
+ const createSubscription = useCallback(() => {
337
+ if (!chaindata) return;
338
+ let active = true;
339
+ const interval = 300_000; // 300_000ms = 300s = 5 minutes
340
+
87
341
  const hydrate = async () => {
88
- if (!shouldHydrate) return;
342
+ if (!active) return;
89
343
  try {
90
- const updated = await chaindataProvider.hydrate();
91
- if (updated) setGeneration(generation => (generation + 1) % Number.MAX_SAFE_INTEGER);
92
- setTimeout(hydrate, timer);
344
+ if (type === "chains") await chaindata.hydrateChains();
345
+ if (type === "evmNetworks") await chaindata.hydrateEvmNetworks();
346
+ if (type === "tokens") await chaindata.hydrateTokens();
347
+ setTimeout(hydrate, interval);
93
348
  } catch (error) {
94
349
  const retryTimeout = 5_000; // 5_000ms = 5 seconds
95
350
  log.error(`Failed to fetch chaindata, retrying in ${Math.round(retryTimeout / 1000)} seconds`, error);
96
351
  setTimeout(hydrate, retryTimeout);
97
352
  }
98
353
  };
99
- setChaindataProvider(chaindataProvider);
100
354
  hydrate();
101
355
  return () => {
102
- shouldHydrate = false;
356
+ active = false;
103
357
  };
104
- }, [options.onfinalityApiKey]);
105
- if (chaindataProvider) chaindataProvider.generation = generation;
106
- return chaindataProvider;
358
+ }, [chaindata, type]);
359
+ const subscribe = useMulticastSubscription(createSubscription);
360
+ return subscribe;
107
361
  }
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);
362
+ function useSubscribeBalances() {
363
+ const balanceModules = useBalanceModules();
364
+ const chaindataProvider = useChaindata();
365
+ const chainConnectors = useChainConnectors();
366
+ const [allAddresses] = useAllAddresses();
367
+ const tokens = useLiveQuery(() => chaindataProvider?.tokens(), [chaindataProvider]);
368
+ const tokenIds = useMemo(() => Object.values(tokens ?? {}).map(({
369
+ id
370
+ }) => id), [tokens]);
371
+ const addressesByToken = useMemo(() => Object.fromEntries(tokenIds.map(tokenId => [tokenId, allAddresses])), [allAddresses, tokenIds]);
372
+ const generationRef = useRef(0);
373
+ const createSubscription = useCallback(() => {
374
+ if (!chainConnectors.substrate) return;
375
+ if (!chainConnectors.evm) return;
376
+ if (!chaindataProvider) return;
377
+ const generation = (generationRef.current + 1) % Number.MAX_SAFE_INTEGER;
378
+ generationRef.current = generation;
379
+ const unsubs = balanceModules.map(balanceModule => {
380
+ // filter out tokens to only include those which this module knows how to fetch balances for
381
+ const moduleTokenIds = Object.values(tokens ?? {}).filter(({
382
+ type
383
+ }) => type === balanceModule.type).map(({
384
+ id
385
+ }) => id);
386
+ const addressesByModuleToken = Object.fromEntries(Object.entries(addressesByToken).filter(([tokenId]) => moduleTokenIds.includes(tokenId)));
387
+ const subscribe = createMulticastSubscription(next => {
388
+ const unsub = balances(balanceModule, chainConnectors, chaindataProvider, addressesByModuleToken, (error, balances) => {
389
+ // log errors
390
+ if (error) return log.error(`Failed to fetch ${balanceModule.type} balances`, error);
391
+ // ignore empty balance responses
392
+ if (!balances) return;
393
+ // ignore balances from old subscriptions which are still in the process of unsubscribing
394
+ if (generationRef.current !== generation) return;
395
+ next(balances);
396
+ });
397
+ return () => {
398
+ // unsubscribe from upstream
399
+ unsub.then(unsubscribe => unsubscribe());
400
+
401
+ // set this subscription's balances in the store to status: cache
402
+ db.transaction("rw", db.balances, async () => await db.balances.filter(balance => {
403
+ if (balance.source !== balanceModule.type) return false;
404
+ if (!Object.keys(addressesByModuleToken).includes(balance.tokenId)) return false;
405
+ if (!addressesByModuleToken[balance.tokenId].includes(balance.address)) return false;
406
+ return true;
407
+ }).modify({
408
+ status: "cache"
409
+ }));
410
+ };
411
+ });
412
+ const unsubscribe = subscribe(balances => {
413
+ const putBalances = Object.entries(balances.toJSON()).map(([id, balance]) => ({
414
+ id,
415
+ ...balance
416
+ }));
417
+ db.transaction("rw", db.balances, async () => await db.balances.bulkPut(putBalances));
418
+ });
419
+ return unsubscribe;
116
420
  });
117
- }, [chaindata, chaindata === null || chaindata === void 0 ? void 0 : chaindata.generation]);
118
- return chains || {};
421
+ const unsubscribeAll = () => unsubs.forEach(unsub => unsub());
422
+ return unsubscribeAll;
423
+ }, [addressesByToken, balanceModules, chainConnectors, chaindataProvider, tokens]);
424
+ const subscribe = useMulticastSubscription(createSubscription);
425
+ return subscribe;
119
426
  }
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;
427
+
428
+ function useChains(withTestnets) {
429
+ // keep db data up to date
430
+ useDbCacheSubscription("chains");
431
+ const {
432
+ chainsWithTestnetsMap,
433
+ chainsWithoutTestnetsMap
434
+ } = useDbCache();
435
+ return withTestnets ? chainsWithTestnetsMap : chainsWithoutTestnetsMap;
128
436
  }
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 || {};
437
+ function useChain(chainId, withTestnets) {
438
+ const chains = useChains(withTestnets);
439
+ return chainId ? chains[chainId] : undefined;
140
440
  }
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;
441
+
442
+ function useEvmNetworks(withTestnets) {
443
+ // keep db data up to date
444
+ useDbCacheSubscription("evmNetworks");
445
+ const {
446
+ evmNetworksWithTestnetsMap,
447
+ evmNetworksWithoutTestnetsMap
448
+ } = useDbCache();
449
+ return withTestnets ? evmNetworksWithTestnetsMap : evmNetworksWithoutTestnetsMap;
149
450
  }
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 || {};
451
+ function useEvmNetwork(evmNetworkId, withTestnets) {
452
+ const evmNetworks = useEvmNetworks(withTestnets);
453
+ return evmNetworkId ? evmNetworks[evmNetworkId] : undefined;
161
454
  }
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;
455
+
456
+ function useTokens(withTestnets) {
457
+ // keep db data up to date
458
+ useDbCacheSubscription("tokens");
459
+ const {
460
+ tokensWithTestnetsMap,
461
+ tokensWithoutTestnetsMap
462
+ } = useDbCache();
463
+ return withTestnets ? tokensWithTestnetsMap : tokensWithoutTestnetsMap;
464
+ }
465
+ function useToken(tokenId, withTestnets) {
466
+ const tokens = useTokens(withTestnets);
467
+ return tokenId ? tokens[tokenId] : undefined;
170
468
  }
171
469
 
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;
470
+ const useBalancesHydrate = withTestnets => {
471
+ const chains = useChains(withTestnets);
472
+ const evmNetworks = useEvmNetworks(withTestnets);
473
+ const tokens = useTokens(withTestnets);
178
474
 
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
- }
475
+ // TODO: Store in a DB so that we don't have to wait for tokens before we can begin to fetch tokenRates
476
+ // useDbCacheSubscription("tokenRates")
477
+ const {
478
+ tokenRatesMap: tokenRates
479
+ } = useDbCache();
480
+ return useMemo(() => ({
481
+ chains,
482
+ evmNetworks,
483
+ tokens,
484
+ tokenRates
485
+ }), [chains, evmNetworks, tokens, tokenRates]);
486
+ };
190
487
 
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 => {
488
+ function useBalances(addressesByToken) {
489
+ // keep db data up to date
490
+ useDbCacheSubscription("balances");
491
+ const balanceModules = useBalanceModules();
492
+ const {
493
+ balances
494
+ } = useDbCache();
495
+ const hydrate = useBalancesHydrate();
496
+ return useMemo(() => new Balances(balances.filter(balance => {
208
497
  // check that this balance is included in our queried balance modules
209
498
  if (!balanceModules.map(({
210
499
  type
@@ -221,137 +510,32 @@ balanceModules, chaindataProvider, addressesByToken, options = {}) {
221
510
 
222
511
  // keep this balance
223
512
  return true;
224
- }).toArray(),
513
+ }),
225
514
  // 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;
237
- }
238
-
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]);
515
+ hydrate), [balances, hydrate, balanceModules, addressesByToken]);
334
516
  }
335
517
 
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
- }
518
+ const BalancesProvider = ({
519
+ balanceModules,
520
+ onfinalityApiKey,
521
+ useTestnets,
522
+ children
523
+ }) => /*#__PURE__*/jsx(ChaindataProvider, {
524
+ onfinalityApiKey: onfinalityApiKey,
525
+ children: /*#__PURE__*/jsx(ChainConnectorsProvider, {
526
+ onfinalityApiKey: onfinalityApiKey,
527
+ children: /*#__PURE__*/jsx(AllAddressesProvider, {
528
+ children: /*#__PURE__*/jsx(BalanceModulesProvider, {
529
+ balanceModules: balanceModules,
530
+ children: /*#__PURE__*/jsx(DbCacheProvider, {
531
+ useTestnets: useTestnets,
532
+ children: /*#__PURE__*/jsx(SubscriptionsProvider, {
533
+ children: children
534
+ })
535
+ })
536
+ })
537
+ })
538
+ })
539
+ });
356
540
 
357
- export { useBalances, useChain, useChaindata, useChains, useEvmNetwork, useEvmNetworks, useToken, useTokenRates, useTokens };
541
+ 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, useTokenRates, useTokens };