@talismn/balances-react 0.0.0-pr2075-20250710131942 → 0.0.0-pr2076-20250703120513
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/dist/declarations/src/atoms/balanceModules.d.ts +2 -0
- package/dist/declarations/src/atoms/balances.d.ts +5 -1
- package/dist/declarations/src/atoms/chaindata.d.ts +1615 -481
- package/dist/declarations/src/atoms/config.d.ts +4 -0
- package/dist/declarations/src/hooks/useBalances.d.ts +2 -1
- package/dist/declarations/src/hooks/useChaindata.d.ts +694 -470
- package/dist/declarations/src/index.d.ts +8 -2
- package/dist/declarations/src/util/balancesPersist.d.ts +10 -0
- package/dist/talismn-balances-react.cjs.dev.js +351 -103
- package/dist/talismn-balances-react.cjs.prod.js +351 -103
- package/dist/talismn-balances-react.esm.js +341 -105
- package/package.json +13 -13
- package/dist/declarations/src/atoms/balancesProvider.d.ts +0 -2
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
import { atom, useSetAtom, useAtomValue } from 'jotai';
|
|
1
|
+
import { atom, useSetAtom, useAtom, useAtomValue } from 'jotai';
|
|
2
2
|
import { useMemo, useEffect } from 'react';
|
|
3
|
-
import {
|
|
3
|
+
import { defaultBalanceModules, configureStore, decompress, compress, db, Balances, getBalanceId, balances } from '@talismn/balances';
|
|
4
|
+
import { DEFAULT_COINSAPI_CONFIG, db as db$1, fetchTokenRates, ALL_CURRENCY_IDS } from '@talismn/token-rates';
|
|
4
5
|
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
5
6
|
import { ChaindataProvider } from '@talismn/chaindata-provider';
|
|
6
7
|
export { evmErc20TokenId, evmNativeTokenId, subAssetTokenId, subNativeTokenId, subPsp22TokenId, subTokensTokenId } from '@talismn/chaindata-provider';
|
|
7
|
-
import {
|
|
8
|
+
import { firstThenDebounce, isTruthy, isAbortError, isEthereumAddress } from '@talismn/util';
|
|
8
9
|
import { atomEffect } from 'jotai-effect';
|
|
9
|
-
import {
|
|
10
|
+
import { atomWithObservable } from 'jotai/utils';
|
|
11
|
+
import { isEqual as isEqual$1 } from 'lodash';
|
|
12
|
+
import { Observable, distinctUntilChanged, map, combineLatest, BehaviorSubject, debounceTime, firstValueFrom } from 'rxjs';
|
|
13
|
+
import anylogger from 'anylogger';
|
|
10
14
|
import { ChainConnector } from '@talismn/chain-connector';
|
|
11
15
|
import { ChainConnectorEvm } from '@talismn/chain-connector-evm';
|
|
12
16
|
import { connectionMetaDb } from '@talismn/connection-meta';
|
|
13
|
-
import { firstThenDebounce, isTruthy, isAbortError } from '@talismn/util';
|
|
14
|
-
import { atomWithObservable } from 'jotai/utils';
|
|
15
|
-
import { combineLatest, Observable, map } from 'rxjs';
|
|
16
17
|
import { liveQuery } from 'dexie';
|
|
17
|
-
import
|
|
18
|
+
import isEqual from 'lodash/isEqual';
|
|
18
19
|
import { cryptoWaitReady } from '@polkadot/util-crypto';
|
|
19
20
|
|
|
21
|
+
const balanceModuleCreatorsAtom = atom(defaultBalanceModules);
|
|
20
22
|
const innerCoinsApiConfigAtom = atom(DEFAULT_COINSAPI_CONFIG);
|
|
21
23
|
const coinsApiConfigAtom = atom(get => get(innerCoinsApiConfigAtom), (_get, set, options) => set(innerCoinsApiConfigAtom, {
|
|
22
24
|
apiUrl: options.apiUrl ?? DEFAULT_COINSAPI_CONFIG.apiUrl
|
|
@@ -28,6 +30,55 @@ const enabledTokensAtom = atom(undefined);
|
|
|
28
30
|
/** Sets the list of addresses for which token balances will be fetched by the balances subscription */
|
|
29
31
|
const allAddressesAtom = atom([]);
|
|
30
32
|
|
|
33
|
+
var packageJson = {
|
|
34
|
+
name: "@talismn/balances-react"};
|
|
35
|
+
|
|
36
|
+
var log = anylogger(packageJson.name);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
// Persistence backend for indexedDB
|
|
40
|
+
// Add a new backend by implementing the BalancesPersistBackend interface
|
|
41
|
+
// configureStore can be called with a different indexedDB table
|
|
42
|
+
*/
|
|
43
|
+
const {
|
|
44
|
+
persistData,
|
|
45
|
+
retrieveData
|
|
46
|
+
} = configureStore();
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
// Persistence backend for localStorage
|
|
50
|
+
*/
|
|
51
|
+
const localStoragePersist = async balances => {
|
|
52
|
+
const storedBalances = balances.map(b => {
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
54
|
+
const {
|
|
55
|
+
status,
|
|
56
|
+
...rest
|
|
57
|
+
} = b;
|
|
58
|
+
return rest;
|
|
59
|
+
});
|
|
60
|
+
const deflated = compress(storedBalances);
|
|
61
|
+
localStorage.setItem("talismanBalances", deflated.toString());
|
|
62
|
+
};
|
|
63
|
+
const localStorageRetrieve = async () => {
|
|
64
|
+
const deflated = localStorage.getItem("talismanBalances");
|
|
65
|
+
if (deflated) {
|
|
66
|
+
// deflated will be a long string of numbers separated by commas
|
|
67
|
+
const deflatedArray = deflated.split(",").map(n => parseInt(n, 10));
|
|
68
|
+
const deflatedBytes = new Uint8Array(deflatedArray.length);
|
|
69
|
+
deflatedArray.forEach((n, i) => deflatedBytes[i] = n);
|
|
70
|
+
return decompress(deflatedBytes).map(b => ({
|
|
71
|
+
...b,
|
|
72
|
+
status: "cache"
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
return [];
|
|
76
|
+
};
|
|
77
|
+
const localStorageBalancesPersistBackend = {
|
|
78
|
+
persist: localStoragePersist,
|
|
79
|
+
retrieve: localStorageRetrieve
|
|
80
|
+
};
|
|
81
|
+
|
|
31
82
|
const chaindataProviderAtom = atom(() => {
|
|
32
83
|
return new ChaindataProvider({});
|
|
33
84
|
});
|
|
@@ -42,44 +93,19 @@ const chainConnectorsAtom = atom(get => {
|
|
|
42
93
|
};
|
|
43
94
|
});
|
|
44
95
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
});
|
|
56
|
-
const filteredChaindataAtom = atom(async get => {
|
|
57
|
-
const enabledNetworkIds = get(enabledChainsAtom);
|
|
58
|
-
const enabledTokenIds = get(enabledTokensAtom);
|
|
59
|
-
const enableTestnets = get(enableTestnetsAtom);
|
|
60
|
-
const chaindata = await get(chaindataAtom);
|
|
61
|
-
const networks = chaindata.networks.filter(n => (enabledNetworkIds?.includes(n.id) || n.isDefault) && (enableTestnets || !n.isTestnet));
|
|
62
|
-
const networkById = keyBy(networks, n => n.id);
|
|
63
|
-
const tokens = chaindata.tokens.filter(token => (enabledTokenIds?.includes(token.id) || token.isDefault) && networkById[token.networkId]);
|
|
64
|
-
return {
|
|
65
|
-
networks,
|
|
66
|
-
tokens
|
|
67
|
-
};
|
|
68
|
-
});
|
|
69
|
-
const tokensAtom = atom(async get => {
|
|
70
|
-
const chaindata = await get(filteredChaindataAtom);
|
|
71
|
-
return chaindata.tokens;
|
|
72
|
-
});
|
|
73
|
-
const networksAtom = atom(async get => {
|
|
74
|
-
const chaindata = await get(filteredChaindataAtom);
|
|
75
|
-
return chaindata.networks;
|
|
96
|
+
const balanceModulesAtom = atom(get => {
|
|
97
|
+
const balanceModuleCreators = get(balanceModuleCreatorsAtom);
|
|
98
|
+
const chainConnectors = get(chainConnectorsAtom);
|
|
99
|
+
const chaindataProvider = get(chaindataProviderAtom);
|
|
100
|
+
if (!chainConnectors.substrate) return [];
|
|
101
|
+
if (!chainConnectors.evm) return [];
|
|
102
|
+
if (!chaindataProvider) return [];
|
|
103
|
+
return balanceModuleCreators.map(mod => mod({
|
|
104
|
+
chainConnectors,
|
|
105
|
+
chaindataProvider
|
|
106
|
+
}));
|
|
76
107
|
});
|
|
77
108
|
|
|
78
|
-
var packageJson = {
|
|
79
|
-
name: "@talismn/balances-react"};
|
|
80
|
-
|
|
81
|
-
var log = anylogger(packageJson.name);
|
|
82
|
-
|
|
83
109
|
/**
|
|
84
110
|
* Converts a dexie Observable into an rxjs Observable.
|
|
85
111
|
*/
|
|
@@ -93,6 +119,54 @@ function dexieToRxjs(o) {
|
|
|
93
119
|
});
|
|
94
120
|
}
|
|
95
121
|
|
|
122
|
+
const chainsAtom = atom(async get => (await get(chaindataAtom)).chains);
|
|
123
|
+
const chainsByIdAtom = atom(async get => (await get(chaindataAtom)).chainsById);
|
|
124
|
+
const chainsByGenesisHashAtom = atom(async get => (await get(chaindataAtom)).chainsByGenesisHash);
|
|
125
|
+
const evmNetworksAtom = atom(async get => (await get(chaindataAtom)).evmNetworks);
|
|
126
|
+
const evmNetworksByIdAtom = atom(async get => (await get(chaindataAtom)).evmNetworksById);
|
|
127
|
+
const tokensAtom = atom(async get => (await get(chaindataAtom)).tokens);
|
|
128
|
+
const tokensByIdAtom = atom(async get => (await get(chaindataAtom)).tokensById);
|
|
129
|
+
const miniMetadatasAtom = atom(async get => (await get(chaindataAtom)).miniMetadatas);
|
|
130
|
+
const chaindataAtom = atomWithObservable(get => {
|
|
131
|
+
const enableTestnets = get(enableTestnetsAtom);
|
|
132
|
+
const filterTestnets = items => enableTestnets ? items : items.filter(({
|
|
133
|
+
isTestnet
|
|
134
|
+
}) => !isTestnet);
|
|
135
|
+
const filterMapTestnets = items => enableTestnets ? items : Object.fromEntries(Object.entries(items).filter(([, {
|
|
136
|
+
isTestnet
|
|
137
|
+
}]) => !isTestnet));
|
|
138
|
+
const filterEnabledTokens = tokens => tokens.filter(token => token.isDefault || "isCustom" in token && token.isCustom);
|
|
139
|
+
const filterMapEnabledTokens = tokensById => Object.fromEntries(Object.entries(tokensById).filter(([, token]) => token.isDefault || "isCustom" in token && token.isCustom));
|
|
140
|
+
const distinctUntilIsEqual = distinctUntilChanged((a, b) => isEqual(a, b));
|
|
141
|
+
const chains = get(chaindataProviderAtom).getNetworks$("polkadot").pipe(distinctUntilIsEqual, map(filterTestnets), distinctUntilIsEqual);
|
|
142
|
+
const chainsById = get(chaindataProviderAtom).getNetworksMapById$("polkadot").pipe(distinctUntilIsEqual, map(filterMapTestnets), distinctUntilIsEqual);
|
|
143
|
+
const chainsByGenesisHash = get(chaindataProviderAtom).getNetworksMapByGenesisHash$().pipe(distinctUntilIsEqual, map(filterMapTestnets), distinctUntilIsEqual);
|
|
144
|
+
const evmNetworks = get(chaindataProviderAtom).getNetworks$("ethereum").pipe(distinctUntilIsEqual, map(filterTestnets), distinctUntilIsEqual);
|
|
145
|
+
const networks = get(chaindataProviderAtom).getNetworks$().pipe(distinctUntilIsEqual, map(filterTestnets), distinctUntilIsEqual);
|
|
146
|
+
const evmNetworksById = get(chaindataProviderAtom).getNetworksMapById$("ethereum").pipe(distinctUntilIsEqual, map(filterMapTestnets), distinctUntilIsEqual);
|
|
147
|
+
const networksById = get(chaindataProviderAtom).getNetworksMapById$().pipe(distinctUntilIsEqual, map(filterMapTestnets), distinctUntilIsEqual);
|
|
148
|
+
const tokens = get(chaindataProviderAtom).tokens$.pipe(distinctUntilIsEqual, map(filterEnabledTokens), distinctUntilIsEqual);
|
|
149
|
+
const tokensById = get(chaindataProviderAtom).getTokensMapById$().pipe(distinctUntilIsEqual, map(filterMapEnabledTokens), distinctUntilIsEqual);
|
|
150
|
+
const miniMetadatasObservable = dexieToRxjs(liveQuery(() => db.miniMetadatas.toArray()));
|
|
151
|
+
const miniMetadatas = combineLatest([miniMetadatasObservable.pipe(distinctUntilIsEqual), chainsById]).pipe(map(([miniMetadatas, chainsById]) => miniMetadatas.filter(m => chainsById[m.chainId])), distinctUntilIsEqual);
|
|
152
|
+
return combineLatest({
|
|
153
|
+
networks,
|
|
154
|
+
networksById,
|
|
155
|
+
chains,
|
|
156
|
+
chainsById,
|
|
157
|
+
chainsByGenesisHash,
|
|
158
|
+
evmNetworks,
|
|
159
|
+
evmNetworksById,
|
|
160
|
+
tokens,
|
|
161
|
+
tokensById,
|
|
162
|
+
miniMetadatas
|
|
163
|
+
}).pipe(
|
|
164
|
+
// debounce to prevent hammering UI with updates
|
|
165
|
+
firstThenDebounce(1_000), distinctUntilIsEqual);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const cryptoWaitReadyAtom = atom(async () => await cryptoWaitReady());
|
|
169
|
+
|
|
96
170
|
const tokenRatesAtom = atom(async get => {
|
|
97
171
|
// runs a timer to keep tokenRates up to date
|
|
98
172
|
get(tokenRatesFetcherAtomEffect);
|
|
@@ -105,7 +179,7 @@ const tokenRatesDbAtom = atomWithObservable(() => {
|
|
|
105
179
|
}) => [tokenId, rates]));
|
|
106
180
|
|
|
107
181
|
// retrieve fetched tokenRates from the db
|
|
108
|
-
return dexieToRxjs(liveQuery(() => db.tokenRates.toArray())).pipe(map(dbRatesToMap));
|
|
182
|
+
return dexieToRxjs(liveQuery(() => db$1.tokenRates.toArray())).pipe(map(dbRatesToMap));
|
|
109
183
|
});
|
|
110
184
|
const tokenRatesFetcherAtomEffect = atomEffect(get => {
|
|
111
185
|
// lets us tear down the existing timer when the effect is restarted
|
|
@@ -113,9 +187,9 @@ const tokenRatesFetcherAtomEffect = atomEffect(get => {
|
|
|
113
187
|
|
|
114
188
|
// we have to get these synchronously so that jotai knows to restart our timer when they change
|
|
115
189
|
const coinsApiConfig = get(coinsApiConfigAtom);
|
|
116
|
-
const
|
|
190
|
+
const tokensByIdPromise = get(tokensByIdAtom);
|
|
117
191
|
(async () => {
|
|
118
|
-
const tokensById =
|
|
192
|
+
const tokensById = await tokensByIdPromise;
|
|
119
193
|
const tokenIds = Object.keys(tokensById);
|
|
120
194
|
const loopMs = 300_000; // 300_000ms = 300s = 5 minutes
|
|
121
195
|
const retryTimeout = 5_000; // 5_000ms = 5 seconds
|
|
@@ -129,15 +203,15 @@ const tokenRatesFetcherAtomEffect = atomEffect(get => {
|
|
|
129
203
|
rates
|
|
130
204
|
}));
|
|
131
205
|
if (abort.signal.aborted) return; // don't insert into db if aborted
|
|
132
|
-
await db.transaction("rw", db.tokenRates, async () => {
|
|
206
|
+
await db$1.transaction("rw", db$1.tokenRates, async () => {
|
|
133
207
|
// override all tokenRates
|
|
134
|
-
await db.tokenRates.bulkPut(putTokenRates);
|
|
208
|
+
await db$1.tokenRates.bulkPut(putTokenRates);
|
|
135
209
|
|
|
136
210
|
// delete tokenRates for tokens which no longer exist
|
|
137
211
|
const validTokenIds = new Set(tokenIds);
|
|
138
|
-
const tokenRatesIds = await db.tokenRates.toCollection().primaryKeys();
|
|
212
|
+
const tokenRatesIds = await db$1.tokenRates.toCollection().primaryKeys();
|
|
139
213
|
const deleteIds = tokenRatesIds.filter(id => !validTokenIds.has(id));
|
|
140
|
-
if (deleteIds.length > 0) await db.tokenRates.bulkDelete(deleteIds);
|
|
214
|
+
if (deleteIds.length > 0) await db$1.tokenRates.bulkDelete(deleteIds);
|
|
141
215
|
});
|
|
142
216
|
if (abort.signal.aborted) return; // don't schedule next loop if aborted
|
|
143
217
|
setTimeout(hydrate, loopMs);
|
|
@@ -156,42 +230,213 @@ const tokenRatesFetcherAtomEffect = atomEffect(get => {
|
|
|
156
230
|
return () => abort.abort("Unsubscribed");
|
|
157
231
|
});
|
|
158
232
|
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
233
|
+
const allBalancesAtom = atom(async get => {
|
|
234
|
+
// set up our subscription to fetch balances from the various blockchains
|
|
235
|
+
get(balancesSubscriptionAtomEffect);
|
|
236
|
+
const [balances, hydrateData] = await Promise.all([get(balancesObservableAtom), get(balancesHydrateDataAtom)]);
|
|
237
|
+
return new Balances(Object.values(balances).filter(balance => !!hydrateData?.tokens?.[balance.tokenId]),
|
|
238
|
+
// hydrate balance chains, evmNetworks, tokens and tokenRates
|
|
239
|
+
hydrateData);
|
|
162
240
|
});
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
};
|
|
174
|
-
})();
|
|
175
|
-
return () => {
|
|
176
|
-
unsub.then(unsubscribe => unsubscribe());
|
|
177
|
-
};
|
|
241
|
+
const balancesObservable = new BehaviorSubject({});
|
|
242
|
+
const balancesObservableAtom = atomWithObservable(() => balancesObservable);
|
|
243
|
+
const balancesPersistBackendAtom = atom(localStorageBalancesPersistBackend);
|
|
244
|
+
const hydrateBalancesObservableAtom = atom(async get => {
|
|
245
|
+
const persistBackend = get(balancesPersistBackendAtom);
|
|
246
|
+
const balances = await persistBackend.retrieve();
|
|
247
|
+
balancesObservable.next(Object.fromEntries(balances.map(b => [getBalanceId(b), {
|
|
248
|
+
...b,
|
|
249
|
+
status: "cache"
|
|
250
|
+
}])));
|
|
178
251
|
});
|
|
179
252
|
const balancesHydrateDataAtom = atom(async get => {
|
|
180
|
-
const [
|
|
181
|
-
|
|
182
|
-
|
|
253
|
+
const [{
|
|
254
|
+
networksById,
|
|
255
|
+
tokensById
|
|
256
|
+
}, tokenRates] = await Promise.all([get(chaindataAtom), get(tokenRatesAtom)]);
|
|
183
257
|
return {
|
|
184
258
|
networks: networksById,
|
|
185
259
|
tokens: tokensById,
|
|
186
260
|
tokenRates
|
|
187
261
|
};
|
|
188
262
|
});
|
|
189
|
-
const
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
263
|
+
const balancesSubscriptionAtomEffect = atomEffect(get => {
|
|
264
|
+
// lets us tear down the existing subscriptions when the atomEffect is restarted
|
|
265
|
+
const abort = new AbortController();
|
|
266
|
+
|
|
267
|
+
// we have to specify these synchronously, otherwise jotai won't know
|
|
268
|
+
// that it needs to restart our subscriptions when they change
|
|
269
|
+
const atomDependencies = Promise.all([get(cryptoWaitReadyAtom), get(balanceModulesAtom), get(allAddressesAtom), get(chainsAtom), get(chainsByIdAtom), get(evmNetworksAtom), get(evmNetworksByIdAtom), get(tokensAtom), get(tokensByIdAtom), get(miniMetadatasAtom), get(enabledChainsAtom), get(enabledTokensAtom), get(hydrateBalancesObservableAtom)]);
|
|
270
|
+
const persistBackend = get(balancesPersistBackendAtom);
|
|
271
|
+
const unsubsPromise = (async () => {
|
|
272
|
+
const [_cryptoReady, balanceModules, allAddresses, chains, chainsById, evmNetworks, evmNetworksById, tokens, tokensById, _miniMetadatas, enabledChainsConfig, enabledTokensConfig] = await atomDependencies;
|
|
273
|
+
if (abort.signal.aborted) return;
|
|
274
|
+
|
|
275
|
+
// persist data every thirty seconds
|
|
276
|
+
balancesObservable.pipe(debounceTime(10000)).subscribe(balancesUpdate => {
|
|
277
|
+
persistBackend.persist(Object.values(balancesUpdate));
|
|
278
|
+
});
|
|
279
|
+
const updateBalances = async balancesUpdates => {
|
|
280
|
+
if (abort.signal.aborted) return;
|
|
281
|
+
const updatesWithIds = new Balances(balancesUpdates);
|
|
282
|
+
const existing = balancesObservable.value;
|
|
283
|
+
|
|
284
|
+
// update initialising set here - before filtering out zero balances
|
|
285
|
+
// while this may include stale balances, the important thing is that the balance is no longer "initialising"
|
|
286
|
+
// balancesUpdates.forEach((b) => this.#initialising.delete(getBalanceId(b)))
|
|
287
|
+
|
|
288
|
+
const newlyZeroBalances = [];
|
|
289
|
+
const changedBalances = Object.fromEntries(updatesWithIds.each.filter(newB => {
|
|
290
|
+
const isZero = newB.total.tokens === "0";
|
|
291
|
+
// Keep new balances which are not zeros
|
|
292
|
+
const existingB = existing[newB.id];
|
|
293
|
+
if (!existingB && !isZero) return true;
|
|
294
|
+
const hasChanged = !isEqual$1(existingB, newB.toJSON());
|
|
295
|
+
// Collect balances now confirmed to be zero separately, so they can be filtered out from the main set
|
|
296
|
+
if (existingB && hasChanged && isZero) newlyZeroBalances.push(newB.id);
|
|
297
|
+
// Keep changed balances, which are not known zeros
|
|
298
|
+
return hasChanged && !isZero;
|
|
299
|
+
}).map(b => [b.id, b.toJSON()]));
|
|
300
|
+
if (Object.keys(changedBalances).length === 0 && newlyZeroBalances.length === 0) return;
|
|
301
|
+
const nonZeroBalances = newlyZeroBalances.length > 0 ? Object.fromEntries(Object.entries(existing).filter(([id]) => !newlyZeroBalances.includes(id))) : existing;
|
|
302
|
+
const newBalancesState = {
|
|
303
|
+
...nonZeroBalances,
|
|
304
|
+
...changedBalances
|
|
305
|
+
};
|
|
306
|
+
if (Object.keys(newBalancesState).length === 0) return;
|
|
307
|
+
balancesObservable.next(newBalancesState);
|
|
308
|
+
};
|
|
309
|
+
const deleteBalances = async balancesFilter => {
|
|
310
|
+
if (abort.signal.aborted) return;
|
|
311
|
+
const balancesToKeep = Object.fromEntries(new Balances(Object.values(await get(balancesObservableAtom))).each.filter(b => !balancesFilter(b)).map(b => [b.id, b.toJSON()]));
|
|
312
|
+
balancesObservable.next(balancesToKeep);
|
|
313
|
+
};
|
|
314
|
+
const enabledChainIds = enabledChainsConfig?.map(genesisHash => chains.find(chain => chain.genesisHash === genesisHash)?.id);
|
|
315
|
+
const enabledChainsFilter = enabledChainIds ? token => token.platform === "polkadot" && enabledChainIds?.includes(token.networkId) : () => true;
|
|
316
|
+
const enabledTokensFilter = enabledTokensConfig ? token => enabledTokensConfig.includes(token.id) : () => true;
|
|
317
|
+
const enabledTokenIds = tokens.filter(enabledChainsFilter).filter(enabledTokensFilter).map(({
|
|
318
|
+
id
|
|
319
|
+
}) => id);
|
|
320
|
+
if (enabledTokenIds.length < 1 || allAddresses.length < 1) return;
|
|
321
|
+
const addressesByTokenByModule = {};
|
|
322
|
+
enabledTokenIds.flatMap(tokenId => tokensById[tokenId]).forEach(token => {
|
|
323
|
+
// filter out tokens on chains/evmNetworks which have no rpcs
|
|
324
|
+
const hasRpcs = token.networkId && (chainsById[token.networkId]?.rpcs?.length ?? 0) > 0 || token.networkId && (evmNetworksById[token.networkId]?.rpcs?.length ?? 0) > 0;
|
|
325
|
+
if (!hasRpcs) return;
|
|
326
|
+
if (!addressesByTokenByModule[token.type]) addressesByTokenByModule[token.type] = {};
|
|
327
|
+
addressesByTokenByModule[token.type][token.id] = allAddresses.filter(address => {
|
|
328
|
+
// for each address, fetch balances only from compatible chains
|
|
329
|
+
return isEthereumAddress(address) ? token.networkId || chainsById[token.networkId ?? ""]?.account === "secp256k1" : token.networkId && chainsById[token.networkId ?? ""]?.account !== "secp256k1";
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
// Delete invalid cached balances
|
|
334
|
+
const chainIds = new Set(chains.map(chain => chain.id));
|
|
335
|
+
const evmNetworkIds = new Set(evmNetworks.map(evmNetwork => evmNetwork.id));
|
|
336
|
+
await deleteBalances(balance => {
|
|
337
|
+
// delete cached balances for accounts which don't exist anymore
|
|
338
|
+
if (!balance.address || !allAddresses.includes(balance.address)) return true;
|
|
339
|
+
|
|
340
|
+
// delete cached balances when chain/evmNetwork doesn't exist
|
|
341
|
+
if (!chainIds.has(balance.networkId) && !evmNetworkIds.has(balance.networkId)) return true;
|
|
342
|
+
|
|
343
|
+
// delete cached balance when token doesn't exist / is disabled
|
|
344
|
+
if (!enabledTokenIds.includes(balance.tokenId)) return true;
|
|
345
|
+
|
|
346
|
+
// delete cached balance when module doesn't exist
|
|
347
|
+
if (!balanceModules.find(module => module.type === balance.source)) return true;
|
|
348
|
+
|
|
349
|
+
// delete cached balance for accounts on incompatible chains
|
|
350
|
+
// (substrate accounts shouldn't have evm balances)
|
|
351
|
+
// (evm accounts shouldn't have substrate balances (unless the chain uses secp256k1 accounts))
|
|
352
|
+
const chain = chains.find(({
|
|
353
|
+
id
|
|
354
|
+
}) => id === balance.networkId) || null;
|
|
355
|
+
const hasChain = chainIds.has(balance.networkId);
|
|
356
|
+
const hasEvmNetwork = evmNetworkIds.has(balance.networkId);
|
|
357
|
+
const chainUsesSecp256k1Accounts = chain?.account === "secp256k1";
|
|
358
|
+
if (!isEthereumAddress(balance.address)) {
|
|
359
|
+
if (!hasChain) return true;
|
|
360
|
+
if (chainUsesSecp256k1Accounts) return true;
|
|
361
|
+
}
|
|
362
|
+
if (isEthereumAddress(balance.address)) {
|
|
363
|
+
if (!hasEvmNetwork && !chainUsesSecp256k1Accounts) return true;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// keep balance
|
|
367
|
+
return false;
|
|
368
|
+
});
|
|
369
|
+
if (abort.signal.aborted) return;
|
|
370
|
+
|
|
371
|
+
// after 30 seconds, change the status of all balances still initializing to stale
|
|
372
|
+
setTimeout(() => {
|
|
373
|
+
if (abort.signal.aborted) return;
|
|
374
|
+
const staleObservable = balancesObservable.pipe(map(val => Object.values(val).filter(({
|
|
375
|
+
status
|
|
376
|
+
}) => status === "cache").map(balance => ({
|
|
377
|
+
...balance,
|
|
378
|
+
status: "stale"
|
|
379
|
+
}))));
|
|
380
|
+
firstValueFrom(staleObservable).then(v => {
|
|
381
|
+
if (v.length) updateBalances(v);
|
|
382
|
+
});
|
|
383
|
+
}, 30_000);
|
|
384
|
+
return balanceModules.map(balanceModule => {
|
|
385
|
+
const unsub = balances(balanceModule, addressesByTokenByModule[balanceModule.type] ?? {}, (error, balances) => {
|
|
386
|
+
// log errors
|
|
387
|
+
if (error) {
|
|
388
|
+
if (error?.type === "STALE_RPC_ERROR" || error?.type === "WEBSOCKET_ALLOCATION_EXHAUSTED_ERROR") {
|
|
389
|
+
const addressesByModuleToken = addressesByTokenByModule[balanceModule.type] ?? {};
|
|
390
|
+
const staleObservable = balancesObservable.pipe(map(val => Object.values(val).filter(balance => {
|
|
391
|
+
const {
|
|
392
|
+
tokenId,
|
|
393
|
+
address,
|
|
394
|
+
source
|
|
395
|
+
} = balance;
|
|
396
|
+
const chainComparison = error.chainId ? "chainId" in balance && error.chainId === balance.chainId : error.evmNetworkId ? "evmNetworkId" in balance && error.evmNetworkId === balance.evmNetworkId : true;
|
|
397
|
+
return chainComparison && addressesByModuleToken[tokenId]?.includes(address) && source === balanceModule.type;
|
|
398
|
+
}).map(balance => ({
|
|
399
|
+
...balance,
|
|
400
|
+
status: "stale"
|
|
401
|
+
}))));
|
|
402
|
+
firstValueFrom(staleObservable).then(v => {
|
|
403
|
+
if (v.length) updateBalances(v);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
return log.error(`Failed to fetch ${balanceModule.type} balances`, error);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ignore empty balance responses
|
|
410
|
+
if (!balances) return;
|
|
411
|
+
// ignore balances from old subscriptions which are still in the process of unsubscribing
|
|
412
|
+
if (abort.signal.aborted) return;
|
|
413
|
+
|
|
414
|
+
// good balances
|
|
415
|
+
if (balances) {
|
|
416
|
+
if ("status" in balances) {
|
|
417
|
+
// For modules using the new SubscriptionResultWithStatus pattern
|
|
418
|
+
//TODO fix initialisin
|
|
419
|
+
// if (result.status === "initialising") this.#initialising.add(balanceModule.type)
|
|
420
|
+
// else this.#initialising.delete(balanceModule.type)
|
|
421
|
+
updateBalances(balances.data);
|
|
422
|
+
} else {
|
|
423
|
+
// add good ones to initialisedBalances
|
|
424
|
+
updateBalances(Object.values(balances.toJSON()));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
return () => unsub.then(unsubscribe => unsubscribe());
|
|
429
|
+
});
|
|
430
|
+
})();
|
|
431
|
+
|
|
432
|
+
// close the existing subscriptions when our effect unmounts
|
|
433
|
+
// (wait 2 seconds before actually unsubscribing, to allow the websocket to be reused in that time)
|
|
434
|
+
const unsubscribe = () => unsubsPromise.then(unsubs => {
|
|
435
|
+
persistBackend.persist(Object.values(balancesObservable.value));
|
|
436
|
+
unsubs?.forEach(unsub => unsub());
|
|
437
|
+
});
|
|
438
|
+
abort.signal.addEventListener("abort", () => setTimeout(unsubscribe, 2_000));
|
|
439
|
+
return () => abort.abort("Unsubscribed");
|
|
195
440
|
});
|
|
196
441
|
|
|
197
442
|
const useSetBalancesAddresses = addresses => {
|
|
@@ -208,8 +453,10 @@ const useSetBalancesAddresses = addresses => {
|
|
|
208
453
|
* @returns a Balances object containing the current balances state.
|
|
209
454
|
*/
|
|
210
455
|
|
|
211
|
-
const useBalances =
|
|
212
|
-
|
|
456
|
+
const useBalances = persistBackend => {
|
|
457
|
+
const [, setPersistBackend] = useAtom(balancesPersistBackendAtom);
|
|
458
|
+
if (persistBackend) setPersistBackend(persistBackend);
|
|
459
|
+
return useAtomValue(allBalancesAtom);
|
|
213
460
|
};
|
|
214
461
|
|
|
215
462
|
// TODO: Extract to shared definition between extension and @talismn/balances-react
|
|
@@ -246,41 +493,30 @@ const useChainConnectors = () => useAtomValue(chainConnectorsAtom);
|
|
|
246
493
|
|
|
247
494
|
const useChaindataProvider = () => useAtomValue(chaindataProviderAtom);
|
|
248
495
|
const useChaindata = () => useAtomValue(chaindataAtom);
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
-
const networksById = useNetworksById();
|
|
258
|
-
return networksById[networkId ?? ""] ?? null;
|
|
259
|
-
};
|
|
260
|
-
const useTokens = () => useChaindata().tokens;
|
|
261
|
-
const useTokensById = () => {
|
|
262
|
-
const {
|
|
263
|
-
tokens
|
|
264
|
-
} = useChaindata();
|
|
265
|
-
return useMemo(() => keyBy(tokens, t => t.id), [tokens]);
|
|
266
|
-
};
|
|
267
|
-
const useToken = tokenId => {
|
|
268
|
-
const tokensById = useTokensById();
|
|
269
|
-
return tokensById[tokenId ?? ""] ?? null;
|
|
270
|
-
};
|
|
496
|
+
const useChains = () => useAtomValue(chainsByIdAtom);
|
|
497
|
+
const useChainsByGenesisHash = () => useAtomValue(chainsByGenesisHashAtom);
|
|
498
|
+
const useEvmNetworks = () => useAtomValue(evmNetworksByIdAtom);
|
|
499
|
+
const useTokens = () => useAtomValue(tokensByIdAtom);
|
|
500
|
+
const useMiniMetadatas = () => useAtomValue(miniMetadatasAtom);
|
|
501
|
+
const useChain = chainId => useChains()[chainId ?? ""] ?? undefined;
|
|
502
|
+
const useEvmNetwork = evmNetworkId => useEvmNetworks()[evmNetworkId ?? ""] ?? undefined;
|
|
503
|
+
const useToken = tokenId => useTokens()[tokenId ?? ""] ?? undefined;
|
|
271
504
|
|
|
272
505
|
const useTokenRates = () => useAtomValue(tokenRatesAtom);
|
|
273
506
|
const useTokenRate = tokenId => useTokenRates()[tokenId ?? ""] ?? undefined;
|
|
274
507
|
|
|
275
|
-
const cryptoWaitReadyAtom = atom(async () => await cryptoWaitReady());
|
|
276
|
-
|
|
277
508
|
const BalancesProvider = ({
|
|
509
|
+
balanceModules,
|
|
278
510
|
coinsApiUrl,
|
|
279
511
|
withTestnets,
|
|
280
512
|
enabledChains,
|
|
281
513
|
enabledTokens,
|
|
282
514
|
children
|
|
283
515
|
}) => {
|
|
516
|
+
const setBalanceModules = useSetAtom(balanceModuleCreatorsAtom);
|
|
517
|
+
useEffect(() => {
|
|
518
|
+
if (balanceModules !== undefined) setBalanceModules(balanceModules);
|
|
519
|
+
}, [balanceModules, setBalanceModules]);
|
|
284
520
|
const setCoinsApiConfig = useSetAtom(coinsApiConfigAtom);
|
|
285
521
|
useEffect(() => {
|
|
286
522
|
setCoinsApiConfig({
|
|
@@ -304,4 +540,4 @@ const BalancesProvider = ({
|
|
|
304
540
|
});
|
|
305
541
|
};
|
|
306
542
|
|
|
307
|
-
export { BalancesProvider, allAddressesAtom,
|
|
543
|
+
export { BalancesProvider, allAddressesAtom, allBalancesAtom, balanceModuleCreatorsAtom, balanceModulesAtom, balancesPersistBackendAtom, chainConnectorsAtom, chaindataAtom, chaindataProviderAtom, chainsAtom, chainsByGenesisHashAtom, chainsByIdAtom, coinsApiConfigAtom, cryptoWaitReadyAtom, enableTestnetsAtom, enabledChainsAtom, enabledTokensAtom, evmNetworksAtom, evmNetworksByIdAtom, getStaleChains, miniMetadatasAtom, tokenRatesAtom, tokensAtom, tokensByIdAtom, useBalances, useBalancesStatus, useChain, useChainConnectors, useChaindata, useChaindataProvider, useChains, useChainsByGenesisHash, useEvmNetwork, useEvmNetworks, useMiniMetadatas, useSetBalancesAddresses, useToken, useTokenRate, useTokenRates, useTokens };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@talismn/balances-react",
|
|
3
|
-
"version": "0.0.0-
|
|
3
|
+
"version": "0.0.0-pr2076-20250703120513",
|
|
4
4
|
"author": "Talisman",
|
|
5
5
|
"homepage": "https://talisman.xyz",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
|
@@ -27,29 +27,29 @@
|
|
|
27
27
|
"dexie-react-hooks": "^1.1.7",
|
|
28
28
|
"jotai": "~2",
|
|
29
29
|
"jotai-effect": "~1",
|
|
30
|
-
"lodash
|
|
30
|
+
"lodash": "4.17.21",
|
|
31
31
|
"react-use": "^17.5.1",
|
|
32
32
|
"rxjs": "^7.8.1",
|
|
33
|
-
"@talismn/balances": "0.0.0-
|
|
34
|
-
"@talismn/chain-connector": "0.0.0-
|
|
35
|
-
"@talismn/chain-connector
|
|
36
|
-
"@talismn/
|
|
37
|
-
"@talismn/connection-meta": "0.0.0-
|
|
38
|
-
"@talismn/
|
|
39
|
-
"@talismn/
|
|
40
|
-
"@talismn/
|
|
33
|
+
"@talismn/balances": "0.0.0-pr2076-20250703120513",
|
|
34
|
+
"@talismn/chain-connector-evm": "0.0.0-pr2076-20250703120513",
|
|
35
|
+
"@talismn/chain-connector": "0.0.0-pr2076-20250703120513",
|
|
36
|
+
"@talismn/token-rates": "0.0.0-pr2076-20250703120513",
|
|
37
|
+
"@talismn/connection-meta": "0.0.0-pr2076-20250703120513",
|
|
38
|
+
"@talismn/scale": "0.1.2",
|
|
39
|
+
"@talismn/util": "0.0.0-pr2076-20250703120513",
|
|
40
|
+
"@talismn/chaindata-provider": "0.0.0-pr2076-20250703120513"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/jest": "^29.5.14",
|
|
44
|
-
"@types/lodash
|
|
44
|
+
"@types/lodash": "^4.17.12",
|
|
45
45
|
"@types/react": "^18.3.12",
|
|
46
46
|
"eslint": "^8.57.1",
|
|
47
47
|
"jest": "^29.7.0",
|
|
48
48
|
"react": "^18.3.1",
|
|
49
49
|
"ts-jest": "^29.2.5",
|
|
50
50
|
"typescript": "^5.6.3",
|
|
51
|
-
"@talismn/
|
|
52
|
-
"@talismn/
|
|
51
|
+
"@talismn/eslint-config": "0.0.3",
|
|
52
|
+
"@talismn/tsconfig": "0.0.2"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
55
|
"@polkadot/util-crypto": "*",
|