@talismn/balances-react 0.6.1 → 0.7.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.
Files changed (37) hide show
  1. package/dist/declarations/src/atoms/allAddresses.d.ts +4 -0
  2. package/dist/declarations/src/atoms/balanceModules.d.ts +2 -0
  3. package/dist/declarations/src/atoms/balances.d.ts +6 -0
  4. package/dist/declarations/src/atoms/chainConnectors.d.ts +2 -0
  5. package/dist/declarations/src/atoms/chaindata.d.ts +34 -0
  6. package/dist/declarations/src/atoms/chaindataProvider.d.ts +5 -0
  7. package/dist/declarations/src/atoms/config.d.ts +18 -0
  8. package/dist/declarations/src/atoms/cryptoWaitReady.d.ts +1 -0
  9. package/dist/declarations/src/atoms/tokenRates.d.ts +3 -0
  10. package/dist/declarations/src/hooks/useBalances.d.ts +27 -3
  11. package/dist/declarations/src/hooks/useChainConnectors.d.ts +1 -12
  12. package/dist/declarations/src/hooks/useChaindata.d.ts +23 -7
  13. package/dist/declarations/src/hooks/useTokenRates.d.ts +4 -2
  14. package/dist/declarations/src/index.d.ts +80 -2
  15. package/dist/declarations/src/util/balancesPersist.d.ts +10 -0
  16. package/dist/declarations/src/util/dexieToRxjs.d.ts +6 -0
  17. package/dist/talismn-balances-react.cjs.dev.js +603 -631
  18. package/dist/talismn-balances-react.cjs.prod.js +603 -631
  19. package/dist/talismn-balances-react.esm.js +547 -611
  20. package/package.json +30 -24
  21. package/CHANGELOG.md +0 -477
  22. package/dist/declarations/src/hooks/index.d.ts +0 -49
  23. package/dist/declarations/src/hooks/useAllAddresses.d.ts +0 -4
  24. package/dist/declarations/src/hooks/useBalanceModules.d.ts +0 -8
  25. package/dist/declarations/src/hooks/useBalancesHydrate.d.ts +0 -1
  26. package/dist/declarations/src/hooks/useBalancesStatus.d.ts +0 -17
  27. package/dist/declarations/src/hooks/useChains.d.ts +0 -3
  28. package/dist/declarations/src/hooks/useDbCache.d.ts +0 -24
  29. package/dist/declarations/src/hooks/useDbCacheSubscription.d.ts +0 -13
  30. package/dist/declarations/src/hooks/useEnabledChains.d.ts +0 -8
  31. package/dist/declarations/src/hooks/useEvmNetworks.d.ts +0 -3
  32. package/dist/declarations/src/hooks/useTokens.d.ts +0 -3
  33. package/dist/declarations/src/hooks/useWithTestnets.d.ts +0 -8
  34. package/dist/declarations/src/util/index.d.ts +0 -2
  35. package/dist/declarations/src/util/provideContext.d.ts +0 -9
  36. package/dist/declarations/src/util/useMulticastSubscription.d.ts +0 -16
  37. package/dist/declarations/src/util/useSharedSubscription.d.ts +0 -9
@@ -1,191 +1,52 @@
1
1
  'use strict';
2
2
 
3
- Object.defineProperty(exports, '__esModule', { value: true });
4
-
3
+ var jotai = require('jotai');
5
4
  var react = require('react');
5
+ var balances = require('@talismn/balances');
6
+ var tokenRates = require('@talismn/token-rates');
6
7
  var jsxRuntime = require('react/jsx-runtime');
8
+ var util = require('@talismn/util');
9
+ var jotaiEffect = require('jotai-effect');
10
+ var utils = require('jotai/utils');
11
+ var lodash = require('lodash');
12
+ var rxjs = require('rxjs');
13
+ var anylogger = require('anylogger');
7
14
  var chainConnector = require('@talismn/chain-connector');
8
15
  var chainConnectorEvm = require('@talismn/chain-connector-evm');
9
16
  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');
13
- var dexieReactHooks = require('dexie-react-hooks');
14
- var reactUse = require('react-use');
15
- var md5 = require('blueimp-md5');
16
- var anylogger = require('anylogger');
17
- var rxjs = require('rxjs');
17
+ var chaindataProvider = require('@talismn/chaindata-provider');
18
+ var utilCrypto = require('@polkadot/util-crypto');
19
+ var dexie = require('dexie');
20
+ var isEqual = require('lodash/isEqual');
18
21
 
19
- function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
22
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
20
23
 
21
- var md5__default = /*#__PURE__*/_interopDefault(md5);
22
24
  var anylogger__default = /*#__PURE__*/_interopDefault(anylogger);
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
25
+ var isEqual__default = /*#__PURE__*/_interopDefault(isEqual);
26
+
27
+ const balanceModuleCreatorsAtom = jotai.atom(balances.defaultBalanceModules);
28
+ const onfinalityApiKeyAtom = jotai.atom(undefined);
29
+ const innerCoingeckoConfigAtom = jotai.atom(tokenRates.DEFAULT_COINGECKO_CONFIG);
30
+ const coingeckoConfigAtom = jotai.atom(get => get(innerCoingeckoConfigAtom), (_get, set, options) => {
31
+ const apiUrl = options.apiUrl ?? tokenRates.DEFAULT_COINGECKO_CONFIG.apiUrl;
32
+ const apiKeyName = options.apiKeyName ?? tokenRates.DEFAULT_COINGECKO_CONFIG.apiKeyName;
33
+ const apiKeyValue = options.apiKeyValue ?? tokenRates.DEFAULT_COINGECKO_CONFIG.apiKeyValue;
34
+ set(innerCoingeckoConfigAtom, {
35
+ apiUrl,
36
+ apiKeyName,
37
+ apiKeyValue
29
38
  });
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]));
39
+ });
40
+ const enableTestnetsAtom = jotai.atom(false);
41
+ const enabledChainsAtom = jotai.atom(undefined);
42
+ const enabledTokensAtom = jotai.atom(undefined);
140
43
 
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);
44
+ /** Sets the list of addresses for which token balances will be fetched by the balances subscription */
45
+ const allAddressesAtom = jotai.atom([]);
185
46
 
186
47
  var packageJson = {
187
48
  name: "@talismn/balances-react",
188
- version: "0.6.1",
49
+ version: "0.7.1",
189
50
  author: "Talisman",
190
51
  homepage: "https://talisman.xyz",
191
52
  license: "GPL-3.0-or-later",
@@ -208,35 +69,41 @@ var packageJson = {
208
69
  scripts: {
209
70
  test: "jest",
210
71
  lint: "eslint src --max-warnings 0",
211
- clean: "rm -rf dist && rm -rf .turbo rm -rf node_modules"
72
+ clean: "rm -rf dist .turbo node_modules"
212
73
  },
213
74
  dependencies: {
214
75
  "@talismn/balances": "workspace:*",
215
76
  "@talismn/chain-connector": "workspace:*",
216
77
  "@talismn/chain-connector-evm": "workspace:*",
217
78
  "@talismn/chaindata-provider": "workspace:*",
218
- "@talismn/chaindata-provider-extension": "workspace:*",
219
79
  "@talismn/connection-meta": "workspace:*",
80
+ "@talismn/scale": "workspace:*",
220
81
  "@talismn/token-rates": "workspace:*",
82
+ "@talismn/util": "workspace:*",
221
83
  anylogger: "^1.0.11",
222
84
  "blueimp-md5": "2.19.0",
223
- dexie: "^3.2.4",
85
+ dexie: "^4.0.9",
224
86
  "dexie-react-hooks": "^1.1.7",
225
- "react-use": "^17.4.0",
87
+ jotai: "~2",
88
+ "jotai-effect": "~1",
89
+ lodash: "4.17.21",
90
+ "react-use": "^17.5.1",
226
91
  rxjs: "^7.8.1"
227
92
  },
228
93
  devDependencies: {
229
94
  "@talismn/eslint-config": "workspace:*",
230
95
  "@talismn/tsconfig": "workspace:*",
231
- "@types/jest": "^27.5.1",
232
- "@types/react": "^18.0.17",
233
- eslint: "^8.4.0",
96
+ "@types/jest": "^29.5.14",
97
+ "@types/lodash": "^4.17.12",
98
+ "@types/react": "^18.3.12",
99
+ eslint: "^8.57.1",
234
100
  jest: "^29.7.0",
235
- react: "^18.2.0",
236
- "ts-jest": "^29.1.1",
237
- typescript: "^4.6.4"
101
+ react: "^18.3.1",
102
+ "ts-jest": "^29.2.5",
103
+ typescript: "^5.6.3"
238
104
  },
239
105
  peerDependencies: {
106
+ "@polkadot/util-crypto": "*",
240
107
  react: "*",
241
108
  "react-dom": "*"
242
109
  },
@@ -248,451 +115,484 @@ var packageJson = {
248
115
  }
249
116
  };
250
117
 
251
- var log = anylogger__default["default"](packageJson.name);
252
-
253
- // global data store containing all subscriptions
254
- const subscriptions = {};
255
-
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
- };
266
- react.useEffect(() => {
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;
283
- }
284
- };
285
- }, [key]);
286
-
287
- // Initialize subscription
288
- react.useEffect(() => {
118
+ var log = anylogger__default.default(packageJson.name);
119
+
120
+ /**
121
+ // Persistence backend for indexedDB
122
+ // Add a new backend by implementing the BalancesPersistBackend interface
123
+ // configureStore can be called with a different indexedDB table
124
+ */
125
+ balances.configureStore();
126
+
127
+ /**
128
+ // Persistence backend for localStorage
129
+ */
130
+ const localStoragePersist = async balances$1 => {
131
+ const storedBalances = balances$1.map(b => {
132
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
289
133
  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));
324
- return () => {
325
- typeof unsubscribe === "function" && unsubscribe();
326
- };
134
+ status,
135
+ ...rest
136
+ } = b;
137
+ return rest;
327
138
  });
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;
139
+ const deflated = balances.compress(storedBalances);
140
+ localStorage.setItem("talismanBalances", deflated.toString());
342
141
  };
343
-
344
- const useEnabledChainsProvider = ({
345
- enabledChains
346
- }) => {
347
- return {
348
- enabledChains
349
- };
350
- };
351
- const [EnabledChainsProvider, useEnabledChains] = provideContext(useEnabledChainsProvider);
352
-
353
- const useWithTestnetsProvider = ({
354
- withTestnets
355
- }) => {
356
- return {
357
- withTestnets
358
- };
142
+ const localStorageRetrieve = async () => {
143
+ const deflated = localStorage.getItem("talismanBalances");
144
+ if (deflated) {
145
+ // deflated will be a long string of numbers separated by commas
146
+ const deflatedArray = deflated.split(",").map(n => parseInt(n, 10));
147
+ const deflatedBytes = new Uint8Array(deflatedArray.length);
148
+ deflatedArray.forEach((n, i) => deflatedBytes[i] = n);
149
+ return balances.decompress(deflatedBytes).map(b => ({
150
+ ...b,
151
+ status: "cache"
152
+ }));
153
+ }
154
+ return [];
359
155
  };
360
- const [WithTestnetsProvider, useWithTestnets] = provideContext(useWithTestnetsProvider);
361
-
362
- /**
363
- * This hook is responsible for fetching the data used for balances and inserting it into the db.
364
- */
365
- const useDbCacheSubscription = subscribeTo => {
366
- const provider = useChaindata();
367
-
368
- // can't handle balances & tokenRates here as they have other dependencies, it would trigger to many subscriptions
369
- const subscribe = react.useCallback(() => {
370
- switch (subscribeTo) {
371
- case "chains":
372
- return subscribeChainDataHydrate(provider, "chains");
373
- case "evmNetworks":
374
- return subscribeChainDataHydrate(provider, "evmNetworks");
375
- case "tokens":
376
- return subscribeChainDataHydrate(provider, "tokens");
377
- }
378
- }, [provider, subscribeTo]);
379
- useSharedSubscription(subscribeTo, subscribe);
156
+ const localStorageBalancesPersistBackend = {
157
+ persist: localStoragePersist,
158
+ retrieve: localStorageRetrieve
380
159
  };
381
160
 
382
- /**
383
- * This hook is responsible for fetching the data used for token rates and inserting it into the db.
384
- */
385
- function useDbCacheTokenRatesSubscription() {
386
- const {
387
- withTestnets
388
- } = useWithTestnets();
389
- const tokens = useTokens$1(withTestnets);
390
- const subscriptionKey = react.useMemo(
391
- // not super sexy but we need key to change based on this stuff
392
- () => {
393
- const key = Object.values(tokens ?? {}).map(({
394
- id
395
- }) => id).sort().join();
396
- return `tokenRates-${md5__default["default"](key)}`;
397
- }, [tokens]);
398
- const subscription = react.useCallback(() => {
399
- if (!Object.values(tokens ?? {}).length) return () => {};
400
- return subscribeTokenRates(tokens);
401
- }, [tokens]);
402
- useSharedSubscription(subscriptionKey, subscription);
403
- }
161
+ const cryptoWaitReadyAtom = jotai.atom(async () => await utilCrypto.cryptoWaitReady());
404
162
 
405
- /**
406
- * This hook is responsible for fetching the data used for balances and inserting it into the db.
407
- */
408
- function useDbCacheBalancesSubscription() {
409
- const {
410
- withTestnets
411
- } = useWithTestnets();
412
- const {
413
- enabledChains
414
- } = useEnabledChains();
415
- const balanceModules = useBalanceModules();
416
- const chaindataProvider = useChaindata();
417
- const chainConnectors = useChainConnectors();
418
- const [allAddresses] = useAllAddresses();
419
- const chains = useChains$1(withTestnets);
420
- const allTokens = useTokens$1(withTestnets);
421
- const tokens = react.useMemo(() => {
422
- if (!enabledChains) return allTokens;
423
- const chainsByGenesisHash = new Map(Object.values(chains).flatMap(chain => chain.genesisHash ? [[chain.genesisHash, chain.id]] : []));
424
- const enabledChainIds = enabledChains.flatMap(genesisHash => chainsByGenesisHash.get(genesisHash) ?? []);
425
- return Object.fromEntries(Object.entries(allTokens).flatMap(([id, token]) => token.chain && enabledChainIds.includes(token.chain.id) ? [[id, token]] : []));
426
- }, [allTokens, chains, enabledChains]);
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, balanceModules);
438
- }, [allAddresses, balanceModules, tokens]);
439
- useSharedSubscription(subscriptionKey, subscription);
440
- }
163
+ const chaindataProviderAtom = jotai.atom(get => {
164
+ // runs a timer to keep chaindata hydrated
165
+ get(chaindataHydrateAtomEffect);
166
+ return new chaindataProvider.ChaindataProvider({
167
+ onfinalityApiKey: get(onfinalityApiKeyAtom)
168
+ });
169
+ });
170
+ const miniMetadataHydratedAtom = jotai.atom(false);
441
171
 
442
- // subscriptionless version of useChains and useTokens, prevents circular dependency
443
- const useChains$1 = withTestnets => {
444
- const {
445
- chainsWithTestnetsMap,
446
- chainsWithoutTestnetsMap
447
- } = useDbCache();
448
- return withTestnets ? chainsWithTestnetsMap : chainsWithoutTestnetsMap;
449
- };
450
- const useTokens$1 = withTestnets => {
451
- const {
452
- tokensWithTestnetsMap,
453
- tokensWithoutTestnetsMap
454
- } = useDbCache();
455
- return withTestnets ? tokensWithTestnetsMap : tokensWithoutTestnetsMap;
456
- };
457
- const subscribeChainDataHydrate = (provider, type) => {
458
- const chaindata = provider;
459
- const delay = 300_000; // 300_000ms = 300s = 5 minutes
172
+ /** This atomEffect keeps chaindata hydrated (i.e. up to date with the GitHub repo) */
173
+ const chaindataHydrateAtomEffect = jotaiEffect.atomEffect((get, set) => {
174
+ const chaindataProvider = get(chaindataProviderAtom);
175
+ const miniMetadataUpdater = get(miniMetadataUpdaterAtom);
176
+ const evmTokenFetcher = get(evmTokenFetcherAtom);
177
+ const loopMs = 300_000; // 300_000ms = 300s = 5 minutes
178
+ const retryTimeout = 5_000; // 5_000ms = 5 seconds
460
179
 
461
180
  let timeout = null;
462
181
  const hydrate = async () => {
463
182
  try {
464
- if (type === "chains") await chaindata.hydrateChains();
465
- if (type === "evmNetworks") await chaindata.hydrateEvmNetworks();
466
- if (type === "tokens") await chaindata.hydrateTokens();
467
- timeout = setTimeout(hydrate, delay);
183
+ await get(cryptoWaitReadyAtom);
184
+ await balances.hydrateChaindataAndMiniMetadata(chaindataProvider, miniMetadataUpdater);
185
+ await balances.updateCustomMiniMetadata(chaindataProvider, miniMetadataUpdater);
186
+ await balances.updateEvmTokens(chaindataProvider, evmTokenFetcher);
187
+ set(miniMetadataHydratedAtom, true);
188
+ timeout = setTimeout(hydrate, loopMs);
468
189
  } catch (error) {
469
- const retryTimeout = 5_000; // 5_000ms = 5 seconds
470
- log.error(`Failed to fetch chaindata, retrying in ${Math.round(retryTimeout / 1000)} seconds`, error);
190
+ log.error(`Failed to hydrate chaindata, retrying in ${Math.round(retryTimeout / 1000)} seconds`, error);
471
191
  timeout = setTimeout(hydrate, retryTimeout);
472
192
  }
473
193
  };
474
194
 
475
195
  // launch the loop
476
196
  hydrate();
477
- return () => {
478
- if (timeout) clearTimeout(timeout);
479
- };
480
- };
481
- const subscribeTokenRates = tokens => {
482
- const REFRESH_INTERVAL = 300_000; // 6 minutes
483
- const RETRY_INTERVAL = 5_000; // 5 sec
484
197
 
485
- let timeout = null;
486
- const refreshTokenRates = async () => {
487
- try {
488
- if (timeout) clearTimeout(timeout);
489
- const tokenRates$1 = await tokenRates.fetchTokenRates(tokens);
490
- const putTokenRates = Object.entries(tokenRates$1).map(([tokenId, rates]) => ({
491
- tokenId,
492
- rates
493
- }));
494
- await tokenRates.db.transaction("rw", tokenRates.db.tokenRates, async () => {
495
- // override all tokenRates
496
- await tokenRates.db.tokenRates.bulkPut(putTokenRates);
497
-
498
- // delete tokenRates for tokens which no longer exist
499
- const tokenIds = await tokenRates.db.tokenRates.toCollection().primaryKeys();
500
- const validTokenIds = new Set(Object.keys(tokenRates$1));
501
- const deleteTokenIds = tokenIds.filter(tokenId => !validTokenIds.has(tokenId));
502
- if (deleteTokenIds.length > 0) await tokenRates.db.tokenRates.bulkDelete(deleteTokenIds);
503
- });
504
- timeout = setTimeout(() => {
505
- refreshTokenRates();
506
- }, REFRESH_INTERVAL);
507
- } catch (error) {
508
- log.error(`Failed to fetch tokenRates, retrying in ${Math.round(RETRY_INTERVAL / 1000)} seconds`, error);
509
- setTimeout(async () => {
510
- refreshTokenRates();
511
- }, RETRY_INTERVAL);
512
- }
513
- };
198
+ // return an unsub function to shut down the loop
199
+ return () => timeout && clearTimeout(timeout);
200
+ });
514
201
 
515
- // launch the loop
516
- refreshTokenRates();
517
- return () => {
518
- if (timeout) clearTimeout(timeout);
519
- };
520
- };
521
- const subscribeBalances = (tokens, addresses, balanceModules) => {
522
- const tokenIds = Object.values(tokens).map(({
523
- id
524
- }) => id);
525
- const addressesByToken = Object.fromEntries(tokenIds.map(tokenId => [tokenId, addresses]));
526
- const subscriptionId = balances.createSubscriptionId();
527
-
528
- // TODO: Create subscriptions in a service worker, where we can detect page closes
529
- // and therefore reliably delete the subscriptionId when the user closes our dapp
530
- //
531
- // For more information, check out https://developer.chrome.com/blog/page-lifecycle-api/#faqs
532
- // and scroll down to:
533
- // - `What is the back/forward cache?`, and
534
- // - `If I can't run asynchronous APIs in the frozen or terminated states, how can I save data to IndexedDB?
535
- //
536
- // For now, we'll just last-ditch remove the subscriptionId (it works surprisingly well!) in the beforeunload event
537
- window.onbeforeunload = () => {
538
- balances.deleteSubscriptionId();
202
+ /** MiniMetadataUpdater is a class used for hydrating chaindata */
203
+ const miniMetadataUpdaterAtom = jotai.atom(get => {
204
+ const chainConnectors = get(chainConnectorsAtom);
205
+ const chaindataProvider = get(chaindataProviderAtom);
206
+ const balanceModules = get(balanceModulesAtom);
207
+ return new balances.MiniMetadataUpdater(chainConnectors, chaindataProvider, balanceModules);
208
+ });
209
+
210
+ /** EvmTokenFetcher is a class used for hydrating chaindata */
211
+ const evmTokenFetcherAtom = jotai.atom(get => {
212
+ const chaindataProvider = get(chaindataProviderAtom);
213
+ const balanceModules = get(balanceModulesAtom);
214
+ return new balances.EvmTokenFetcher(chaindataProvider, balanceModules);
215
+ });
216
+
217
+ const chainConnectorsAtom = jotai.atom(get => {
218
+ const onfinalityApiKey = get(onfinalityApiKeyAtom);
219
+ const chaindataProvider = get(chaindataProviderAtom);
220
+ const substrate = new chainConnector.ChainConnector(chaindataProvider, connectionMeta.connectionMetaDb);
221
+ const evm = new chainConnectorEvm.ChainConnectorEvm(chaindataProvider, {
222
+ onfinalityApiKey
223
+ });
224
+ return {
225
+ substrate,
226
+ evm
539
227
  };
540
- const updateDb = balances$1 => {
541
- const putBalances = Object.entries(balances$1.toJSON()).map(([id, balance]) => ({
542
- id,
543
- ...balance,
544
- status: balances.BalanceStatusLive(subscriptionId)
545
- }));
546
- balances.db.transaction("rw", balances.db.balances, async () => await balances.db.balances.bulkPut(putBalances));
228
+ });
229
+
230
+ const balanceModulesAtom = jotai.atom(get => {
231
+ const balanceModuleCreators = get(balanceModuleCreatorsAtom);
232
+ const chainConnectors = get(chainConnectorsAtom);
233
+ const chaindataProvider = get(chaindataProviderAtom);
234
+ if (!chainConnectors.substrate) return [];
235
+ if (!chainConnectors.evm) return [];
236
+ if (!chaindataProvider) return [];
237
+ return balanceModuleCreators.map(mod => mod({
238
+ chainConnectors,
239
+ chaindataProvider
240
+ }));
241
+ });
242
+
243
+ /**
244
+ * Converts a dexie Observable into an rxjs Observable.
245
+ */
246
+ function dexieToRxjs(o) {
247
+ return new rxjs.Observable(observer => {
248
+ const subscription = o.subscribe({
249
+ next: value => observer.next(value),
250
+ error: error => observer.error(error)
251
+ });
252
+ return () => subscription.unsubscribe();
253
+ });
254
+ }
255
+
256
+ const chainsAtom = jotai.atom(async get => (await get(chaindataAtom)).chains);
257
+ const chainsByIdAtom = jotai.atom(async get => (await get(chaindataAtom)).chainsById);
258
+ const chainsByGenesisHashAtom = jotai.atom(async get => (await get(chaindataAtom)).chainsByGenesisHash);
259
+ const evmNetworksAtom = jotai.atom(async get => (await get(chaindataAtom)).evmNetworks);
260
+ const evmNetworksByIdAtom = jotai.atom(async get => (await get(chaindataAtom)).evmNetworksById);
261
+ const tokensAtom = jotai.atom(async get => (await get(chaindataAtom)).tokens);
262
+ const tokensByIdAtom = jotai.atom(async get => (await get(chaindataAtom)).tokensById);
263
+ const miniMetadatasAtom = jotai.atom(async get => (await get(chaindataAtom)).miniMetadatas);
264
+ const chaindataAtom = utils.atomWithObservable(get => {
265
+ const enableTestnets = get(enableTestnetsAtom);
266
+ const filterTestnets = items => enableTestnets ? items : items.filter(({
267
+ isTestnet
268
+ }) => !isTestnet);
269
+ const filterMapTestnets = items => enableTestnets ? items : Object.fromEntries(Object.entries(items).filter(([, {
270
+ isTestnet
271
+ }]) => !isTestnet));
272
+ const filterEnabledTokens = tokens => tokens.filter(token => token.isDefault || "isCustom" in token && token.isCustom);
273
+ const filterMapEnabledTokens = tokensById => Object.fromEntries(Object.entries(tokensById).filter(([, token]) => token.isDefault || "isCustom" in token && token.isCustom));
274
+ const distinctUntilIsEqual = rxjs.distinctUntilChanged((a, b) => isEqual__default.default(a, b));
275
+ const chains = get(chaindataProviderAtom).chainsObservable.pipe(distinctUntilIsEqual, rxjs.map(filterTestnets), distinctUntilIsEqual);
276
+ const chainsById = get(chaindataProviderAtom).chainsByIdObservable.pipe(distinctUntilIsEqual, rxjs.map(filterMapTestnets), distinctUntilIsEqual);
277
+ const chainsByGenesisHash = get(chaindataProviderAtom).chainsByGenesisHashObservable.pipe(distinctUntilIsEqual, rxjs.map(filterMapTestnets), distinctUntilIsEqual);
278
+ const evmNetworks = get(chaindataProviderAtom).evmNetworksObservable.pipe(distinctUntilIsEqual, rxjs.map(filterTestnets), distinctUntilIsEqual);
279
+ const evmNetworksById = get(chaindataProviderAtom).evmNetworksByIdObservable.pipe(distinctUntilIsEqual, rxjs.map(filterMapTestnets), distinctUntilIsEqual);
280
+ const tokens = get(chaindataProviderAtom).tokensObservable.pipe(distinctUntilIsEqual, rxjs.map(filterTestnets), rxjs.map(filterEnabledTokens), distinctUntilIsEqual);
281
+ const tokensById = get(chaindataProviderAtom).tokensByIdObservable.pipe(distinctUntilIsEqual, rxjs.map(filterMapTestnets), rxjs.map(filterMapEnabledTokens), distinctUntilIsEqual);
282
+ const miniMetadatasObservable = dexieToRxjs(dexie.liveQuery(() => balances.db.miniMetadatas.toArray()));
283
+ const miniMetadatas = rxjs.combineLatest([miniMetadatasObservable.pipe(distinctUntilIsEqual), chainsById]).pipe(rxjs.map(([miniMetadatas, chainsById]) => miniMetadatas.filter(m => chainsById[m.chainId])), distinctUntilIsEqual);
284
+ return rxjs.combineLatest({
285
+ chains,
286
+ chainsById,
287
+ chainsByGenesisHash,
288
+ evmNetworks,
289
+ evmNetworksById,
290
+ tokens,
291
+ tokensById,
292
+ miniMetadatas
293
+ }).pipe(
294
+ // debounce to prevent hammering UI with updates
295
+ util.firstThenDebounce(1_000), distinctUntilIsEqual);
296
+ });
297
+
298
+ const tokenRatesAtom = jotai.atom(async get => {
299
+ // runs a timer to keep tokenRates up to date
300
+ get(tokenRatesFetcherAtomEffect);
301
+ return await get(tokenRatesDbAtom);
302
+ });
303
+ const tokenRatesDbAtom = utils.atomWithObservable(() => {
304
+ const dbRatesToMap = dbRates => Object.fromEntries(dbRates.map(({
305
+ tokenId,
306
+ rates
307
+ }) => [tokenId, rates]));
308
+
309
+ // retrieve fetched tokenRates from the db
310
+ return dexieToRxjs(dexie.liveQuery(() => tokenRates.db.tokenRates.toArray())).pipe(rxjs.map(dbRatesToMap));
311
+ });
312
+ const tokenRatesFetcherAtomEffect = jotaiEffect.atomEffect(get => {
313
+ // lets us tear down the existing timer when the effect is restarted
314
+ const abort = new AbortController();
315
+
316
+ // we have to get these synchronously so that jotai knows to restart our timer when they change
317
+ const coingeckoConfig = get(coingeckoConfigAtom);
318
+ const tokensByIdPromise = get(tokensByIdAtom);
319
+ (async () => {
320
+ const tokensById = await tokensByIdPromise;
321
+ const tokenIds = Object.keys(tokensById);
322
+ const loopMs = 300_000; // 300_000ms = 300s = 5 minutes
323
+ const retryTimeout = 5_000; // 5_000ms = 5 seconds
324
+
325
+ const hydrate = async () => {
326
+ try {
327
+ if (abort.signal.aborted) return; // don't fetch if aborted
328
+ const tokenRates$1 = await tokenRates.fetchTokenRates(tokensById, coingeckoConfig);
329
+ const putTokenRates = Object.entries(tokenRates$1).map(([tokenId, rates]) => ({
330
+ tokenId,
331
+ rates
332
+ }));
333
+ if (abort.signal.aborted) return; // don't insert into db if aborted
334
+ await tokenRates.db.transaction("rw", tokenRates.db.tokenRates, async () => {
335
+ // override all tokenRates
336
+ await tokenRates.db.tokenRates.bulkPut(putTokenRates);
337
+
338
+ // delete tokenRates for tokens which no longer exist
339
+ const validTokenIds = new Set(tokenIds);
340
+ const tokenRatesIds = await tokenRates.db.tokenRates.toCollection().primaryKeys();
341
+ const deleteIds = tokenRatesIds.filter(id => !validTokenIds.has(id));
342
+ if (deleteIds.length > 0) await tokenRates.db.tokenRates.bulkDelete(deleteIds);
343
+ });
344
+ if (abort.signal.aborted) return; // don't schedule next loop if aborted
345
+ setTimeout(hydrate, loopMs);
346
+ } catch (error) {
347
+ const retrying = !abort.signal.aborted;
348
+ const messageParts = ["Failed to fetch tokenRates", retrying && `retrying in ${Math.round(retryTimeout / 1000)} seconds`, !retrying && `giving up (timer no longer needed)`].filter(Boolean);
349
+ log.error(messageParts.join(", "), error);
350
+ if (abort.signal.aborted) return; // don't schedule retry if aborted
351
+ setTimeout(hydrate, retryTimeout);
352
+ }
353
+ };
354
+
355
+ // launch the loop
356
+ hydrate();
357
+ })();
358
+ return () => abort.abort("Unsubscribed");
359
+ });
360
+
361
+ const allBalancesAtom = jotai.atom(async get => {
362
+ // set up our subscription to fetch balances from the various blockchains
363
+ get(balancesSubscriptionAtomEffect);
364
+ const [balances$1, hydrateData] = await Promise.all([get(balancesObservableAtom), get(balancesHydrateDataAtom)]);
365
+ return new balances.Balances(Object.values(balances$1).filter(balance => !!hydrateData?.tokens?.[balance.tokenId]),
366
+ // hydrate balance chains, evmNetworks, tokens and tokenRates
367
+ hydrateData);
368
+ });
369
+ const balancesObservable = new rxjs.BehaviorSubject({});
370
+ const balancesObservableAtom = utils.atomWithObservable(() => balancesObservable);
371
+ const balancesPersistBackendAtom = jotai.atom(localStorageBalancesPersistBackend);
372
+ const hydrateBalancesObservableAtom = jotai.atom(async get => {
373
+ const persistBackend = get(balancesPersistBackendAtom);
374
+ const balances$1 = await persistBackend.retrieve();
375
+ balancesObservable.next(Object.fromEntries(balances$1.map(b => [balances.getBalanceId(b), {
376
+ ...b,
377
+ status: "cache"
378
+ }])));
379
+ });
380
+ const balancesHydrateDataAtom = jotai.atom(async get => {
381
+ const [{
382
+ chainsById,
383
+ evmNetworksById,
384
+ tokensById
385
+ }, tokenRates] = await Promise.all([get(chaindataAtom), get(tokenRatesAtom)]);
386
+ return {
387
+ chains: chainsById,
388
+ evmNetworks: evmNetworksById,
389
+ tokens: tokensById,
390
+ tokenRates
547
391
  };
548
- let unsubscribed = false;
549
-
550
- // eslint-disable-next-line no-console
551
- log.log("subscribing to balance changes for %d tokens and %d addresses", tokenIds.length, addresses.length);
552
- const unsubs = balanceModules.map(async balanceModule => {
553
- // filter out tokens to only include those which this module knows how to fetch balances for
554
- const moduleTokenIds = Object.values(tokens ?? {}).filter(({
555
- type
556
- }) => type === balanceModule.type).map(({
392
+ });
393
+ const balancesSubscriptionAtomEffect = jotaiEffect.atomEffect(get => {
394
+ // lets us tear down the existing subscriptions when the atomEffect is restarted
395
+ const abort = new AbortController();
396
+
397
+ // we have to specify these synchronously, otherwise jotai won't know
398
+ // that it needs to restart our subscriptions when they change
399
+ const atomDependencies = Promise.all([get(cryptoWaitReadyAtom), get(balanceModulesAtom), get(miniMetadataHydratedAtom), get(allAddressesAtom), get(chainsAtom), get(chainsByIdAtom), get(evmNetworksAtom), get(evmNetworksByIdAtom), get(tokensAtom), get(tokensByIdAtom), get(miniMetadatasAtom), get(enabledChainsAtom), get(enabledTokensAtom), get(hydrateBalancesObservableAtom)]);
400
+ const persistBackend = get(balancesPersistBackendAtom);
401
+ const unsubsPromise = (async () => {
402
+ const [_cryptoReady, balanceModules, miniMetadataHydrated, allAddresses, chains, chainsById, evmNetworks, evmNetworksById, tokens, tokensById, _miniMetadatas, enabledChainsConfig, enabledTokensConfig] = await atomDependencies;
403
+ if (!miniMetadataHydrated) return;
404
+ if (abort.signal.aborted) return;
405
+
406
+ // persist data every thirty seconds
407
+ balancesObservable.pipe(rxjs.debounceTime(10000)).subscribe(balancesUpdate => {
408
+ persistBackend.persist(Object.values(balancesUpdate));
409
+ });
410
+ const updateBalances = async balancesUpdates => {
411
+ if (abort.signal.aborted) return;
412
+ const updatesWithIds = new balances.Balances(balancesUpdates);
413
+ const existing = balancesObservable.value;
414
+
415
+ // update initialising set here - before filtering out zero balances
416
+ // while this may include stale balances, the important thing is that the balance is no longer "initialising"
417
+ // balancesUpdates.forEach((b) => this.#initialising.delete(getBalanceId(b)))
418
+
419
+ const newlyZeroBalances = [];
420
+ const changedBalances = Object.fromEntries(updatesWithIds.each.filter(newB => {
421
+ const isZero = newB.total.tokens === "0";
422
+ // Keep new balances which are not zeros
423
+ const existingB = existing[newB.id];
424
+ if (!existingB && !isZero) return true;
425
+ const hasChanged = !lodash.isEqual(existingB, newB.toJSON());
426
+ // Collect balances now confirmed to be zero separately, so they can be filtered out from the main set
427
+ if (existingB && hasChanged && isZero) newlyZeroBalances.push(newB.id);
428
+ // Keep changed balances, which are not known zeros
429
+ return hasChanged && !isZero;
430
+ }).map(b => [b.id, b.toJSON()]));
431
+ if (Object.keys(changedBalances).length === 0 && newlyZeroBalances.length === 0) return;
432
+ const nonZeroBalances = newlyZeroBalances.length > 0 ? Object.fromEntries(Object.entries(existing).filter(([id]) => !newlyZeroBalances.includes(id))) : existing;
433
+ const newBalancesState = {
434
+ ...nonZeroBalances,
435
+ ...changedBalances
436
+ };
437
+ if (Object.keys(newBalancesState).length === 0) return;
438
+ balancesObservable.next(newBalancesState);
439
+ };
440
+ const deleteBalances = async balancesFilter => {
441
+ if (abort.signal.aborted) return;
442
+ const balancesToKeep = Object.fromEntries(new balances.Balances(Object.values(await get(balancesObservableAtom))).each.filter(b => !balancesFilter(b)).map(b => [b.id, b.toJSON()]));
443
+ balancesObservable.next(balancesToKeep);
444
+ };
445
+ const enabledChainIds = enabledChainsConfig?.map(genesisHash => chains.find(chain => chain.genesisHash === genesisHash)?.id);
446
+ const enabledChainsFilter = enabledChainIds ? token => token.chain && enabledChainIds?.includes(token.chain.id) : () => true;
447
+ const enabledTokensFilter = enabledTokensConfig ? token => enabledTokensConfig.includes(token.id) : () => true;
448
+ const enabledTokenIds = tokens.filter(enabledChainsFilter).filter(enabledTokensFilter).map(({
557
449
  id
558
450
  }) => id);
559
- const addressesByModuleToken = Object.fromEntries(Object.entries(addressesByToken).filter(([tokenId]) => moduleTokenIds.includes(tokenId)));
560
- const unsub = balances.balances(balanceModule, addressesByModuleToken, (error, balances$1) => {
561
- // log errors
562
- if (error) {
563
- if (error?.type === "STALE_RPC_ERROR" || error?.type === "WEBSOCKET_ALLOCATION_EXHAUSTED_ERROR") return balances.db.balances.where({
564
- source: balanceModule.type,
565
- chainId: error.chainId
566
- }).filter(balance => {
567
- if (!Object.keys(addressesByModuleToken).includes(balance.tokenId)) return false;
568
- if (!addressesByModuleToken[balance.tokenId].includes(balance.address)) return false;
569
- return true;
570
- }).modify({
571
- status: "stale"
572
- });
573
- return log.error(`Failed to fetch ${balanceModule.type} balances`, error);
451
+ if (enabledTokenIds.length < 1 || allAddresses.length < 1) return;
452
+ const addressesByTokenByModule = {};
453
+ enabledTokenIds.flatMap(tokenId => tokensById[tokenId]).forEach(token => {
454
+ // filter out tokens on chains/evmNetworks which have no rpcs
455
+ const hasRpcs = token.chain?.id && (chainsById[token.chain.id]?.rpcs?.length ?? 0) > 0 || token.evmNetwork?.id && (evmNetworksById[token.evmNetwork.id]?.rpcs?.length ?? 0) > 0;
456
+ if (!hasRpcs) return;
457
+ if (!addressesByTokenByModule[token.type]) addressesByTokenByModule[token.type] = {};
458
+ addressesByTokenByModule[token.type][token.id] = allAddresses.filter(address => {
459
+ // for each address, fetch balances only from compatible chains
460
+ return util.isEthereumAddress(address) ? token.evmNetwork?.id || chainsById[token.chain?.id ?? ""]?.account === "secp256k1" : token.chain?.id && chainsById[token.chain?.id ?? ""]?.account !== "secp256k1";
461
+ });
462
+ });
463
+
464
+ // Delete invalid cached balances
465
+ const chainIds = new Set(chains.map(chain => chain.id));
466
+ const evmNetworkIds = new Set(evmNetworks.map(evmNetwork => evmNetwork.id));
467
+ await deleteBalances(balance => {
468
+ // delete cached balances for accounts which don't exist anymore
469
+ if (!balance.address || !allAddresses.includes(balance.address)) return true;
470
+
471
+ // delete cached balances when chain/evmNetwork doesn't exist
472
+ if (balance.chainId === undefined && balance.evmNetworkId === undefined) return true;
473
+ if (balance.chainId !== undefined && !chainIds.has(balance.chainId)) return true;
474
+ if (balance.evmNetworkId !== undefined && !evmNetworkIds.has(balance.evmNetworkId)) return true;
475
+
476
+ // delete cached balance when token doesn't exist / is disabled
477
+ if (!enabledTokenIds.includes(balance.tokenId)) return true;
478
+
479
+ // delete cached balance when module doesn't exist
480
+ if (!balanceModules.find(module => module.type === balance.source)) return true;
481
+
482
+ // delete cached balance for accounts on incompatible chains
483
+ // (substrate accounts shouldn't have evm balances)
484
+ // (evm accounts shouldn't have substrate balances (unless the chain uses secp256k1 accounts))
485
+ const chain = balance.chainId && chains.find(({
486
+ id
487
+ }) => id === balance.chainId) || null;
488
+ const hasChain = balance.chainId && chainIds.has(balance.chainId);
489
+ const hasEvmNetwork = balance.evmNetworkId && evmNetworkIds.has(balance.evmNetworkId);
490
+ const chainUsesSecp256k1Accounts = chain?.account === "secp256k1";
491
+ if (!util.isEthereumAddress(balance.address)) {
492
+ if (!hasChain) return true;
493
+ if (chainUsesSecp256k1Accounts) return true;
494
+ }
495
+ if (util.isEthereumAddress(balance.address)) {
496
+ if (!hasEvmNetwork && !chainUsesSecp256k1Accounts) return true;
574
497
  }
575
- // ignore empty balance responses
576
- if (!balances$1) return;
577
- // ignore balances from old subscriptions which are still in the process of unsubscribing
578
- if (unsubscribed) return;
579
- updateDb(balances$1);
498
+
499
+ // keep balance
500
+ return false;
580
501
  });
581
- return () => {
582
- // wait 2 seconds before actually unsubscribing, allowing for websocket to be reused
583
- unsub.then(unsubscribe => {
584
- setTimeout(unsubscribe, 2_000);
502
+ if (abort.signal.aborted) return;
503
+
504
+ // after 30 seconds, change the status of all balances still initializing to stale
505
+ setTimeout(() => {
506
+ if (abort.signal.aborted) return;
507
+ const staleObservable = balancesObservable.pipe(rxjs.map(val => Object.values(val).filter(({
508
+ status
509
+ }) => status === "cache").map(balance => ({
510
+ ...balance,
511
+ status: "stale"
512
+ }))));
513
+ rxjs.firstValueFrom(staleObservable).then(v => {
514
+ if (v.length) updateBalances(v);
585
515
  });
586
- balances.deleteSubscriptionId();
587
- };
588
- });
589
- const unsubscribeAll = () => {
590
- unsubscribed = true;
591
- unsubs.forEach(unsub => unsub.then(unsubscribe => unsubscribe()));
592
- };
593
- return unsubscribeAll;
594
- };
595
-
596
- function useChains(withTestnets) {
597
- // keep db data up to date
598
- useDbCacheSubscription("chains");
599
- const {
600
- chainsWithTestnetsMap,
601
- chainsWithoutTestnetsMap
602
- } = useDbCache();
603
- return withTestnets ? chainsWithTestnetsMap : chainsWithoutTestnetsMap;
604
- }
605
- function useChain(chainId, withTestnets) {
606
- const chains = useChains(withTestnets);
607
- return chainId ? chains[chainId] : undefined;
608
- }
516
+ }, 30_000);
517
+ return balanceModules.map(balanceModule => {
518
+ const unsub = balances.balances(balanceModule, addressesByTokenByModule[balanceModule.type] ?? {}, (error, balances) => {
519
+ // log errors
520
+ if (error) {
521
+ if (error?.type === "STALE_RPC_ERROR" || error?.type === "WEBSOCKET_ALLOCATION_EXHAUSTED_ERROR") {
522
+ const addressesByModuleToken = addressesByTokenByModule[balanceModule.type] ?? {};
523
+ const staleObservable = balancesObservable.pipe(rxjs.map(val => Object.values(val).filter(balance => {
524
+ const {
525
+ tokenId,
526
+ address,
527
+ source
528
+ } = balance;
529
+ const chainComparison = error.chainId ? "chainId" in balance && error.chainId === balance.chainId : error.evmNetworkId ? "evmNetworkId" in balance && error.evmNetworkId === balance.evmNetworkId : true;
530
+ return chainComparison && addressesByModuleToken[tokenId]?.includes(address) && source === balanceModule.type;
531
+ }).map(balance => ({
532
+ ...balance,
533
+ status: "stale"
534
+ }))));
535
+ rxjs.firstValueFrom(staleObservable).then(v => {
536
+ if (v.length) updateBalances(v);
537
+ });
538
+ }
539
+ return log.error(`Failed to fetch ${balanceModule.type} balances`, error);
540
+ }
541
+
542
+ // ignore empty balance responses
543
+ if (!balances) return;
544
+ // ignore balances from old subscriptions which are still in the process of unsubscribing
545
+ if (abort.signal.aborted) return;
546
+
547
+ // good balances
548
+ if (balances) {
549
+ if ("status" in balances) {
550
+ // For modules using the new SubscriptionResultWithStatus pattern
551
+ //TODO fix initialisin
552
+ // if (result.status === "initialising") this.#initialising.add(balanceModule.type)
553
+ // else this.#initialising.delete(balanceModule.type)
554
+ updateBalances(balances.data);
555
+ } else {
556
+ // add good ones to initialisedBalances
557
+ updateBalances(Object.values(balances.toJSON()));
558
+ }
559
+ }
560
+ });
561
+ return () => unsub.then(unsubscribe => unsubscribe());
562
+ });
563
+ })();
609
564
 
610
- function useEvmNetworks(withTestnets) {
611
- // keep db data up to date
612
- useDbCacheSubscription("evmNetworks");
613
- const {
614
- evmNetworksWithTestnetsMap,
615
- evmNetworksWithoutTestnetsMap
616
- } = useDbCache();
617
- return withTestnets ? evmNetworksWithTestnetsMap : evmNetworksWithoutTestnetsMap;
618
- }
619
- function useEvmNetwork(evmNetworkId, withTestnets) {
620
- const evmNetworks = useEvmNetworks(withTestnets);
621
- return evmNetworkId ? evmNetworks[evmNetworkId] : undefined;
622
- }
565
+ // close the existing subscriptions when our effect unmounts
566
+ // (wait 2 seconds before actually unsubscribing, to allow the websocket to be reused in that time)
567
+ const unsubscribe = () => unsubsPromise.then(unsubs => {
568
+ persistBackend.persist(Object.values(balancesObservable.value));
569
+ unsubs?.forEach(unsub => unsub());
570
+ });
571
+ abort.signal.addEventListener("abort", () => setTimeout(unsubscribe, 2_000));
572
+ return () => abort.abort("Unsubscribed");
573
+ });
623
574
 
624
- function useTokenRates() {
625
- // keep db data up to date
626
- useDbCacheTokenRatesSubscription();
627
- const {
628
- tokenRatesMap
629
- } = useDbCache();
630
- return tokenRatesMap;
631
- }
632
- function useTokenRate(tokenId) {
633
- const tokenRates = useTokenRates();
634
- return tokenId ? tokenRates[tokenId] : undefined;
635
- }
575
+ const useSetBalancesAddresses = addresses => {
576
+ const setAllAddresses = jotai.useSetAtom(allAddressesAtom);
577
+ react.useEffect(() => {
578
+ setAllAddresses(a => JSON.stringify(a) === JSON.stringify(addresses) ? a : addresses);
579
+ }, [addresses, setAllAddresses]);
580
+ };
636
581
 
637
- function useTokens(withTestnets) {
638
- // keep db data up to date
639
- useDbCacheSubscription("tokens");
640
- const {
641
- tokensWithTestnetsMap,
642
- tokensWithoutTestnetsMap
643
- } = useDbCache();
644
- return withTestnets ? tokensWithTestnetsMap : tokensWithoutTestnetsMap;
645
- }
646
- function useToken(tokenId, withTestnets) {
647
- const tokens = useTokens(withTestnets);
648
- return tokenId ? tokens[tokenId] : undefined;
649
- }
582
+ /**
583
+ * @name useBalances
584
+ * @description Hook to get the current balances state.
585
+ * @param persistBackend an optional BalancesPersistBackend backend to use for persisting the balances state. By default, indexedDB is used.
586
+ * @returns a Balances object containing the current balances state.
587
+ */
650
588
 
651
- const useBalancesHydrate = () => {
652
- const {
653
- withTestnets
654
- } = useWithTestnets();
655
- const chains = useChains(withTestnets);
656
- const evmNetworks = useEvmNetworks(withTestnets);
657
- const tokens = useTokens(withTestnets);
658
- const tokenRates = useTokenRates();
659
- return react.useMemo(() => ({
660
- chains,
661
- evmNetworks,
662
- tokens,
663
- tokenRates
664
- }), [chains, evmNetworks, tokens, tokenRates]);
589
+ const useBalances = persistBackend => {
590
+ const [, setPersistBackend] = jotai.useAtom(balancesPersistBackendAtom);
591
+ if (persistBackend) setPersistBackend(persistBackend);
592
+ return jotai.useAtomValue(allBalancesAtom);
665
593
  };
666
594
 
667
- function useBalances(addressesByToken) {
668
- // keep db data up to date
669
- useDbCacheBalancesSubscription();
670
- const balanceModules = useBalanceModules();
671
- const {
672
- balances: balances$1
673
- } = useDbCache();
674
- const hydrate = useBalancesHydrate();
675
- return react.useMemo(() => new balances.Balances(balances.deriveStatuses(balances.getValidSubscriptionIds(), balances$1.filter(balance => {
676
- // check that this balance is included in our queried balance modules
677
- if (!balanceModules.map(({
678
- type
679
- }) => type).includes(balance.source)) return false;
680
-
681
- // check that our query includes some tokens and addresses
682
- if (!addressesByToken) return false;
683
-
684
- // check that this balance is included in our queried tokens
685
- if (!Object.keys(addressesByToken).includes(balance.tokenId)) return false;
686
-
687
- // check that this balance is included in our queried addresses for this token
688
- if (!addressesByToken[balance.tokenId].includes(balance.address)) return false;
689
-
690
- // keep this balance
691
- return true;
692
- })),
693
- // hydrate balance chains, evmNetworks, tokens and tokenRates
694
- hydrate), [balances$1, hydrate, balanceModules, addressesByToken]);
695
- }
595
+ // TODO: Extract to shared definition between extension and @talismn/balances-react
696
596
 
697
597
  /**
698
598
  * Given a collection of `Balances`, this hook returns a `BalancesStatus` summary for the collection.
@@ -722,61 +622,133 @@ const useBalancesStatus = balances => react.useMemo(() => {
722
622
  }, [balances]);
723
623
  const getStaleChains = balances => [...new Set(balances.sorted.filter(b => b.status === "stale").map(b => b.chain?.name ?? b.chainId ?? "Unknown"))];
724
624
 
625
+ const useChainConnectors = () => jotai.useAtomValue(chainConnectorsAtom);
626
+
627
+ const useChaindataProvider = () => jotai.useAtomValue(chaindataProviderAtom);
628
+ const useChaindata = () => jotai.useAtomValue(chaindataAtom);
629
+ const useChains = () => jotai.useAtomValue(chainsByIdAtom);
630
+ const useChainsByGenesisHash = () => jotai.useAtomValue(chainsByGenesisHashAtom);
631
+ const useEvmNetworks = () => jotai.useAtomValue(evmNetworksByIdAtom);
632
+ const useTokens = () => jotai.useAtomValue(tokensByIdAtom);
633
+ const useMiniMetadatas = () => jotai.useAtomValue(miniMetadatasAtom);
634
+ const useChain = chainId => useChains()[chainId ?? ""] ?? undefined;
635
+ const useEvmNetwork = evmNetworkId => useEvmNetworks()[evmNetworkId ?? ""] ?? undefined;
636
+ const useToken = tokenId => useTokens()[tokenId ?? ""] ?? undefined;
637
+
638
+ const useTokenRates = () => jotai.useAtomValue(tokenRatesAtom);
639
+ const useTokenRate = tokenId => useTokenRates()[tokenId ?? ""] ?? undefined;
640
+
725
641
  const BalancesProvider = ({
726
642
  balanceModules,
727
643
  onfinalityApiKey,
644
+ coingeckoApiUrl,
645
+ coingeckoApiKeyName,
646
+ coingeckoApiKeyValue,
728
647
  withTestnets,
729
648
  enabledChains,
649
+ enabledTokens,
730
650
  children
731
- }) => /*#__PURE__*/jsxRuntime.jsx(WithTestnetsProvider, {
732
- withTestnets: withTestnets,
733
- children: /*#__PURE__*/jsxRuntime.jsx(EnabledChainsProvider, {
734
- enabledChains: enabledChains,
735
- children: /*#__PURE__*/jsxRuntime.jsx(ChaindataProvider, {
736
- onfinalityApiKey: onfinalityApiKey,
737
- children: /*#__PURE__*/jsxRuntime.jsx(ChainConnectorsProvider, {
738
- onfinalityApiKey: onfinalityApiKey,
739
- children: /*#__PURE__*/jsxRuntime.jsx(AllAddressesProvider, {
740
- children: /*#__PURE__*/jsxRuntime.jsx(BalanceModulesProvider, {
741
- balanceModules: balanceModules,
742
- children: /*#__PURE__*/jsxRuntime.jsx(DbCacheProvider, {
743
- children: children
744
- })
745
- })
746
- })
747
- })
748
- })
749
- })
750
- });
651
+ }) => {
652
+ const setBalanceModules = jotai.useSetAtom(balanceModuleCreatorsAtom);
653
+ react.useEffect(() => {
654
+ if (balanceModules !== undefined) setBalanceModules(balanceModules);
655
+ }, [balanceModules, setBalanceModules]);
656
+ const setOnfinalityApiKey = jotai.useSetAtom(onfinalityApiKeyAtom);
657
+ react.useEffect(() => {
658
+ setOnfinalityApiKey(onfinalityApiKey);
659
+ }, [onfinalityApiKey, setOnfinalityApiKey]);
660
+ const setCoingeckoConfig = jotai.useSetAtom(coingeckoConfigAtom);
661
+ react.useEffect(() => {
662
+ setCoingeckoConfig({
663
+ apiUrl: coingeckoApiUrl,
664
+ apiKeyName: coingeckoApiKeyName,
665
+ apiKeyValue: coingeckoApiKeyValue
666
+ });
667
+ }, [coingeckoApiKeyName, coingeckoApiKeyValue, coingeckoApiUrl, setCoingeckoConfig]);
668
+ const setEnableTestnets = jotai.useSetAtom(enableTestnetsAtom);
669
+ react.useEffect(() => {
670
+ setEnableTestnets(withTestnets ?? false);
671
+ }, [setEnableTestnets, withTestnets]);
672
+ const setEnabledChains = jotai.useSetAtom(enabledChainsAtom);
673
+ react.useEffect(() => {
674
+ setEnabledChains(enabledChains);
675
+ }, [enabledChains, setEnabledChains]);
676
+ const setEnabledTokens = jotai.useSetAtom(enabledTokensAtom);
677
+ react.useEffect(() => {
678
+ setEnabledTokens(enabledTokens);
679
+ }, [enabledTokens, setEnabledTokens]);
680
+ return /*#__PURE__*/jsxRuntime.jsx(jsxRuntime.Fragment, {
681
+ children: children
682
+ });
683
+ };
751
684
 
752
- exports.AllAddressesProvider = AllAddressesProvider;
753
- exports.BalanceModulesProvider = BalanceModulesProvider;
685
+ Object.defineProperty(exports, "evmErc20TokenId", {
686
+ enumerable: true,
687
+ get: function () { return balances.evmErc20TokenId; }
688
+ });
689
+ Object.defineProperty(exports, "evmNativeTokenId", {
690
+ enumerable: true,
691
+ get: function () { return balances.evmNativeTokenId; }
692
+ });
693
+ Object.defineProperty(exports, "subAssetTokenId", {
694
+ enumerable: true,
695
+ get: function () { return balances.subAssetTokenId; }
696
+ });
697
+ Object.defineProperty(exports, "subEquilibriumTokenId", {
698
+ enumerable: true,
699
+ get: function () { return balances.subEquilibriumTokenId; }
700
+ });
701
+ Object.defineProperty(exports, "subNativeTokenId", {
702
+ enumerable: true,
703
+ get: function () { return balances.subNativeTokenId; }
704
+ });
705
+ Object.defineProperty(exports, "subPsp22TokenId", {
706
+ enumerable: true,
707
+ get: function () { return balances.subPsp22TokenId; }
708
+ });
709
+ Object.defineProperty(exports, "subTokensTokenId", {
710
+ enumerable: true,
711
+ get: function () { return balances.subTokensTokenId; }
712
+ });
754
713
  exports.BalancesProvider = BalancesProvider;
755
- exports.ChainConnectorsProvider = ChainConnectorsProvider;
756
- exports.ChaindataProvider = ChaindataProvider;
757
- exports.DbCacheProvider = DbCacheProvider;
758
- exports.WithTestnetsProvider = WithTestnetsProvider;
759
- exports.createMulticastSubscription = createMulticastSubscription;
714
+ exports.allAddressesAtom = allAddressesAtom;
715
+ exports.allBalancesAtom = allBalancesAtom;
716
+ exports.balanceModuleCreatorsAtom = balanceModuleCreatorsAtom;
717
+ exports.balanceModulesAtom = balanceModulesAtom;
718
+ exports.balancesPersistBackendAtom = balancesPersistBackendAtom;
719
+ exports.chainConnectorsAtom = chainConnectorsAtom;
720
+ exports.chaindataAtom = chaindataAtom;
721
+ exports.chaindataProviderAtom = chaindataProviderAtom;
722
+ exports.chainsAtom = chainsAtom;
723
+ exports.chainsByGenesisHashAtom = chainsByGenesisHashAtom;
724
+ exports.chainsByIdAtom = chainsByIdAtom;
725
+ exports.coingeckoConfigAtom = coingeckoConfigAtom;
726
+ exports.cryptoWaitReadyAtom = cryptoWaitReadyAtom;
727
+ exports.enableTestnetsAtom = enableTestnetsAtom;
728
+ exports.enabledChainsAtom = enabledChainsAtom;
729
+ exports.enabledTokensAtom = enabledTokensAtom;
730
+ exports.evmNetworksAtom = evmNetworksAtom;
731
+ exports.evmNetworksByIdAtom = evmNetworksByIdAtom;
760
732
  exports.getStaleChains = getStaleChains;
761
- exports.provideContext = provideContext;
762
- exports.useAllAddresses = useAllAddresses;
763
- exports.useBalanceModules = useBalanceModules;
733
+ exports.miniMetadataHydratedAtom = miniMetadataHydratedAtom;
734
+ exports.miniMetadatasAtom = miniMetadatasAtom;
735
+ exports.onfinalityApiKeyAtom = onfinalityApiKeyAtom;
736
+ exports.tokenRatesAtom = tokenRatesAtom;
737
+ exports.tokensAtom = tokensAtom;
738
+ exports.tokensByIdAtom = tokensByIdAtom;
764
739
  exports.useBalances = useBalances;
765
- exports.useBalancesHydrate = useBalancesHydrate;
766
740
  exports.useBalancesStatus = useBalancesStatus;
767
741
  exports.useChain = useChain;
768
742
  exports.useChainConnectors = useChainConnectors;
769
743
  exports.useChaindata = useChaindata;
744
+ exports.useChaindataProvider = useChaindataProvider;
770
745
  exports.useChains = useChains;
771
- exports.useDbCache = useDbCache;
772
- exports.useDbCacheBalancesSubscription = useDbCacheBalancesSubscription;
773
- exports.useDbCacheSubscription = useDbCacheSubscription;
774
- exports.useDbCacheTokenRatesSubscription = useDbCacheTokenRatesSubscription;
746
+ exports.useChainsByGenesisHash = useChainsByGenesisHash;
775
747
  exports.useEvmNetwork = useEvmNetwork;
776
748
  exports.useEvmNetworks = useEvmNetworks;
777
- exports.useMulticastSubscription = useMulticastSubscription;
749
+ exports.useMiniMetadatas = useMiniMetadatas;
750
+ exports.useSetBalancesAddresses = useSetBalancesAddresses;
778
751
  exports.useToken = useToken;
779
752
  exports.useTokenRate = useTokenRate;
780
753
  exports.useTokenRates = useTokenRates;
781
754
  exports.useTokens = useTokens;
782
- exports.useWithTestnets = useWithTestnets;