@talismn/balances-react 0.3.3 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/dist/declarations/src/hooks/index.d.ts +20 -0
- package/dist/declarations/src/hooks/useAllAddresses.d.ts +4 -0
- package/dist/declarations/src/hooks/useBalanceModules.d.ts +8 -0
- package/dist/declarations/src/hooks/useBalances.d.ts +3 -6
- package/dist/declarations/src/hooks/useBalancesHydrate.d.ts +2 -0
- package/dist/declarations/src/hooks/useBalancesStatus.d.ts +18 -0
- package/dist/declarations/src/hooks/useChainConnectors.d.ts +12 -0
- package/dist/declarations/src/hooks/useChaindata.d.ts +6 -23
- package/dist/declarations/src/hooks/useChains.d.ts +3 -0
- package/dist/declarations/src/hooks/useDbCache.d.ts +24 -0
- package/dist/declarations/src/hooks/useDbCacheSubscription.d.ts +13 -0
- package/dist/declarations/src/hooks/useEvmNetworks.d.ts +3 -0
- package/dist/declarations/src/hooks/useTokenRates.d.ts +3 -3
- package/dist/declarations/src/hooks/useTokens.d.ts +3 -0
- package/dist/declarations/src/hooks/useWithTestnets.d.ts +8 -0
- package/dist/declarations/src/index.d.ts +1 -0
- package/dist/declarations/src/util/index.d.ts +2 -0
- package/dist/declarations/src/util/provideContext.d.ts +9 -0
- package/dist/declarations/src/util/useMulticastSubscription.d.ts +16 -0
- package/dist/declarations/src/util/useSharedSubscription.d.ts +9 -0
- package/dist/talismn-balances-react.cjs.dev.js +628 -257
- package/dist/talismn-balances-react.cjs.prod.js +628 -257
- package/dist/talismn-balances-react.esm.js +606 -258
- package/package.json +17 -14
@@ -1,16 +1,182 @@
|
|
1
|
-
import {
|
1
|
+
import { useContext, createContext, useState, useEffect, useMemo, useRef, useCallback } from 'react';
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
2
3
|
import { ChainConnector } from '@talismn/chain-connector';
|
3
4
|
import { ChainConnectorEvm } from '@talismn/chain-connector-evm';
|
5
|
+
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';
|
4
9
|
import { useLiveQuery } from 'dexie-react-hooks';
|
5
|
-
import { useState, useEffect, useRef } from 'react';
|
6
10
|
import { useDebounce } from 'react-use';
|
11
|
+
import md5 from 'blueimp-md5';
|
7
12
|
import anylogger from 'anylogger';
|
8
|
-
import {
|
9
|
-
|
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
|
20
|
+
});
|
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]));
|
131
|
+
|
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);
|
10
176
|
|
11
177
|
var packageJson = {
|
12
178
|
name: "@talismn/balances-react",
|
13
|
-
version: "0.
|
179
|
+
version: "0.4.1",
|
14
180
|
author: "Talisman",
|
15
181
|
homepage: "https://talisman.xyz",
|
16
182
|
license: "UNLICENSED",
|
@@ -28,28 +194,31 @@ var packageJson = {
|
|
28
194
|
"/dist"
|
29
195
|
],
|
30
196
|
engines: {
|
31
|
-
node: ">=
|
197
|
+
node: ">=18"
|
32
198
|
},
|
33
199
|
scripts: {
|
34
200
|
test: "jest",
|
35
|
-
lint: "eslint
|
201
|
+
lint: "eslint src --max-warnings 0",
|
36
202
|
clean: "rm -rf dist && rm -rf .turbo rm -rf node_modules"
|
37
203
|
},
|
38
204
|
dependencies: {
|
39
|
-
"@talismn/balances": "workspace
|
40
|
-
"@talismn/chain-connector": "workspace
|
41
|
-
"@talismn/chain-connector-evm": "workspace
|
42
|
-
"@talismn/chaindata-provider": "workspace
|
43
|
-
"@talismn/chaindata-provider-extension": "workspace
|
44
|
-
"@talismn/
|
205
|
+
"@talismn/balances": "workspace:*",
|
206
|
+
"@talismn/chain-connector": "workspace:*",
|
207
|
+
"@talismn/chain-connector-evm": "workspace:*",
|
208
|
+
"@talismn/chaindata-provider": "workspace:*",
|
209
|
+
"@talismn/chaindata-provider-extension": "workspace:*",
|
210
|
+
"@talismn/connection-meta": "workspace:*",
|
211
|
+
"@talismn/token-rates": "workspace:*",
|
45
212
|
anylogger: "^1.0.11",
|
46
|
-
|
47
|
-
|
48
|
-
"react-
|
213
|
+
"blueimp-md5": "2.19.0",
|
214
|
+
dexie: "^3.2.3",
|
215
|
+
"dexie-react-hooks": "^1.1.3",
|
216
|
+
"react-use": "^17.4.0",
|
217
|
+
rxjs: "^7.8.1"
|
49
218
|
},
|
50
219
|
devDependencies: {
|
51
|
-
"@talismn/eslint-config": "workspace
|
52
|
-
"@talismn/tsconfig": "workspace
|
220
|
+
"@talismn/eslint-config": "workspace:*",
|
221
|
+
"@talismn/tsconfig": "workspace:*",
|
53
222
|
"@types/jest": "^27.5.1",
|
54
223
|
"@types/react": "^18.0.17",
|
55
224
|
eslint: "^8.4.0",
|
@@ -72,139 +241,394 @@ var packageJson = {
|
|
72
241
|
|
73
242
|
var log = anylogger(packageJson.name);
|
74
243
|
|
75
|
-
//
|
76
|
-
|
77
|
-
const [chaindataProvider, setChaindataProvider] = useState(null);
|
244
|
+
// global data store containing all subscriptions
|
245
|
+
const subscriptions = {};
|
78
246
|
|
79
|
-
|
80
|
-
|
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
|
+
};
|
81
257
|
useEffect(() => {
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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;
|
97
274
|
}
|
98
275
|
};
|
99
|
-
|
100
|
-
|
276
|
+
}, [key]);
|
277
|
+
|
278
|
+
// Initialize subscription
|
279
|
+
useEffect(() => {
|
280
|
+
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));
|
101
315
|
return () => {
|
102
|
-
|
316
|
+
typeof unsubscribe === "function" && unsubscribe();
|
103
317
|
};
|
104
|
-
}
|
105
|
-
|
106
|
-
|
318
|
+
});
|
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;
|
333
|
+
};
|
334
|
+
|
335
|
+
const useWithTestnetsProvider = ({
|
336
|
+
withTestnets
|
337
|
+
}) => {
|
338
|
+
return {
|
339
|
+
withTestnets
|
340
|
+
};
|
341
|
+
};
|
342
|
+
const [WithTestnetsProvider, useWithTestnets] = provideContext(useWithTestnetsProvider);
|
343
|
+
|
344
|
+
/**
|
345
|
+
* This hook is responsible for fetching the data used for balances and inserting it into the db.
|
346
|
+
*/
|
347
|
+
const useDbCacheSubscription = subscribeTo => {
|
348
|
+
const provider = useChaindata();
|
349
|
+
|
350
|
+
// can't handle balances & tokenRates here as they have other dependencies, it would trigger to many subscriptions
|
351
|
+
const subscribe = useCallback(() => {
|
352
|
+
switch (subscribeTo) {
|
353
|
+
case "chains":
|
354
|
+
return subscribeChainDataHydrate(provider, "chains");
|
355
|
+
case "evmNetworks":
|
356
|
+
return subscribeChainDataHydrate(provider, "evmNetworks");
|
357
|
+
case "tokens":
|
358
|
+
return subscribeChainDataHydrate(provider, "tokens");
|
359
|
+
}
|
360
|
+
}, [provider, subscribeTo]);
|
361
|
+
useSharedSubscription(subscribeTo, subscribe);
|
362
|
+
};
|
363
|
+
|
364
|
+
/**
|
365
|
+
* This hook is responsible for fetching the data used for token rates and inserting it into the db.
|
366
|
+
*/
|
367
|
+
function useDbCacheTokenRatesSubscription() {
|
368
|
+
const {
|
369
|
+
withTestnets
|
370
|
+
} = useWithTestnets();
|
371
|
+
const tokens = useTokens$1(withTestnets);
|
372
|
+
const subscriptionKey = useMemo(
|
373
|
+
// not super sexy but we need key to change based on this stuff
|
374
|
+
() => {
|
375
|
+
const key = Object.values(tokens ?? {}).map(({
|
376
|
+
id
|
377
|
+
}) => id).sort().join();
|
378
|
+
return `tokenRates-${md5(key)}`;
|
379
|
+
}, [tokens]);
|
380
|
+
const subscription = useCallback(() => {
|
381
|
+
if (!Object.values(tokens ?? {}).length) return () => {};
|
382
|
+
return subscribeTokenRates(tokens);
|
383
|
+
}, [tokens]);
|
384
|
+
useSharedSubscription(subscriptionKey, subscription);
|
107
385
|
}
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
386
|
+
|
387
|
+
/**
|
388
|
+
* This hook is responsible for fetching the data used for balances and inserting it into the db.
|
389
|
+
*/
|
390
|
+
function useDbCacheBalancesSubscription() {
|
391
|
+
const {
|
392
|
+
withTestnets
|
393
|
+
} = useWithTestnets();
|
394
|
+
const balanceModules = useBalanceModules();
|
395
|
+
const chaindataProvider = useChaindata();
|
396
|
+
const chainConnectors = useChainConnectors();
|
397
|
+
const [allAddresses] = useAllAddresses();
|
398
|
+
const tokens = useTokens$1(withTestnets);
|
399
|
+
const subscriptionKey = useMemo(
|
400
|
+
// not super sexy but we need key to change based on this stuff
|
401
|
+
() => {
|
402
|
+
const key = allAddresses.sort().join().concat(...Object.values(tokens ?? {}).map(({
|
403
|
+
id
|
404
|
+
}) => id).sort()).concat(`evm:${!!chainConnectors.evm}`, `sub:${!!chainConnectors.substrate}`, ...balanceModules.map(m => m.type).sort(), `cd:${!!chaindataProvider}`);
|
405
|
+
return `balances-${md5(key)}`;
|
406
|
+
}, [allAddresses, balanceModules, chainConnectors, chaindataProvider, tokens]);
|
407
|
+
const subscription = useCallback(() => {
|
408
|
+
if (!Object.values(tokens ?? {}).length || !allAddresses.length) return () => {};
|
409
|
+
return subscribeBalances(tokens ?? {}, allAddresses, balanceModules);
|
410
|
+
}, [allAddresses, balanceModules, tokens]);
|
411
|
+
useSharedSubscription(subscriptionKey, subscription);
|
412
|
+
}
|
413
|
+
|
414
|
+
// subscriptionless version of useTokens, prevents circular dependency
|
415
|
+
const useTokens$1 = withTestnets => {
|
416
|
+
const {
|
417
|
+
tokensWithTestnetsMap,
|
418
|
+
tokensWithoutTestnetsMap
|
419
|
+
} = useDbCache();
|
420
|
+
return withTestnets ? tokensWithTestnetsMap : tokensWithoutTestnetsMap;
|
421
|
+
};
|
422
|
+
const subscribeChainDataHydrate = (provider, type) => {
|
423
|
+
const chaindata = provider;
|
424
|
+
const delay = 300_000; // 300_000ms = 300s = 5 minutes
|
425
|
+
|
426
|
+
let timeout = null;
|
427
|
+
const hydrate = async () => {
|
428
|
+
try {
|
429
|
+
if (type === "chains") await chaindata.hydrateChains();
|
430
|
+
if (type === "evmNetworks") await chaindata.hydrateEvmNetworks();
|
431
|
+
if (type === "tokens") await chaindata.hydrateTokens();
|
432
|
+
timeout = setTimeout(hydrate, delay);
|
433
|
+
} catch (error) {
|
434
|
+
const retryTimeout = 5_000; // 5_000ms = 5 seconds
|
435
|
+
log.error(`Failed to fetch chaindata, retrying in ${Math.round(retryTimeout / 1000)} seconds`, error);
|
436
|
+
timeout = setTimeout(hydrate, retryTimeout);
|
437
|
+
}
|
438
|
+
};
|
439
|
+
|
440
|
+
// launch the loop
|
441
|
+
hydrate();
|
442
|
+
return () => {
|
443
|
+
if (timeout) clearTimeout(timeout);
|
444
|
+
};
|
445
|
+
};
|
446
|
+
const subscribeTokenRates = tokens => {
|
447
|
+
const REFRESH_INTERVAL = 300_000; // 6 minutes
|
448
|
+
const RETRY_INTERVAL = 5_000; // 5 sec
|
449
|
+
|
450
|
+
let timeout = null;
|
451
|
+
const refreshTokenRates = async () => {
|
452
|
+
try {
|
453
|
+
if (timeout) clearTimeout(timeout);
|
454
|
+
const tokenRates = await fetchTokenRates(tokens);
|
455
|
+
const putTokenRates = Object.entries(tokenRates).map(([tokenId, rates]) => ({
|
456
|
+
tokenId,
|
457
|
+
rates
|
458
|
+
}));
|
459
|
+
db.transaction("rw", db.tokenRates, async () => await db.tokenRates.bulkPut(putTokenRates));
|
460
|
+
timeout = setTimeout(() => {
|
461
|
+
refreshTokenRates();
|
462
|
+
}, REFRESH_INTERVAL);
|
463
|
+
} catch (error) {
|
464
|
+
log.error(`Failed to fetch tokenRates, retrying in ${Math.round(RETRY_INTERVAL / 1000)} seconds`, error);
|
465
|
+
setTimeout(async () => {
|
466
|
+
refreshTokenRates();
|
467
|
+
}, RETRY_INTERVAL);
|
468
|
+
}
|
469
|
+
};
|
470
|
+
|
471
|
+
// launch the loop
|
472
|
+
refreshTokenRates();
|
473
|
+
return () => {
|
474
|
+
if (timeout) clearTimeout(timeout);
|
475
|
+
};
|
476
|
+
};
|
477
|
+
const subscribeBalances = (tokens, addresses, balanceModules) => {
|
478
|
+
const tokenIds = Object.values(tokens).map(({
|
479
|
+
id
|
480
|
+
}) => id);
|
481
|
+
const addressesByToken = Object.fromEntries(tokenIds.map(tokenId => [tokenId, addresses]));
|
482
|
+
const subscriptionId = createSubscriptionId();
|
483
|
+
|
484
|
+
// TODO: Create subscriptions in a service worker, where we can detect page closes
|
485
|
+
// and therefore reliably delete the subscriptionId when the user closes our dapp
|
486
|
+
//
|
487
|
+
// For more information, check out https://developer.chrome.com/blog/page-lifecycle-api/#faqs
|
488
|
+
// and scroll down to:
|
489
|
+
// - `What is the back/forward cache?`, and
|
490
|
+
// - `If I can't run asynchronous APIs in the frozen or terminated states, how can I save data to IndexedDB?
|
491
|
+
//
|
492
|
+
// For now, we'll just last-ditch remove the subscriptionId (it works surprisingly well!) in the beforeunload event
|
493
|
+
window.onbeforeunload = () => {
|
494
|
+
deleteSubscriptionId();
|
495
|
+
};
|
496
|
+
const updateDb = balances => {
|
497
|
+
const putBalances = Object.entries(balances.toJSON()).map(([id, balance]) => ({
|
498
|
+
id,
|
499
|
+
...balance,
|
500
|
+
status: BalanceStatusLive(subscriptionId)
|
501
|
+
}));
|
502
|
+
db$1.transaction("rw", db$1.balances, async () => await db$1.balances.bulkPut(putBalances));
|
503
|
+
};
|
504
|
+
let unsubscribed = false;
|
505
|
+
|
506
|
+
// eslint-disable-next-line no-console
|
507
|
+
log.log("subscribing to balance changes for %d tokens and %d addresses", tokenIds.length, addresses.length);
|
508
|
+
const unsubs = balanceModules.map(async balanceModule => {
|
509
|
+
// filter out tokens to only include those which this module knows how to fetch balances for
|
510
|
+
const moduleTokenIds = Object.values(tokens ?? {}).filter(({
|
511
|
+
type
|
512
|
+
}) => type === balanceModule.type).map(({
|
513
|
+
id
|
514
|
+
}) => id);
|
515
|
+
const addressesByModuleToken = Object.fromEntries(Object.entries(addressesByToken).filter(([tokenId]) => moduleTokenIds.includes(tokenId)));
|
516
|
+
const unsub = balances(balanceModule, addressesByModuleToken, (error, balances) => {
|
517
|
+
// log errors
|
518
|
+
if (error) {
|
519
|
+
if (error?.type === "STALE_RPC_ERROR" || error?.type === "WEBSOCKET_ALLOCATION_EXHAUSTED_ERROR") return db$1.balances.where({
|
520
|
+
source: balanceModule.type,
|
521
|
+
chainId: error.chainId
|
522
|
+
}).filter(balance => {
|
523
|
+
if (!Object.keys(addressesByModuleToken).includes(balance.tokenId)) return false;
|
524
|
+
if (!addressesByModuleToken[balance.tokenId].includes(balance.address)) return false;
|
525
|
+
return true;
|
526
|
+
}).modify({
|
527
|
+
status: "stale"
|
528
|
+
});
|
529
|
+
return log.error(`Failed to fetch ${balanceModule.type} balances`, error);
|
530
|
+
}
|
531
|
+
// ignore empty balance responses
|
532
|
+
if (!balances) return;
|
533
|
+
// ignore balances from old subscriptions which are still in the process of unsubscribing
|
534
|
+
if (unsubscribed) return;
|
535
|
+
updateDb(balances);
|
116
536
|
});
|
117
|
-
|
118
|
-
|
537
|
+
return () => {
|
538
|
+
// wait 2 seconds before actually unsubscribing, allowing for websocket to be reused
|
539
|
+
unsub.then(unsubscribe => {
|
540
|
+
setTimeout(unsubscribe, 2_000);
|
541
|
+
});
|
542
|
+
deleteSubscriptionId();
|
543
|
+
};
|
544
|
+
});
|
545
|
+
const unsubscribeAll = () => {
|
546
|
+
unsubscribed = true;
|
547
|
+
unsubs.forEach(unsub => unsub.then(unsubscribe => unsubscribe()));
|
548
|
+
};
|
549
|
+
return unsubscribeAll;
|
550
|
+
};
|
551
|
+
|
552
|
+
function useChains(withTestnets) {
|
553
|
+
// keep db data up to date
|
554
|
+
useDbCacheSubscription("chains");
|
555
|
+
const {
|
556
|
+
chainsWithTestnetsMap,
|
557
|
+
chainsWithoutTestnetsMap
|
558
|
+
} = useDbCache();
|
559
|
+
return withTestnets ? chainsWithTestnetsMap : chainsWithoutTestnetsMap;
|
119
560
|
}
|
120
|
-
function useChain(
|
121
|
-
const
|
122
|
-
|
123
|
-
if (chaindata === null) return;
|
124
|
-
if (!chainId) return;
|
125
|
-
chaindata.getChain(chainId).then(setChain);
|
126
|
-
}, [chainId, chaindata, chaindata?.generation]);
|
127
|
-
return chain;
|
561
|
+
function useChain(chainId, withTestnets) {
|
562
|
+
const chains = useChains(withTestnets);
|
563
|
+
return chainId ? chains[chainId] : undefined;
|
128
564
|
}
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
}, [chaindata, chaindata?.generation]);
|
139
|
-
return evmNetworks || {};
|
565
|
+
|
566
|
+
function useEvmNetworks(withTestnets) {
|
567
|
+
// keep db data up to date
|
568
|
+
useDbCacheSubscription("evmNetworks");
|
569
|
+
const {
|
570
|
+
evmNetworksWithTestnetsMap,
|
571
|
+
evmNetworksWithoutTestnetsMap
|
572
|
+
} = useDbCache();
|
573
|
+
return withTestnets ? evmNetworksWithTestnetsMap : evmNetworksWithoutTestnetsMap;
|
140
574
|
}
|
141
|
-
function useEvmNetwork(
|
142
|
-
const
|
143
|
-
|
144
|
-
if (chaindata === null) return;
|
145
|
-
if (!evmNetworkId) return;
|
146
|
-
chaindata.getEvmNetwork(evmNetworkId).then(setEvmNetwork);
|
147
|
-
}, [chaindata, chaindata?.generation, evmNetworkId]);
|
148
|
-
return evmNetwork;
|
575
|
+
function useEvmNetwork(evmNetworkId, withTestnets) {
|
576
|
+
const evmNetworks = useEvmNetworks(withTestnets);
|
577
|
+
return evmNetworkId ? evmNetworks[evmNetworkId] : undefined;
|
149
578
|
}
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
});
|
159
|
-
}, [chaindata, chaindata?.generation]);
|
160
|
-
return tokens || {};
|
579
|
+
|
580
|
+
function useTokenRates() {
|
581
|
+
// keep db data up to date
|
582
|
+
useDbCacheTokenRatesSubscription();
|
583
|
+
const {
|
584
|
+
tokenRatesMap
|
585
|
+
} = useDbCache();
|
586
|
+
return tokenRatesMap;
|
161
587
|
}
|
162
|
-
function
|
163
|
-
const
|
164
|
-
|
165
|
-
if (chaindata === null) return;
|
166
|
-
if (!tokenId) return;
|
167
|
-
chaindata.getToken(tokenId).then(setToken);
|
168
|
-
}, [chaindata, chaindata?.generation, tokenId]);
|
169
|
-
return token;
|
588
|
+
function useTokenRate(tokenId) {
|
589
|
+
const tokenRates = useTokenRates();
|
590
|
+
return tokenId ? tokenRates[tokenId] : undefined;
|
170
591
|
}
|
171
592
|
|
172
|
-
function
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
if (thisGeneration !== generation.current) return;
|
185
|
-
setTokenRates(tokenRates);
|
186
|
-
});
|
187
|
-
}, [tokens]);
|
188
|
-
return tokenRates || {};
|
593
|
+
function useTokens(withTestnets) {
|
594
|
+
// keep db data up to date
|
595
|
+
useDbCacheSubscription("tokens");
|
596
|
+
const {
|
597
|
+
tokensWithTestnetsMap,
|
598
|
+
tokensWithoutTestnetsMap
|
599
|
+
} = useDbCache();
|
600
|
+
return withTestnets ? tokensWithTestnetsMap : tokensWithoutTestnetsMap;
|
601
|
+
}
|
602
|
+
function useToken(tokenId, withTestnets) {
|
603
|
+
const tokens = useTokens(withTestnets);
|
604
|
+
return tokenId ? tokens[tokenId] : undefined;
|
189
605
|
}
|
190
606
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
607
|
+
const useBalancesHydrate = () => {
|
608
|
+
const {
|
609
|
+
withTestnets
|
610
|
+
} = useWithTestnets();
|
611
|
+
const chains = useChains(withTestnets);
|
612
|
+
const evmNetworks = useEvmNetworks(withTestnets);
|
613
|
+
const tokens = useTokens(withTestnets);
|
614
|
+
const tokenRates = useTokenRates();
|
615
|
+
return useMemo(() => ({
|
616
|
+
chains,
|
617
|
+
evmNetworks,
|
618
|
+
tokens,
|
619
|
+
tokenRates
|
620
|
+
}), [chains, evmNetworks, tokens, tokenRates]);
|
621
|
+
};
|
622
|
+
|
623
|
+
function useBalances(addressesByToken) {
|
624
|
+
// keep db data up to date
|
625
|
+
useDbCacheBalancesSubscription();
|
626
|
+
const balanceModules = useBalanceModules();
|
627
|
+
const {
|
628
|
+
balances
|
629
|
+
} = useDbCache();
|
630
|
+
const hydrate = useBalancesHydrate();
|
631
|
+
return useMemo(() => new Balances(deriveStatuses([...getValidSubscriptionIds()], balances.filter(balance => {
|
208
632
|
// check that this balance is included in our queried balance modules
|
209
633
|
if (!balanceModules.map(({
|
210
634
|
type
|
@@ -221,136 +645,60 @@ balanceModules, chaindataProvider, addressesByToken, options = {}) {
|
|
221
645
|
|
222
646
|
// keep this balance
|
223
647
|
return true;
|
224
|
-
})
|
648
|
+
})),
|
225
649
|
// hydrate balance chains, evmNetworks, tokens and tokenRates
|
226
|
-
|
227
|
-
chains,
|
228
|
-
evmNetworks,
|
229
|
-
tokens,
|
230
|
-
tokenRates
|
231
|
-
}), [balanceModules, addressesByToken, chains, evmNetworks, tokens, tokenRates]);
|
232
|
-
|
233
|
-
// debounce every 100ms to prevent hammering UI with updates
|
234
|
-
const [debouncedBalances, setDebouncedBalances] = useState(balances);
|
235
|
-
useDebounce(() => balances && setDebouncedBalances(balances), 100, [balances]);
|
236
|
-
return debouncedBalances;
|
650
|
+
hydrate), [balances, hydrate, balanceModules, addressesByToken]);
|
237
651
|
}
|
238
652
|
|
239
|
-
|
240
|
-
|
653
|
+
/**
|
654
|
+
* Given a collection of `Balances`, this hook returns a `BalancesStatus` summary for the collection.
|
655
|
+
*
|
656
|
+
* @param balances The collection of balances to get the status from.
|
657
|
+
* @returns An instance of `BalancesStatus` which represents the status of the balances collection.
|
241
658
|
|
242
|
-
|
243
|
-
|
244
|
-
//
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
// >({})
|
251
|
-
|
252
|
-
const addSubscription = (key, balanceModule, chainConnectors, chaindataProvider, addressesByToken) => {
|
253
|
-
// create subscription if it doesn't already exist
|
254
|
-
if (!subscriptions[key] || subscriptions[key].refcount === 0) {
|
255
|
-
const generation = ((subscriptions[key]?.generation || 0) + 1) % Number.MAX_SAFE_INTEGER;
|
256
|
-
const unsub = balances(balanceModule, chainConnectors, chaindataProvider, addressesByToken, (error, balances) => {
|
257
|
-
if (error) return log.error(`Failed to fetch ${balanceModule.type} balances`, error);
|
258
|
-
if (!balances) return;
|
259
|
-
|
260
|
-
// ignore balances from old subscriptions which are still in the process of unsubscribing
|
261
|
-
if (subscriptions[key].generation !== generation) return;
|
262
|
-
const putBalances = Object.entries(balances.toJSON()).map(([id, balance]) => ({
|
263
|
-
id,
|
264
|
-
...balance
|
265
|
-
}));
|
266
|
-
db.transaction("rw", db.balances, async () => await db.balances.bulkPut(putBalances));
|
267
|
-
});
|
268
|
-
subscriptions[key] = {
|
269
|
-
unsub,
|
270
|
-
refcount: 0,
|
271
|
-
generation
|
272
|
-
};
|
273
|
-
}
|
659
|
+
*/
|
660
|
+
const useBalancesStatus = balances => useMemo(() => {
|
661
|
+
// stale
|
662
|
+
const staleChains = getStaleChains(balances);
|
663
|
+
if (staleChains.length > 0) return {
|
664
|
+
status: "stale",
|
665
|
+
staleChains
|
666
|
+
};
|
274
667
|
|
275
|
-
|
276
|
-
|
668
|
+
// fetching
|
669
|
+
const hasCachedBalances = balances.each.some(b => b.status === "cache");
|
670
|
+
if (hasCachedBalances) return {
|
671
|
+
status: "fetching"
|
277
672
|
};
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
// drop the refcount by one
|
283
|
-
subscriptions[key].refcount -= 1;
|
284
|
-
|
285
|
-
// unsubscribe if refcount is now 0 (nobody wants this subcription anymore)
|
286
|
-
if (subscriptions[key].refcount < 1) {
|
287
|
-
// remove subscription
|
288
|
-
subscriptions[key].unsub.then(unsub => unsub());
|
289
|
-
delete subscriptions[key];
|
290
|
-
|
291
|
-
// set this subscription's balances in the store to status: cache
|
292
|
-
db.transaction("rw", db.balances, async () => await db.balances.filter(balance => {
|
293
|
-
if (balance.source !== balanceModule.type) return false;
|
294
|
-
if (!Object.keys(addressesByToken).includes(balance.tokenId)) return false;
|
295
|
-
if (!addressesByToken[balance.tokenId].includes(balance.address)) return false;
|
296
|
-
return true;
|
297
|
-
}).modify({
|
298
|
-
status: "cache"
|
299
|
-
}));
|
300
|
-
}
|
673
|
+
|
674
|
+
// live
|
675
|
+
return {
|
676
|
+
status: "live"
|
301
677
|
};
|
302
|
-
|
303
|
-
|
304
|
-
const tokens = useTokens(chaindataProvider);
|
305
|
-
useEffect(() => {
|
306
|
-
if (chainConnector === null) return;
|
307
|
-
if (chainConnectorEvm === null) return;
|
308
|
-
if (chaindataProvider === null) return;
|
309
|
-
if (addressesByToken === null) return;
|
310
|
-
const unsubs = balanceModules.map(balanceModule => {
|
311
|
-
const subscriptionKey = `${balanceModule.type}-${JSON.stringify(addressesByToken)}`;
|
312
|
-
|
313
|
-
// filter out tokens to only include those which this module knows how to fetch balances for
|
314
|
-
const moduleTokenIds = Object.values(tokens).filter(({
|
315
|
-
type
|
316
|
-
}) => type === balanceModule.type).map(({
|
317
|
-
id
|
318
|
-
}) => id);
|
319
|
-
const addressesByModuleToken = Object.fromEntries(Object.entries(addressesByToken).filter(([tokenId]) => moduleTokenIds.includes(tokenId)));
|
320
|
-
|
321
|
-
// add balance subscription for this module
|
322
|
-
addSubscription(subscriptionKey, balanceModule, {
|
323
|
-
substrate: chainConnector,
|
324
|
-
evm: chainConnectorEvm
|
325
|
-
}, chaindataProvider, addressesByModuleToken);
|
326
|
-
|
327
|
-
// return an unsub method, to be called when this effect unmounts
|
328
|
-
return () => removeSubscription(subscriptionKey, balanceModule, addressesByToken);
|
329
|
-
});
|
330
|
-
const unsubAll = () => unsubs.forEach(unsub => unsub());
|
331
|
-
return unsubAll;
|
332
|
-
}, [addressesByToken, balanceModules, chainConnector, chainConnectorEvm, chaindataProvider, tokens]);
|
333
|
-
}
|
678
|
+
}, [balances]);
|
679
|
+
const getStaleChains = balances => [...new Set(balances.sorted.filter(b => b.status === "stale").map(b => b.chain?.name ?? b.chainId ?? "Unknown"))];
|
334
680
|
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
}
|
681
|
+
const BalancesProvider = ({
|
682
|
+
balanceModules,
|
683
|
+
onfinalityApiKey,
|
684
|
+
withTestnets,
|
685
|
+
children
|
686
|
+
}) => /*#__PURE__*/jsx(WithTestnetsProvider, {
|
687
|
+
withTestnets: withTestnets,
|
688
|
+
children: /*#__PURE__*/jsx(ChaindataProvider, {
|
689
|
+
onfinalityApiKey: onfinalityApiKey,
|
690
|
+
children: /*#__PURE__*/jsx(ChainConnectorsProvider, {
|
691
|
+
onfinalityApiKey: onfinalityApiKey,
|
692
|
+
children: /*#__PURE__*/jsx(AllAddressesProvider, {
|
693
|
+
children: /*#__PURE__*/jsx(BalanceModulesProvider, {
|
694
|
+
balanceModules: balanceModules,
|
695
|
+
children: /*#__PURE__*/jsx(DbCacheProvider, {
|
696
|
+
children: children
|
697
|
+
})
|
698
|
+
})
|
699
|
+
})
|
700
|
+
})
|
701
|
+
})
|
702
|
+
});
|
355
703
|
|
356
|
-
export { useBalances, useChain, useChaindata, useChains, useEvmNetwork, useEvmNetworks, useToken, useTokenRates, useTokens };
|
704
|
+
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 };
|