@talismn/balances-react 0.0.0-pr2080-20250710073919 → 0.0.0-pr2091-20250715125148
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/balances.d.ts +1 -5
- package/dist/declarations/src/atoms/balancesProvider.d.ts +2 -0
- package/dist/declarations/src/atoms/chaindata.d.ts +427 -1450
- package/dist/declarations/src/atoms/config.d.ts +0 -4
- package/dist/declarations/src/hooks/useBalances.d.ts +1 -2
- package/dist/declarations/src/hooks/useChaindata.d.ts +633 -709
- package/dist/declarations/src/index.d.ts +2 -8
- package/dist/talismn-balances-react.cjs.dev.js +107 -352
- package/dist/talismn-balances-react.cjs.prod.js +107 -352
- package/dist/talismn-balances-react.esm.js +109 -342
- package/package.json +14 -13
- package/dist/declarations/src/atoms/balanceModules.d.ts +0 -2
- package/dist/declarations/src/util/balancesPersist.d.ts +0 -10
|
@@ -1,24 +1,23 @@
|
|
|
1
|
-
import { atom, useSetAtom,
|
|
1
|
+
import { atom, useSetAtom, useAtomValue } from 'jotai';
|
|
2
2
|
import { useMemo, useEffect } from 'react';
|
|
3
|
-
import {
|
|
4
|
-
import { DEFAULT_COINSAPI_CONFIG, db as db$1, fetchTokenRates, ALL_CURRENCY_IDS } from '@talismn/token-rates';
|
|
3
|
+
import { DEFAULT_COINSAPI_CONFIG, db, fetchTokenRates, ALL_CURRENCY_IDS } from '@talismn/token-rates';
|
|
5
4
|
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
6
5
|
import { ChaindataProvider } from '@talismn/chaindata-provider';
|
|
7
6
|
export { evmErc20TokenId, evmNativeTokenId, subAssetTokenId, subNativeTokenId, subPsp22TokenId, subTokensTokenId } from '@talismn/chaindata-provider';
|
|
8
|
-
import {
|
|
7
|
+
import { BalancesProvider as BalancesProvider$1, Balances } from '@talismn/balances';
|
|
9
8
|
import { atomEffect } from 'jotai-effect';
|
|
10
|
-
import {
|
|
11
|
-
import { isEqual as isEqual$1 } from 'lodash';
|
|
12
|
-
import { Observable, distinctUntilChanged, map, combineLatest, BehaviorSubject, debounceTime, firstValueFrom } from 'rxjs';
|
|
13
|
-
import anylogger from 'anylogger';
|
|
9
|
+
import { keyBy, fromPairs } from 'lodash-es';
|
|
14
10
|
import { ChainConnector } from '@talismn/chain-connector';
|
|
15
11
|
import { ChainConnectorEvm } from '@talismn/chain-connector-evm';
|
|
12
|
+
import { ChainConnectorSol } from '@talismn/chain-connector-sol';
|
|
16
13
|
import { connectionMetaDb } from '@talismn/connection-meta';
|
|
14
|
+
import { firstThenDebounce, isTruthy, isAbortError } from '@talismn/util';
|
|
15
|
+
import { atomWithObservable } from 'jotai/utils';
|
|
16
|
+
import { combineLatest, Observable, map } from 'rxjs';
|
|
17
17
|
import { liveQuery } from 'dexie';
|
|
18
|
-
import
|
|
18
|
+
import anylogger from 'anylogger';
|
|
19
19
|
import { cryptoWaitReady } from '@polkadot/util-crypto';
|
|
20
20
|
|
|
21
|
-
const balanceModuleCreatorsAtom = atom(defaultBalanceModules);
|
|
22
21
|
const innerCoinsApiConfigAtom = atom(DEFAULT_COINSAPI_CONFIG);
|
|
23
22
|
const coinsApiConfigAtom = atom(get => get(innerCoinsApiConfigAtom), (_get, set, options) => set(innerCoinsApiConfigAtom, {
|
|
24
23
|
apiUrl: options.apiUrl ?? DEFAULT_COINSAPI_CONFIG.apiUrl
|
|
@@ -30,55 +29,6 @@ const enabledTokensAtom = atom(undefined);
|
|
|
30
29
|
/** Sets the list of addresses for which token balances will be fetched by the balances subscription */
|
|
31
30
|
const allAddressesAtom = atom([]);
|
|
32
31
|
|
|
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
|
-
|
|
82
32
|
const chaindataProviderAtom = atom(() => {
|
|
83
33
|
return new ChaindataProvider({});
|
|
84
34
|
});
|
|
@@ -87,25 +37,52 @@ const chainConnectorsAtom = atom(get => {
|
|
|
87
37
|
const chaindataProvider = get(chaindataProviderAtom);
|
|
88
38
|
const substrate = new ChainConnector(chaindataProvider, connectionMetaDb);
|
|
89
39
|
const evm = new ChainConnectorEvm(chaindataProvider);
|
|
40
|
+
const solana = new ChainConnectorSol(chaindataProvider);
|
|
90
41
|
return {
|
|
91
42
|
substrate,
|
|
92
|
-
evm
|
|
43
|
+
evm,
|
|
44
|
+
solana
|
|
93
45
|
};
|
|
94
46
|
});
|
|
95
47
|
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
}));
|
|
48
|
+
const balancesProviderAtom = atom(get => {
|
|
49
|
+
return new BalancesProvider$1(get(chaindataProviderAtom), get(chainConnectorsAtom)) // TODO pass storage
|
|
50
|
+
;
|
|
107
51
|
});
|
|
108
52
|
|
|
53
|
+
const chaindataAtom = atomWithObservable(get => {
|
|
54
|
+
return combineLatest({
|
|
55
|
+
networks: get(chaindataProviderAtom).networks$,
|
|
56
|
+
tokens: get(chaindataProviderAtom).tokens$
|
|
57
|
+
}).pipe(firstThenDebounce(1_000));
|
|
58
|
+
});
|
|
59
|
+
const filteredChaindataAtom = atom(async get => {
|
|
60
|
+
const enabledNetworkIds = get(enabledChainsAtom);
|
|
61
|
+
const enabledTokenIds = get(enabledTokensAtom);
|
|
62
|
+
const enableTestnets = get(enableTestnetsAtom);
|
|
63
|
+
const chaindata = await get(chaindataAtom);
|
|
64
|
+
const networks = chaindata.networks.filter(n => (enabledNetworkIds?.includes(n.id) || n.isDefault) && (enableTestnets || !n.isTestnet));
|
|
65
|
+
const networkById = keyBy(networks, n => n.id);
|
|
66
|
+
const tokens = chaindata.tokens.filter(token => (enabledTokenIds?.includes(token.id) || token.isDefault) && networkById[token.networkId]);
|
|
67
|
+
return {
|
|
68
|
+
networks,
|
|
69
|
+
tokens
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
const tokensAtom = atom(async get => {
|
|
73
|
+
const chaindata = await get(filteredChaindataAtom);
|
|
74
|
+
return chaindata.tokens;
|
|
75
|
+
});
|
|
76
|
+
const networksAtom = atom(async get => {
|
|
77
|
+
const chaindata = await get(filteredChaindataAtom);
|
|
78
|
+
return chaindata.networks;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
var packageJson = {
|
|
82
|
+
name: "@talismn/balances-react"};
|
|
83
|
+
|
|
84
|
+
var log = anylogger(packageJson.name);
|
|
85
|
+
|
|
109
86
|
/**
|
|
110
87
|
* Converts a dexie Observable into an rxjs Observable.
|
|
111
88
|
*/
|
|
@@ -119,54 +96,6 @@ function dexieToRxjs(o) {
|
|
|
119
96
|
});
|
|
120
97
|
}
|
|
121
98
|
|
|
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
|
-
|
|
170
99
|
const tokenRatesAtom = atom(async get => {
|
|
171
100
|
// runs a timer to keep tokenRates up to date
|
|
172
101
|
get(tokenRatesFetcherAtomEffect);
|
|
@@ -179,7 +108,7 @@ const tokenRatesDbAtom = atomWithObservable(() => {
|
|
|
179
108
|
}) => [tokenId, rates]));
|
|
180
109
|
|
|
181
110
|
// retrieve fetched tokenRates from the db
|
|
182
|
-
return dexieToRxjs(liveQuery(() => db
|
|
111
|
+
return dexieToRxjs(liveQuery(() => db.tokenRates.toArray())).pipe(map(dbRatesToMap));
|
|
183
112
|
});
|
|
184
113
|
const tokenRatesFetcherAtomEffect = atomEffect(get => {
|
|
185
114
|
// lets us tear down the existing timer when the effect is restarted
|
|
@@ -187,9 +116,9 @@ const tokenRatesFetcherAtomEffect = atomEffect(get => {
|
|
|
187
116
|
|
|
188
117
|
// we have to get these synchronously so that jotai knows to restart our timer when they change
|
|
189
118
|
const coinsApiConfig = get(coinsApiConfigAtom);
|
|
190
|
-
const
|
|
119
|
+
const tokensPromise = get(tokensAtom);
|
|
191
120
|
(async () => {
|
|
192
|
-
const tokensById = await
|
|
121
|
+
const tokensById = keyBy(await tokensPromise, "id");
|
|
193
122
|
const tokenIds = Object.keys(tokensById);
|
|
194
123
|
const loopMs = 300_000; // 300_000ms = 300s = 5 minutes
|
|
195
124
|
const retryTimeout = 5_000; // 5_000ms = 5 seconds
|
|
@@ -203,15 +132,15 @@ const tokenRatesFetcherAtomEffect = atomEffect(get => {
|
|
|
203
132
|
rates
|
|
204
133
|
}));
|
|
205
134
|
if (abort.signal.aborted) return; // don't insert into db if aborted
|
|
206
|
-
await db
|
|
135
|
+
await db.transaction("rw", db.tokenRates, async () => {
|
|
207
136
|
// override all tokenRates
|
|
208
|
-
await db
|
|
137
|
+
await db.tokenRates.bulkPut(putTokenRates);
|
|
209
138
|
|
|
210
139
|
// delete tokenRates for tokens which no longer exist
|
|
211
140
|
const validTokenIds = new Set(tokenIds);
|
|
212
|
-
const tokenRatesIds = await db
|
|
141
|
+
const tokenRatesIds = await db.tokenRates.toCollection().primaryKeys();
|
|
213
142
|
const deleteIds = tokenRatesIds.filter(id => !validTokenIds.has(id));
|
|
214
|
-
if (deleteIds.length > 0) await db
|
|
143
|
+
if (deleteIds.length > 0) await db.tokenRates.bulkDelete(deleteIds);
|
|
215
144
|
});
|
|
216
145
|
if (abort.signal.aborted) return; // don't schedule next loop if aborted
|
|
217
146
|
setTimeout(hydrate, loopMs);
|
|
@@ -230,213 +159,42 @@ const tokenRatesFetcherAtomEffect = atomEffect(get => {
|
|
|
230
159
|
return () => abort.abort("Unsubscribed");
|
|
231
160
|
});
|
|
232
161
|
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
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
|
+
const addressesByTokenIdAtom = atom(async get => {
|
|
163
|
+
const [tokens, addresses] = await Promise.all([get(tokensAtom), get(allAddressesAtom)]);
|
|
164
|
+
return fromPairs(tokens.map(token => [token.id, addresses]));
|
|
240
165
|
});
|
|
241
|
-
const
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
166
|
+
const rawBalancesAtom = atom([]);
|
|
167
|
+
const subscribeBalancesAtom = atomEffect((get, set) => {
|
|
168
|
+
const unsub = (async () => {
|
|
169
|
+
const balancesProvider = get(balancesProviderAtom);
|
|
170
|
+
const addressesByTokenId = await get(addressesByTokenIdAtom);
|
|
171
|
+
const sub = balancesProvider.getBalances$(addressesByTokenId).subscribe(balances => {
|
|
172
|
+
set(rawBalancesAtom, balances.balances);
|
|
173
|
+
});
|
|
174
|
+
return () => {
|
|
175
|
+
return sub.unsubscribe();
|
|
176
|
+
};
|
|
177
|
+
})();
|
|
178
|
+
return () => {
|
|
179
|
+
unsub.then(unsubscribe => unsubscribe());
|
|
180
|
+
};
|
|
251
181
|
});
|
|
252
182
|
const balancesHydrateDataAtom = atom(async get => {
|
|
253
|
-
const [
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}, tokenRates] = await Promise.all([get(chaindataAtom), get(tokenRatesAtom)]);
|
|
183
|
+
const [chaindata, tokenRates] = await Promise.all([get(chaindataAtom), get(tokenRatesAtom)]);
|
|
184
|
+
const networksById = keyBy(chaindata.networks, "id");
|
|
185
|
+
const tokensById = keyBy(chaindata.tokens, "id");
|
|
257
186
|
return {
|
|
258
187
|
networks: networksById,
|
|
259
188
|
tokens: tokensById,
|
|
260
189
|
tokenRates
|
|
261
190
|
};
|
|
262
191
|
});
|
|
263
|
-
const
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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");
|
|
192
|
+
const balancesAtom = atom(async get => {
|
|
193
|
+
// subscribe to balancesProvider getBalance with addressesByTokenIdAtom as param
|
|
194
|
+
get(subscribeBalancesAtom);
|
|
195
|
+
const hydrate = await get(balancesHydrateDataAtom);
|
|
196
|
+
const rawBalances = get(rawBalancesAtom);
|
|
197
|
+
return new Balances(rawBalances, hydrate);
|
|
440
198
|
});
|
|
441
199
|
|
|
442
200
|
const useSetBalancesAddresses = addresses => {
|
|
@@ -453,10 +211,8 @@ const useSetBalancesAddresses = addresses => {
|
|
|
453
211
|
* @returns a Balances object containing the current balances state.
|
|
454
212
|
*/
|
|
455
213
|
|
|
456
|
-
const useBalances =
|
|
457
|
-
|
|
458
|
-
if (persistBackend) setPersistBackend(persistBackend);
|
|
459
|
-
return useAtomValue(allBalancesAtom);
|
|
214
|
+
const useBalances = () => {
|
|
215
|
+
return useAtomValue(balancesAtom);
|
|
460
216
|
};
|
|
461
217
|
|
|
462
218
|
// TODO: Extract to shared definition between extension and @talismn/balances-react
|
|
@@ -493,30 +249,41 @@ const useChainConnectors = () => useAtomValue(chainConnectorsAtom);
|
|
|
493
249
|
|
|
494
250
|
const useChaindataProvider = () => useAtomValue(chaindataProviderAtom);
|
|
495
251
|
const useChaindata = () => useAtomValue(chaindataAtom);
|
|
496
|
-
const
|
|
497
|
-
const
|
|
498
|
-
const
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const
|
|
252
|
+
const useNetworks = () => useChaindata().networks;
|
|
253
|
+
const useNetworksById = () => {
|
|
254
|
+
const {
|
|
255
|
+
networks
|
|
256
|
+
} = useChaindata();
|
|
257
|
+
return useMemo(() => keyBy(networks, n => n.id), [networks]);
|
|
258
|
+
};
|
|
259
|
+
const useNetwork = networkId => {
|
|
260
|
+
const networksById = useNetworksById();
|
|
261
|
+
return networksById[networkId ?? ""] ?? null;
|
|
262
|
+
};
|
|
263
|
+
const useTokens = () => useChaindata().tokens;
|
|
264
|
+
const useTokensById = () => {
|
|
265
|
+
const {
|
|
266
|
+
tokens
|
|
267
|
+
} = useChaindata();
|
|
268
|
+
return useMemo(() => keyBy(tokens, t => t.id), [tokens]);
|
|
269
|
+
};
|
|
270
|
+
const useToken = tokenId => {
|
|
271
|
+
const tokensById = useTokensById();
|
|
272
|
+
return tokensById[tokenId ?? ""] ?? null;
|
|
273
|
+
};
|
|
504
274
|
|
|
505
275
|
const useTokenRates = () => useAtomValue(tokenRatesAtom);
|
|
506
276
|
const useTokenRate = tokenId => useTokenRates()[tokenId ?? ""] ?? undefined;
|
|
507
277
|
|
|
278
|
+
const cryptoWaitReadyAtom = atom(async () => await cryptoWaitReady());
|
|
279
|
+
|
|
508
280
|
const BalancesProvider = ({
|
|
509
|
-
balanceModules,
|
|
510
281
|
coinsApiUrl,
|
|
511
282
|
withTestnets,
|
|
512
283
|
enabledChains,
|
|
513
284
|
enabledTokens,
|
|
514
285
|
children
|
|
515
286
|
}) => {
|
|
516
|
-
const setBalanceModules = useSetAtom(balanceModuleCreatorsAtom);
|
|
517
|
-
useEffect(() => {
|
|
518
|
-
if (balanceModules !== undefined) setBalanceModules(balanceModules);
|
|
519
|
-
}, [balanceModules, setBalanceModules]);
|
|
520
287
|
const setCoinsApiConfig = useSetAtom(coinsApiConfigAtom);
|
|
521
288
|
useEffect(() => {
|
|
522
289
|
setCoinsApiConfig({
|
|
@@ -540,4 +307,4 @@ const BalancesProvider = ({
|
|
|
540
307
|
});
|
|
541
308
|
};
|
|
542
309
|
|
|
543
|
-
export { BalancesProvider, allAddressesAtom,
|
|
310
|
+
export { BalancesProvider, allAddressesAtom, balancesAtom, chainConnectorsAtom, chaindataAtom, chaindataProviderAtom, coinsApiConfigAtom, cryptoWaitReadyAtom, enableTestnetsAtom, enabledChainsAtom, enabledTokensAtom, getStaleChains, networksAtom, tokenRatesAtom, tokensAtom, useBalances, useBalancesStatus, useChainConnectors, useChaindata, useChaindataProvider, useNetwork, useNetworks, useNetworksById, useSetBalancesAddresses, useToken, useTokenRate, useTokenRates, useTokens, useTokensById };
|
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-pr2091-20250715125148",
|
|
4
4
|
"author": "Talisman",
|
|
5
5
|
"homepage": "https://talisman.xyz",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
|
@@ -27,29 +27,30 @@
|
|
|
27
27
|
"dexie-react-hooks": "^1.1.7",
|
|
28
28
|
"jotai": "~2",
|
|
29
29
|
"jotai-effect": "~1",
|
|
30
|
-
"lodash": "4.17.21",
|
|
30
|
+
"lodash-es": "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/
|
|
36
|
-
"@talismn/
|
|
37
|
-
"@talismn/
|
|
38
|
-
"@talismn/connection-meta": "0.0.0-
|
|
39
|
-
"@talismn/
|
|
40
|
-
"@talismn/token-rates": "0.0.0-
|
|
33
|
+
"@talismn/balances": "0.0.0-pr2091-20250715125148",
|
|
34
|
+
"@talismn/chain-connector-evm": "0.0.0-pr2091-20250715125148",
|
|
35
|
+
"@talismn/chaindata-provider": "0.0.0-pr2091-20250715125148",
|
|
36
|
+
"@talismn/chain-connector-sol": "0.0.0-pr2091-20250715125148",
|
|
37
|
+
"@talismn/chain-connector": "0.0.0-pr2091-20250715125148",
|
|
38
|
+
"@talismn/connection-meta": "0.0.0-pr2091-20250715125148",
|
|
39
|
+
"@talismn/scale": "0.0.0-pr2091-20250715125148",
|
|
40
|
+
"@talismn/token-rates": "0.0.0-pr2091-20250715125148",
|
|
41
|
+
"@talismn/util": "0.0.0-pr2091-20250715125148"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@types/jest": "^29.5.14",
|
|
44
|
-
"@types/lodash": "^4.17.12",
|
|
45
|
+
"@types/lodash-es": "^4.17.12",
|
|
45
46
|
"@types/react": "^18.3.12",
|
|
46
47
|
"eslint": "^8.57.1",
|
|
47
48
|
"jest": "^29.7.0",
|
|
48
49
|
"react": "^18.3.1",
|
|
49
50
|
"ts-jest": "^29.2.5",
|
|
50
51
|
"typescript": "^5.6.3",
|
|
51
|
-
"@talismn/
|
|
52
|
-
"@talismn/
|
|
52
|
+
"@talismn/eslint-config": "0.0.3",
|
|
53
|
+
"@talismn/tsconfig": "0.0.2"
|
|
53
54
|
},
|
|
54
55
|
"peerDependencies": {
|
|
55
56
|
"@polkadot/util-crypto": "*",
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { BalanceJson } from "@talismn/balances";
|
|
2
|
-
type PersistFn = (balances: BalanceJson[]) => Promise<void>;
|
|
3
|
-
type RetrieveFn = () => Promise<BalanceJson[]>;
|
|
4
|
-
export type BalancesPersistBackend = {
|
|
5
|
-
persist: PersistFn;
|
|
6
|
-
retrieve: RetrieveFn;
|
|
7
|
-
};
|
|
8
|
-
export declare const indexedDbBalancesPersistBackend: BalancesPersistBackend;
|
|
9
|
-
export declare const localStorageBalancesPersistBackend: BalancesPersistBackend;
|
|
10
|
-
export {};
|