@talismn/balances-react 0.0.0-pr595-20230306020834 → 0.0.0-pr596-20230306035406

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,163 @@
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, useMemo, useRef, 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 md5 from 'blueimp-md5';
9
+ import anylogger from 'anylogger';
10
+ import { Subject, Observable, defer, shareReplay } from 'rxjs';
11
+ import { ChainConnector } from '@talismn/chain-connector';
12
+ import { ChainConnectorEvm } from '@talismn/chain-connector-evm';
13
+
14
+ const provideContext = useProviderContext => {
15
+ // automatic typing based on our hook's return type
16
+
17
+ const Context = /*#__PURE__*/createContext({
18
+ __provideContextInternalDefaultValue: true
19
+ });
20
+ const Provider = ({
21
+ children,
22
+ ...props
23
+ }) => {
24
+ const ctx = useProviderContext(props);
25
+ return /*#__PURE__*/jsx(Context.Provider, {
26
+ value: ctx,
27
+ children: children
28
+ });
29
+ };
30
+ const useProvidedContext = () => {
31
+ const context = useContext(Context);
32
+ if (typeof context === "object" && context && "__provideContextInternalDefaultValue" in context) throw new Error("This hook requires a provider to be present above it in the tree");
33
+ return context;
34
+ };
35
+ const result = [Provider, useProvidedContext];
36
+ return result;
37
+ };
38
+
39
+ const useAllAddressesProvider = () => useState([]);
40
+ const [AllAddressesProvider, useAllAddresses] = provideContext(useAllAddressesProvider);
41
+
42
+ const useBalanceModulesProvider = ({
43
+ balanceModules
44
+ }) => balanceModules;
45
+ const [BalanceModulesProvider, useBalanceModules] = provideContext(useBalanceModulesProvider);
46
+
47
+ function useChaindataProvider(options = {}) {
48
+ const [onfinalityApiKey, setOnfinalityApiKey] = useState(options.onfinalityApiKey);
49
+
50
+ // make sure we recreate provider only when the onfinalityApiKey changes
51
+ useEffect(() => {
52
+ if (options.onfinalityApiKey !== onfinalityApiKey) setOnfinalityApiKey(options.onfinalityApiKey);
53
+ }, [options.onfinalityApiKey, onfinalityApiKey]);
54
+ return useMemo(() => new ChaindataProviderExtension({
55
+ onfinalityApiKey
56
+ }), [onfinalityApiKey]);
57
+ }
58
+ const [ChaindataProvider, useChaindata] = provideContext(useChaindataProvider);
59
+
60
+ const filterNoTestnet = ({
61
+ isTestnet
62
+ }) => isTestnet === false;
63
+ const DEFAULT_VALUE = {
64
+ chainsWithTestnets: [],
65
+ chainsWithoutTestnets: [],
66
+ evmNetworksWithTestnets: [],
67
+ evmNetworksWithoutTestnets: [],
68
+ tokensWithTestnets: [],
69
+ tokensWithoutTestnets: [],
70
+ chainsWithTestnetsMap: {},
71
+ chainsWithoutTestnetsMap: {},
72
+ evmNetworksWithTestnetsMap: {},
73
+ evmNetworksWithoutTestnetsMap: {},
74
+ tokensWithTestnetsMap: {},
75
+ tokensWithoutTestnetsMap: {},
76
+ tokenRatesMap: {},
77
+ balances: []
78
+ };
79
+ const consolidateDbCache = (chainsMap, evmNetworksMap, tokensMap, tokenRates, allBalances) => {
80
+ if (!chainsMap || !evmNetworksMap || !tokensMap || !tokenRates || !allBalances) return DEFAULT_VALUE;
81
+
82
+ // BEGIN: temp hack to indicate that
83
+ // - EVM GLMR is a mirror of substrate GLMR
84
+ // - EVM MOVR is a mirror of substrate MOVR
85
+ // - EVM DEV is a mirror of substrate DEV
86
+ // - EVM ACA is a mirror of substrate ACA
87
+ const mirrorTokenIds = {
88
+ "1284-evm-native-glmr": "moonbeam-substrate-native-glmr",
89
+ "1285-evm-native-movr": "moonriver-substrate-native-movr",
90
+ "1287-evm-native-dev": "moonbase-alpha-testnet-substrate-native-dev",
91
+ "787-evm-native-aca": "acala-substrate-native-aca"
92
+ };
93
+ Object.entries(mirrorTokenIds).filter(([mirrorToken]) => tokensMap[mirrorToken]).forEach(([mirrorToken, mirrorOf]) => tokensMap[mirrorToken].mirrorOf = mirrorOf);
94
+ // END: temp hack
95
+
96
+ const chainsWithTestnets = Object.values(chainsMap);
97
+ const chainsWithoutTestnets = chainsWithTestnets.filter(filterNoTestnet);
98
+ const chainsWithoutTestnetsMap = Object.fromEntries(chainsWithoutTestnets.map(network => [network.id, network]));
99
+ const evmNetworksWithTestnets = Object.values(evmNetworksMap);
100
+ const evmNetworksWithoutTestnets = evmNetworksWithTestnets.filter(filterNoTestnet);
101
+ const evmNetworksWithoutTestnetsMap = Object.fromEntries(evmNetworksWithoutTestnets.map(network => [network.id, network]));
102
+
103
+ // ensure that we have corresponding network for each token
104
+ const tokensWithTestnets = Object.values(tokensMap).filter(token => token.chain && chainsMap[token.chain.id] || token.evmNetwork && evmNetworksMap[token.evmNetwork.id]);
105
+ const tokensWithoutTestnets = tokensWithTestnets.filter(filterNoTestnet).filter(token => token.chain && chainsWithoutTestnetsMap[token.chain.id] || token.evmNetwork && evmNetworksWithoutTestnetsMap[token.evmNetwork.id]);
106
+ const tokensWithTestnetsMap = Object.fromEntries(tokensWithTestnets.map(token => [token.id, token]));
107
+ const tokensWithoutTestnetsMap = Object.fromEntries(tokensWithoutTestnets.map(token => [token.id, token]));
108
+ const tokenRatesMap = Object.fromEntries(tokenRates.map(({
109
+ tokenId,
110
+ rates
111
+ }) => [tokenId, rates]));
112
+
113
+ // return only balances for which we have a token
114
+ const balances = allBalances.filter(b => tokensWithTestnetsMap[b.tokenId]);
115
+ return {
116
+ chainsWithTestnets,
117
+ chainsWithoutTestnets,
118
+ evmNetworksWithTestnets,
119
+ evmNetworksWithoutTestnets,
120
+ tokensWithTestnets,
121
+ tokensWithoutTestnets,
122
+ chainsWithTestnetsMap: chainsMap,
123
+ chainsWithoutTestnetsMap,
124
+ evmNetworksWithTestnetsMap: evmNetworksMap,
125
+ evmNetworksWithoutTestnetsMap,
126
+ tokensWithTestnetsMap,
127
+ tokensWithoutTestnetsMap,
128
+ tokenRatesMap,
129
+ balances
130
+ };
131
+ };
132
+ const useDbCacheProvider = () => {
133
+ const chaindataProvider = useChaindata();
134
+ const chainList = useLiveQuery(() => chaindataProvider === null || chaindataProvider === void 0 ? void 0 : chaindataProvider.chains(), [chaindataProvider]);
135
+ const evmNetworkList = useLiveQuery(() => chaindataProvider === null || chaindataProvider === void 0 ? void 0 : chaindataProvider.evmNetworks(), [chaindataProvider]);
136
+ const tokenList = useLiveQuery(() => chaindataProvider === null || chaindataProvider === void 0 ? void 0 : chaindataProvider.tokens(), [chaindataProvider]);
137
+ const tokenRates = useLiveQuery(() => db.tokenRates.toArray(), []);
138
+ const rawBalances = useLiveQuery(() => db$1.balances.toArray(), []);
139
+ const [dbData, setDbData] = useState(DEFAULT_VALUE);
140
+
141
+ // debounce every 500ms to prevent hammering UI with updates
142
+ useDebounce(() => {
143
+ setDbData(consolidateDbCache(chainList, evmNetworkList, tokenList, tokenRates, rawBalances));
144
+ }, 500, [chainList, evmNetworkList, tokenList, rawBalances, tokenRates]);
145
+ const refInitialized = useRef(false);
146
+
147
+ // force an update as soon as all datasources are fetched, so UI can display data ASAP
148
+ useEffect(() => {
149
+ if (!refInitialized.current && chainList && evmNetworkList && tokenList && tokenRates && rawBalances) {
150
+ setDbData(consolidateDbCache(chainList, evmNetworkList, tokenList, tokenRates, rawBalances));
151
+ refInitialized.current = true;
152
+ }
153
+ }, [chainList, evmNetworkList, rawBalances, tokenList, tokenRates]);
154
+ return dbData;
155
+ };
156
+ const [DbCacheProvider, useDbCache] = provideContext(useDbCacheProvider);
10
157
 
11
158
  var packageJson = {
12
159
  name: "@talismn/balances-react",
13
- version: "0.0.0-pr595-20230306020834",
160
+ version: "0.0.0-pr596-20230306035406",
14
161
  author: "Talisman",
15
162
  homepage: "https://talisman.xyz",
16
163
  license: "UNLICENSED",
@@ -43,9 +190,11 @@ var packageJson = {
43
190
  "@talismn/chaindata-provider-extension": "workspace:^",
44
191
  "@talismn/token-rates": "workspace:^",
45
192
  anylogger: "^1.0.11",
193
+ "blueimp-md5": "2.19.0",
46
194
  dexie: "^3.2.3",
47
195
  "dexie-react-hooks": "^1.1.1",
48
- "react-use": "^17.4.0"
196
+ "react-use": "^17.4.0",
197
+ rxjs: "^7.8.0"
49
198
  },
50
199
  devDependencies: {
51
200
  "@talismn/eslint-config": "workspace:^",
@@ -72,286 +221,432 @@ var packageJson = {
72
221
 
73
222
  var log = anylogger(packageJson.name);
74
223
 
75
- // TODO: Allow user to call useChaindata from multiple places
76
- function useChaindata(options = {}) {
77
- const [chaindataProvider, setChaindataProvider] = useState(null);
224
+ // global data store containing all subscriptions
225
+ const subscriptions = {};
78
226
 
79
- // this number is incremented each time the chaindataProvider has fetched new data
80
- const [generation, setGeneration] = useState(0);
227
+ /**
228
+ * This hook ensures a subscription is created only once, and unsubscribe automatically as soon as there is no consumer to the hook
229
+ * @param key key that is unique to the subscription's parameters
230
+ * @param subscribe // subscribe function that will be shared by all consumers of the key
231
+ */
232
+ const useSharedSubscription = (key, subscribe) => {
233
+ // create the rxJS subject if it doesn't exist
234
+ if (!subscriptions[key]) subscriptions[key] = {
235
+ subject: new Subject()
236
+ };
81
237
  useEffect(() => {
82
- const chaindataProvider = new ChaindataProviderExtension({
83
- onfinalityApiKey: options.onfinalityApiKey
84
- });
85
- let shouldHydrate = true;
86
- const timer = 300_000; // 300_000ms = 300s = 5 minutes
87
- const hydrate = async () => {
88
- if (!shouldHydrate) return;
89
- try {
90
- const updated = await chaindataProvider.hydrate();
91
- if (updated) setGeneration(generation => (generation + 1) % Number.MAX_SAFE_INTEGER);
92
- setTimeout(hydrate, timer);
93
- } catch (error) {
94
- const retryTimeout = 5_000; // 5_000ms = 5 seconds
95
- log.error(`Failed to fetch chaindata, retrying in ${Math.round(retryTimeout / 1000)} seconds`, error);
96
- setTimeout(hydrate, retryTimeout);
97
- }
98
- };
99
- setChaindataProvider(chaindataProvider);
100
- hydrate();
238
+ // subscribe to subject.
239
+ // it won't change but we need to count subscribers, to unsubscribe main subscription when no more observers
240
+ const s = subscriptions[key].subject.subscribe();
101
241
  return () => {
102
- shouldHydrate = false;
242
+ // unsubscribe from our local observable updates to prevent memory leaks
243
+ s.unsubscribe();
244
+ const {
245
+ subject,
246
+ unsubscribe
247
+ } = subscriptions[key];
248
+ if (!subject.observed && unsubscribe) {
249
+ log.debug(`[useSharedSubscription] unsubscribing ${key}`);
250
+
251
+ // unsubscribe from backend updates to prevent unnecessary network connections
252
+ unsubscribe();
253
+ delete subscriptions[key].unsubscribe;
254
+ }
103
255
  };
104
- }, [options.onfinalityApiKey]);
105
- if (chaindataProvider) chaindataProvider.generation = generation;
106
- return chaindataProvider;
107
- }
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;
128
- }
129
- function useEvmNetworks(chaindata) {
130
- const [evmNetworks, setEvmNetworks] = useState();
256
+ }, [key]);
257
+
258
+ // Initialize subscription
131
259
  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 || {};
140
- }
141
- function useEvmNetwork(chaindata, evmNetworkId) {
142
- const [evmNetwork, setEvmNetwork] = useState();
260
+ const {
261
+ unsubscribe
262
+ } = subscriptions[key];
263
+ // launch the subscription if it's a new key
264
+ if (!unsubscribe) {
265
+ const cb = subscribe();
266
+ log.debug(`[useSharedSubscription] subscribing ${key}`);
267
+ if (cb) subscriptions[key].unsubscribe = cb;
268
+ // this error should only happen when developping a new hook, let it bubble up
269
+ else throw new Error(`${key} subscribe did not return an unsubscribe callback`);
270
+ }
271
+ }, [key, subscribe]);
272
+ };
273
+
274
+ function useChainConnectorsProvider(options) {
275
+ const [onfinalityApiKey, setOnfinalityApiKey] = useState(options.onfinalityApiKey);
276
+
277
+ // make sure we recreate provider only when the onfinalityApiKey changes
143
278
  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;
279
+ if (options.onfinalityApiKey !== onfinalityApiKey) setOnfinalityApiKey(options.onfinalityApiKey);
280
+ }, [options.onfinalityApiKey, onfinalityApiKey]);
281
+
282
+ // chaindata dependency
283
+ const chaindata = useChaindata();
284
+
285
+ // substrate connector
286
+ const substrate = useMemo(() => new ChainConnector(chaindata), [chaindata]);
287
+
288
+ // evm connector
289
+ const evm = useMemo(() => new ChainConnectorEvm(chaindata, {
290
+ onfinalityApiKey
291
+ }), [chaindata, onfinalityApiKey]);
292
+ return useMemo(() => ({
293
+ substrate,
294
+ evm
295
+ }), [substrate, evm]);
149
296
  }
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 || {};
297
+ const [ChainConnectorsProvider, useChainConnectors] = provideContext(useChainConnectorsProvider);
298
+
299
+ function useTokens(withTestnets) {
300
+ // keep db data up to date
301
+ useDbCacheSubscription("tokens");
302
+ const {
303
+ tokensWithTestnetsMap,
304
+ tokensWithoutTestnetsMap
305
+ } = useDbCache();
306
+ return withTestnets ? tokensWithTestnetsMap : tokensWithoutTestnetsMap;
161
307
  }
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;
308
+ function useToken(tokenId, withTestnets) {
309
+ const tokens = useTokens(withTestnets);
310
+ return tokenId ? tokens[tokenId] : undefined;
170
311
  }
171
312
 
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;
178
-
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
- }
313
+ /**
314
+ * Creates a subscription function that can be used to subscribe to a multicast observable created from an upstream source.
315
+ *
316
+ * An example of when this is useful is when we want to subscribe to some data from multiple components, but we only want
317
+ * to actively keep that data hydrated when at least one component is subscribed to it.
318
+ *
319
+ * When the first component subscribes, the `upstream` function will be called. It should then set up a subscription and return a teardown function.
320
+ * When subsequent components subscribe, they will be added to the existing subscription.
321
+ * When the last component unsubscribes, the teardown function returned from the `upstream` function will be called.
322
+ *
323
+ * @param upstream A function that takes a "next" callback function as an argument, and returns either an unsubscribe function or void.
324
+ * @returns A subscription function that can be used to subscribe to the multicast observable.
325
+ */
326
+ const useMulticastSubscription = upstream => {
327
+ const subscribe = useMemo(() => createMulticastSubscription(upstream), [upstream]);
328
+ return subscribe;
329
+ };
330
+ const createMulticastSubscription = upstream => {
331
+ // Create an upstream observable using the provided function.
332
+ const upstreamObservable = new Observable(subscriber => {
333
+ const unsubscribe = upstream(val => subscriber.next(val));
334
+ return () => {
335
+ typeof unsubscribe === "function" && unsubscribe();
336
+ };
337
+ });
190
338
 
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 => {
208
- // check that this balance is included in our queried balance modules
209
- if (!balanceModules.map(({
210
- type
211
- }) => type).includes(balance.source)) return false;
339
+ // Create a multicast observable from the upstream observable, using the shareReplay operator.
340
+ const multicastObservable = defer(() => upstreamObservable).pipe(shareReplay({
341
+ bufferSize: 1,
342
+ refCount: true
343
+ }));
212
344
 
213
- // check that our query includes some tokens and addresses
214
- if (!addressesByToken) return false;
345
+ // Create a subscription function that subscribes to the multicast observable and returns an unsubscribe function.
346
+ const subscribe = callback => {
347
+ const subscription = multicastObservable.subscribe(callback);
348
+ const unsubscribe = () => subscription.unsubscribe();
349
+ return unsubscribe;
350
+ };
351
+ return subscribe;
352
+ };
215
353
 
216
- // check that this balance is included in our queried tokens
217
- if (!Object.keys(addressesByToken).includes(balance.tokenId)) return false;
354
+ const useWithTestnetsProvider = ({
355
+ withTestnets
356
+ }) => {
357
+ return {
358
+ withTestnets
359
+ };
360
+ };
361
+ const [WithTestnetsProvider, useWithTestnets] = provideContext(useWithTestnetsProvider);
218
362
 
219
- // check that this balance is included in our queried addresses for this token
220
- if (!addressesByToken[balance.tokenId].includes(balance.address)) return false;
363
+ /**
364
+ * This hook is responsible for fetching the data used for balances and inserting it into the db.
365
+ */
366
+ const useDbCacheSubscription = subscribeTo => {
367
+ const provider = useChaindata();
221
368
 
222
- // keep this balance
223
- return true;
224
- }).toArray(),
225
- // hydrate balance chains, evmNetworks, tokens and tokenRates
226
- {
227
- chains,
228
- evmNetworks,
229
- tokens,
230
- tokenRates
231
- }), [balanceModules, addressesByToken, chains, evmNetworks, tokens, tokenRates]);
369
+ // can't handle balances & tokenRates here as they have other dependencies, it would trigger to many subscriptions
370
+ const subscribe = useCallback(() => {
371
+ switch (subscribeTo) {
372
+ case "chains":
373
+ return subscribeChainDataHydrate(provider, "chains");
374
+ case "evmNetworks":
375
+ return subscribeChainDataHydrate(provider, "evmNetworks");
376
+ case "tokens":
377
+ return subscribeChainDataHydrate(provider, "tokens");
378
+ }
379
+ }, [provider, subscribeTo]);
380
+ useSharedSubscription(subscribeTo, subscribe);
381
+ };
232
382
 
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;
383
+ /**
384
+ * This hook is responsible for fetching the data used for token rates and inserting it into the db.
385
+ */
386
+ function useDbCacheTokenRatesSubscription() {
387
+ const {
388
+ withTestnets
389
+ } = useWithTestnets();
390
+ const tokens = useTokens(withTestnets);
391
+ const subscriptionKey = useMemo(
392
+ // not super sexy but we need key to change based on this stuff
393
+ () => {
394
+ const key = Object.values(tokens ?? {}).map(({
395
+ id
396
+ }) => id).sort().join();
397
+ return `tokenRates-${md5(key)}`;
398
+ }, [tokens]);
399
+ const subscription = useCallback(() => {
400
+ if (!Object.values(tokens ?? {}).length) return () => {};
401
+ return subscribeTokenRates(tokens);
402
+ }, [tokens]);
403
+ useSharedSubscription(subscriptionKey, subscription);
237
404
  }
238
405
 
239
- // TODO: Turn into react context
240
- const subscriptions = {};
406
+ /**
407
+ * This hook is responsible for fetching the data used for balances and inserting it into the db.
408
+ */
409
+ function useDbCacheBalancesSubscription() {
410
+ const {
411
+ withTestnets
412
+ } = useWithTestnets();
413
+ const balanceModules = useBalanceModules();
414
+ const chaindataProvider = useChaindata();
415
+ const chainConnectors = useChainConnectors();
416
+ const [allAddresses] = useAllAddresses();
417
+ const tokens = useTokens(withTestnets);
418
+ const subscriptionKey = useMemo(
419
+ // not super sexy but we need key to change based on this stuff
420
+ () => {
421
+ const key = allAddresses.sort().join().concat(...Object.values(tokens ?? {}).map(({
422
+ id
423
+ }) => id).sort()).concat(`evm:${!!chainConnectors.evm}`, `sub:${!!chainConnectors.substrate}`, ...balanceModules.map(m => m.type).sort(), `cd:${!!chaindataProvider}`);
424
+ return `balances-${md5(key)}`;
425
+ }, [allAddresses, balanceModules, chainConnectors, chaindataProvider, tokens]);
426
+ const subscription = useCallback(() => {
427
+ if (!Object.values(tokens ?? {}).length || !allAddresses.length) return () => {};
428
+ return subscribeBalances(tokens ?? {}, allAddresses, chainConnectors, chaindataProvider, balanceModules);
429
+ }, [allAddresses, balanceModules, chainConnectors, chaindataProvider, tokens]);
430
+ useSharedSubscription(subscriptionKey, subscription);
431
+ }
432
+ const subscribeChainDataHydrate = (provider, type) => {
433
+ const chaindata = provider;
434
+ const delay = 300_000; // 300_000ms = 300s = 5 minutes
241
435
 
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
- };
436
+ let timeout = null;
437
+ const hydrate = async () => {
438
+ try {
439
+ if (type === "chains") await chaindata.hydrateChains();
440
+ if (type === "evmNetworks") await chaindata.hydrateEvmNetworks();
441
+ if (type === "tokens") await chaindata.hydrateTokens();
442
+ timeout = setTimeout(hydrate, delay);
443
+ } catch (error) {
444
+ const retryTimeout = 5_000; // 5_000ms = 5 seconds
445
+ log.error(`Failed to fetch chaindata, retrying in ${Math.round(retryTimeout / 1000)} seconds`, error);
446
+ timeout = setTimeout(hydrate, retryTimeout);
274
447
  }
448
+ };
275
449
 
276
- // bump up the refcount by 1
277
- subscriptions[key].refcount += 1;
450
+ // launch the loop
451
+ hydrate();
452
+ return () => {
453
+ if (timeout) clearTimeout(timeout);
278
454
  };
279
- const removeSubscription = (key, balanceModule, addressesByToken) => {
280
- // ignore dead subscriptions
281
- if (!subscriptions[key] || subscriptions[key].refcount === 0) return;
455
+ };
456
+ const subscribeTokenRates = tokens => {
457
+ const REFRESH_INTERVAL = 300_000; // 6 minutes
458
+ const RETRY_INTERVAL = 5_000; // 5 sec
282
459
 
283
- // drop the refcount by one
284
- subscriptions[key].refcount -= 1;
460
+ let timeout = null;
461
+ const refreshTokenRates = async () => {
462
+ try {
463
+ if (timeout) clearTimeout(timeout);
464
+ const tokenRates = await fetchTokenRates(tokens);
465
+ const putTokenRates = Object.entries(tokenRates).map(([tokenId, rates]) => ({
466
+ tokenId,
467
+ rates
468
+ }));
469
+ db.transaction("rw", db.tokenRates, async () => await db.tokenRates.bulkPut(putTokenRates));
470
+ timeout = setTimeout(() => {
471
+ refreshTokenRates();
472
+ }, REFRESH_INTERVAL);
473
+ } catch (error) {
474
+ log.error(`Failed to fetch tokenRates, retrying in ${Math.round(RETRY_INTERVAL / 1000)} seconds`, error);
475
+ setTimeout(async () => {
476
+ refreshTokenRates();
477
+ }, RETRY_INTERVAL);
478
+ }
479
+ };
285
480
 
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];
481
+ // launch the loop
482
+ refreshTokenRates();
483
+ return () => {
484
+ if (timeout) clearTimeout(timeout);
485
+ };
486
+ };
487
+ const subscribeBalances = (tokens, addresses, chainConnectors, provider, balanceModules) => {
488
+ const tokenIds = Object.values(tokens).map(({
489
+ id
490
+ }) => id);
491
+ const addressesByToken = Object.fromEntries(tokenIds.map(tokenId => [tokenId, addresses]));
492
+ const updateDb = balances => {
493
+ const putBalances = Object.entries(balances.toJSON()).map(([id, balance]) => ({
494
+ id,
495
+ ...balance
496
+ }));
497
+ db$1.transaction("rw", db$1.balances, async () => await db$1.balances.bulkPut(putBalances));
498
+ };
499
+ let unsubscribed = false;
291
500
 
292
- // set this subscription's balances in the store to status: cache
293
- db.transaction("rw", db.balances, async () => await db.balances.filter(balance => {
501
+ // eslint-disable-next-line no-console
502
+ log.log("subscribing to balance changes for %d tokens and %d addresses", tokenIds.length, addresses.length);
503
+ const unsubs = balanceModules.map(async balanceModule => {
504
+ // filter out tokens to only include those which this module knows how to fetch balances for
505
+ const moduleTokenIds = Object.values(tokens ?? {}).filter(({
506
+ type
507
+ }) => type === balanceModule.type).map(({
508
+ id
509
+ }) => id);
510
+ const addressesByModuleToken = Object.fromEntries(Object.entries(addressesByToken).filter(([tokenId]) => moduleTokenIds.includes(tokenId)));
511
+ const unsub = balances(balanceModule, chainConnectors, provider, addressesByModuleToken, (error, balances) => {
512
+ // log errors
513
+ if (error) return log.error(`Failed to fetch ${balanceModule.type} balances`, error);
514
+ // ignore empty balance responses
515
+ if (!balances) return;
516
+ // ignore balances from old subscriptions which are still in the process of unsubscribing
517
+ if (unsubscribed) return;
518
+ updateDb(balances);
519
+ });
520
+ return () => {
521
+ // wait 2 seconds before actually unsubscribing, allowing for websocket to be reused
522
+ unsub.then(unsubscribe => {
523
+ setTimeout(unsubscribe, 2_000);
524
+ });
525
+ db$1.transaction("rw", db$1.balances, async () => await db$1.balances.filter(balance => {
294
526
  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;
527
+ if (!Object.keys(addressesByModuleToken).includes(balance.tokenId)) return false;
528
+ if (!addressesByModuleToken[balance.tokenId].includes(balance.address)) return false;
297
529
  return true;
298
530
  }).modify({
299
531
  status: "cache"
300
532
  }));
301
- }
533
+ };
534
+ });
535
+ const unsubscribeAll = () => {
536
+ unsubscribed = true;
537
+ unsubs.forEach(unsub => unsub.then(unsubscribe => unsubscribe()));
302
538
  };
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]);
539
+ return unsubscribeAll;
540
+ };
541
+
542
+ function useChains(withTestnets) {
543
+ // keep db data up to date
544
+ useDbCacheSubscription("chains");
545
+ const {
546
+ chainsWithTestnetsMap,
547
+ chainsWithoutTestnetsMap
548
+ } = useDbCache();
549
+ return withTestnets ? chainsWithTestnetsMap : chainsWithoutTestnetsMap;
550
+ }
551
+ function useChain(chainId, withTestnets) {
552
+ const chains = useChains(withTestnets);
553
+ return chainId ? chains[chainId] : undefined;
334
554
  }
335
555
 
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;
556
+ function useEvmNetworks(withTestnets) {
557
+ // keep db data up to date
558
+ useDbCacheSubscription("evmNetworks");
559
+ const {
560
+ evmNetworksWithTestnetsMap,
561
+ evmNetworksWithoutTestnetsMap
562
+ } = useDbCache();
563
+ return withTestnets ? evmNetworksWithTestnetsMap : evmNetworksWithoutTestnetsMap;
344
564
  }
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;
565
+ function useEvmNetwork(evmNetworkId, withTestnets) {
566
+ const evmNetworks = useEvmNetworks(withTestnets);
567
+ return evmNetworkId ? evmNetworks[evmNetworkId] : undefined;
568
+ }
569
+
570
+ function useTokenRates() {
571
+ // keep db data up to date
572
+ useDbCacheTokenRatesSubscription();
573
+ const {
574
+ tokenRatesMap
575
+ } = useDbCache();
576
+ return tokenRatesMap;
577
+ }
578
+ function useTokenRate(tokenId) {
579
+ const tokenRates = useTokenRates();
580
+ return tokenId ? tokenRates[tokenId] : undefined;
581
+ }
582
+
583
+ const useBalancesHydrate = () => {
584
+ const {
585
+ withTestnets
586
+ } = useWithTestnets();
587
+ const chains = useChains(withTestnets);
588
+ const evmNetworks = useEvmNetworks(withTestnets);
589
+ const tokens = useTokens(withTestnets);
590
+ const tokenRates = useTokenRates();
591
+ return useMemo(() => ({
592
+ chains,
593
+ evmNetworks,
594
+ tokens,
595
+ tokenRates
596
+ }), [chains, evmNetworks, tokens, tokenRates]);
597
+ };
598
+
599
+ function useBalances(addressesByToken) {
600
+ // keep db data up to date
601
+ useDbCacheBalancesSubscription();
602
+ const balanceModules = useBalanceModules();
603
+ const {
604
+ balances
605
+ } = useDbCache();
606
+ const hydrate = useBalancesHydrate();
607
+ return useMemo(() => new Balances(balances.filter(balance => {
608
+ // check that this balance is included in our queried balance modules
609
+ if (!balanceModules.map(({
610
+ type
611
+ }) => type).includes(balance.source)) return false;
612
+
613
+ // check that our query includes some tokens and addresses
614
+ if (!addressesByToken) return false;
615
+
616
+ // check that this balance is included in our queried tokens
617
+ if (!Object.keys(addressesByToken).includes(balance.tokenId)) return false;
618
+
619
+ // check that this balance is included in our queried addresses for this token
620
+ if (!addressesByToken[balance.tokenId].includes(balance.address)) return false;
621
+
622
+ // keep this balance
623
+ return true;
624
+ }),
625
+ // hydrate balance chains, evmNetworks, tokens and tokenRates
626
+ hydrate), [balances, hydrate, balanceModules, addressesByToken]);
355
627
  }
356
628
 
357
- export { useBalances, useChain, useChaindata, useChains, useEvmNetwork, useEvmNetworks, useToken, useTokenRates, useTokens };
629
+ const BalancesProvider = ({
630
+ balanceModules,
631
+ onfinalityApiKey,
632
+ withTestnets,
633
+ children
634
+ }) => /*#__PURE__*/jsx(WithTestnetsProvider, {
635
+ withTestnets: withTestnets,
636
+ children: /*#__PURE__*/jsx(ChaindataProvider, {
637
+ onfinalityApiKey: onfinalityApiKey,
638
+ children: /*#__PURE__*/jsx(ChainConnectorsProvider, {
639
+ onfinalityApiKey: onfinalityApiKey,
640
+ children: /*#__PURE__*/jsx(AllAddressesProvider, {
641
+ children: /*#__PURE__*/jsx(BalanceModulesProvider, {
642
+ balanceModules: balanceModules,
643
+ children: /*#__PURE__*/jsx(DbCacheProvider, {
644
+ children: children
645
+ })
646
+ })
647
+ })
648
+ })
649
+ })
650
+ });
651
+
652
+ export { AllAddressesProvider, BalanceModulesProvider, BalancesProvider, ChainConnectorsProvider, ChaindataProvider, DbCacheProvider, WithTestnetsProvider, createMulticastSubscription, provideContext, useAllAddresses, useBalanceModules, useBalances, useBalancesHydrate, useChain, useChainConnectors, useChaindata, useChains, useDbCache, useDbCacheBalancesSubscription, useDbCacheSubscription, useDbCacheTokenRatesSubscription, useEvmNetwork, useEvmNetworks, useMulticastSubscription, useToken, useTokenRate, useTokenRates, useTokens, useWithTestnets };