@talismn/balances-react 0.3.3 → 0.4.1

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