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