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