@talismn/balances 0.0.0-pr2075-20250709135256 → 0.0.0-pr2075-20250710032705

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/dist/declarations/src/BalancesProvider.d.ts +3 -1
  2. package/dist/declarations/src/{getMiniMetadata/getMiniMetadatas.d.ts → getMiniMetadatas/index.d.ts} +1 -0
  3. package/dist/declarations/src/index.d.ts +0 -1
  4. package/dist/declarations/src/modules/evm-erc20/types.d.ts +1 -1
  5. package/dist/declarations/src/modules/evm-native/types.d.ts +1 -1
  6. package/dist/declarations/src/modules/evm-uniswapv2/types.d.ts +1 -1
  7. package/dist/declarations/src/modules/index.d.ts +9 -9
  8. package/dist/declarations/src/modules/substrate-assets/types.d.ts +1 -1
  9. package/dist/declarations/src/modules/substrate-foreignassets/types.d.ts +1 -1
  10. package/dist/declarations/src/modules/substrate-hydration/types.d.ts +1 -1
  11. package/dist/declarations/src/modules/substrate-native/types.d.ts +1 -1
  12. package/dist/declarations/src/modules/substrate-psp22/types.d.ts +1 -1
  13. package/dist/declarations/src/modules/substrate-tokens/types.d.ts +1 -1
  14. package/dist/declarations/src/types/tokens.d.ts +1 -1
  15. package/dist/talismn-balances.cjs.dev.js +1602 -1657
  16. package/dist/talismn-balances.cjs.prod.js +1602 -1657
  17. package/dist/talismn-balances.esm.js +1603 -1656
  18. package/package.json +8 -8
  19. package/dist/declarations/src/TalismanBalancesDatabase.d.ts +0 -11
  20. package/dist/declarations/src/upgrades/2024-01-25-upgradeRemoveSymbolFromNativeTokenId.d.ts +0 -2
  21. package/dist/declarations/src/upgrades/2024-03-19-upgradeBalancesDataBlob.d.ts +0 -2
  22. package/dist/declarations/src/upgrades/index.d.ts +0 -2
  23. /package/dist/declarations/src/{getMiniMetadata → getMiniMetadatas}/getMetadataRpc.d.ts +0 -0
  24. /package/dist/declarations/src/{getMiniMetadata → getMiniMetadatas}/getSpecVersion.d.ts +0 -0
@@ -1,16 +1,14 @@
1
- import { Dexie } from 'dexie';
2
- import anylogger from 'anylogger';
3
- import { evmErc20TokenId, MINIMETADATA_VERSION, EvmErc20TokenSchema, parseTokenId, parseEvmErc20TokenId, isTokenOfType, TokenBaseSchema, EvmNativeTokenSchema, evmNativeTokenId, EvmUniswapV2TokenSchema, evmUniswapV2TokenId, SubAssetsTokenSchema, subAssetTokenId, SubForeignAssetsTokenSchema, subForeignAssetTokenId, SubHydrationTokenSchema, subHydrationTokenId, SubNativeTokenSchema, subNativeTokenId, SubPsp22TokenSchema, subPsp22TokenId, SubTokensTokenSchema, subTokensTokenId, isNetworkDot } from '@talismn/chaindata-provider';
1
+ import { EvmErc20TokenSchema, parseTokenId, parseEvmErc20TokenId, evmErc20TokenId, isTokenOfType, TokenBaseSchema, EvmNativeTokenSchema, evmNativeTokenId, EvmUniswapV2TokenSchema, evmUniswapV2TokenId, SubAssetsTokenSchema, subAssetTokenId, MINIMETADATA_VERSION, SubForeignAssetsTokenSchema, subForeignAssetTokenId, SubHydrationTokenSchema, subHydrationTokenId, SubNativeTokenSchema, subNativeTokenId, SubPsp22TokenSchema, subPsp22TokenId, SubTokensTokenSchema, subTokensTokenId, isNetworkDot } from '@talismn/chaindata-provider';
4
2
  export { MINIMETADATA_VERSION } from '@talismn/chaindata-provider';
5
- import { newTokenRates } from '@talismn/token-rates';
6
- import { isBigInt, BigMath, planckToTokens, isArrayOf, isTruthy, isEthereumAddress, isNotNil, isAbortError, getSharedObservable } from '@talismn/util';
7
- import BigNumber from 'bignumber.js';
8
- import { toHex, Twox64Concat, parseMetadataRpc, unifyMetadata, decAnyMetadata, getDynamicBuilder, getLookupFn, decodeScale, getStorageKeyPrefix, compactMetadata, encodeMetadata, papiParse, papiStringify } from '@talismn/scale';
9
- import pako from 'pako';
3
+ import { isEthereumAddress, isNotNil, BigMath, isArrayOf, isBigInt, planckToTokens, isAbortError, getSharedObservable } from '@talismn/util';
10
4
  import { parseAbi, erc20Abi, getContract, ContractFunctionExecutionError, hexToString, erc20Abi_bytes32, encodeFunctionData, withRetry } from 'viem';
11
5
  import { assign, omit, isEqual, keyBy, keys, uniq, fromPairs, values, toPairs } from 'lodash';
12
6
  import z from 'zod/v4';
7
+ import anylogger from 'anylogger';
13
8
  import { of, Observable, distinctUntilChanged, map, timer, switchMap, from, firstValueFrom, combineLatest, BehaviorSubject, startWith, filter, tap } from 'rxjs';
9
+ import { parseMetadataRpc, toHex, unifyMetadata, decAnyMetadata, getDynamicBuilder, getLookupFn, decodeScale, getStorageKeyPrefix, Twox64Concat, compactMetadata, encodeMetadata, papiParse, papiStringify } from '@talismn/scale';
10
+ import { newTokenRates } from '@talismn/token-rates';
11
+ import BigNumber from 'bignumber.js';
14
12
  import { mergeUint8 } from '@polkadot-api/utils';
15
13
  import { Binary, Enum, AccountId } from 'polkadot-api';
16
14
  import upperFirst from 'lodash/upperFirst';
@@ -21,876 +19,12 @@ import { u8aToHex, u8aConcatStrict, u8aToString, hexToNumber } from '@polkadot/u
21
19
  import PQueue from 'p-queue';
22
20
  import { fetchBestMetadata } from '@talismn/sapi';
23
21
 
24
- var packageJson = {
25
- name: "@talismn/balances"};
26
-
27
- var log = anylogger(packageJson.name);
28
-
29
- function excludeFromTransferableAmount(locks) {
30
- if (typeof locks === "string") return BigInt(locks);
31
- if (!Array.isArray(locks)) locks = [locks];
32
- return locks.filter(lock => lock.includeInTransferable !== true).map(lock => lock.amount.planck).reduce((max, lock) => BigMath.max(max, lock), 0n);
33
- }
34
- function excludeFromFeePayableLocks(locks) {
35
- if (typeof locks === "string") return [];
36
- if (!Array.isArray(locks)) locks = [locks];
37
- return locks.filter(lock => lock.excludeFromFeePayable);
38
- }
39
- function includeInTotalExtraAmount(extra) {
40
- if (!extra) return 0n;
41
- if (!Array.isArray(extra)) extra = [extra];
42
- return extra.filter(extra => extra.includeInTotal).map(extra => extra.amount.planck).reduce((a, b) => a + b, 0n);
43
- }
44
-
45
- /**
46
- * Have the importing library define its Token and BalanceJson enums (as a sum type of all plugins) and pass them into some
47
- * internal global typescript context, which is then picked up on by this module.
48
- */
49
-
50
- /** A utility type used to extract the underlying `BalanceType` of a specific source from a generalised `BalanceJson` */
51
-
52
- /** TODO: Remove this in favour of a frontend-friendly `ChaindataProvider` */
53
-
54
- /**
55
- * A collection of balances.
56
- */
57
- class Balances {
58
- //
59
- // Properties
60
- //
61
-
62
- #balances = [];
63
-
64
- //
65
- // Methods
66
- //
67
-
68
- constructor(balances, hydrate) {
69
- // handle Balances (convert to Balance[])
70
- if (balances instanceof Balances) return new Balances(balances.each, hydrate);
71
-
72
- // handle Balance (convert to Balance[])
73
- if (balances instanceof Balance) return new Balances([balances], hydrate);
74
-
75
- // handle BalanceJsonList (the only remaining non-array type of balances) (convert to BalanceJson[])
76
- if (!Array.isArray(balances)) return new Balances(Object.values(balances), hydrate);
77
-
78
- // handle no balances
79
- if (balances.length === 0) return this;
80
-
81
- // handle BalanceJson[]
82
- if (!isArrayOf(balances, Balance)) return new Balances(balances.map(storage => new Balance(storage)), hydrate);
83
-
84
- // handle Balance[]
85
- this.#balances = balances;
86
- if (hydrate !== undefined) this.hydrate(hydrate);
87
- }
88
-
89
- /**
90
- * Calling toJSON on a collection of balances will return the underlying BalanceJsonList.
91
- */
92
- toJSON = () => Object.fromEntries(this.#balances.map(balance => {
93
- try {
94
- return [balance.id, balance.toJSON()];
95
- } catch (error) {
96
- log.error("Failed to convert balance to JSON", error, {
97
- id: balance.id,
98
- balance
99
- });
100
- return null;
101
- }
102
- }).filter(Array.isArray));
103
-
104
- /**
105
- * Allows the collection to be iterated over.
106
- *
107
- * @example
108
- * [...balances].forEach(balance => { // do something // })
109
- *
110
- * @example
111
- * for (const balance of balances) {
112
- * // do something
113
- * }
114
- */
115
- [Symbol.iterator] = () =>
116
- // Create an array of the balances in this collection and return the result of its iterator.
117
- this.#balances[Symbol.iterator]();
118
-
119
- /**
120
- * Hydrates all balances in this collection.
121
- *
122
- * @param sources - The sources to hydrate from.
123
- */
124
- hydrate = sources => {
125
- this.#balances.map(balance => balance.hydrate(sources));
126
- };
127
-
128
- /**
129
- * Retrieve a balance from this collection by id.
130
- *
131
- * @param id - The id of the balance to fetch.
132
- * @returns The balance if one exists, or none.
133
- */
134
- get = id => this.#balances.find(balance => balance.id === id) ?? null;
135
-
136
- /**
137
- * Retrieve balances from this collection by search query.
138
- *
139
- * @param query - The search query.
140
- * @returns All balances which match the query.
141
- */
142
- find = query => {
143
- // construct filter
144
- const queryArray = Array.isArray(query) ? query : [query];
145
- const orQueries = queryArray.map(query => typeof query === "function" ? query : Object.entries(query));
146
-
147
- // filter balances
148
- const filter = balance => orQueries.some(query => typeof query === "function" ? query(balance) : query.every(([key, value]) => balance[key] === value));
149
-
150
- // return filter matches
151
- return new Balances([...this].filter(filter));
152
- };
153
-
154
- /**
155
- * Filters this collection to exclude token balances where the token has a `mirrorOf` field
156
- * and another balance exists in this collection for the token specified by the `mirrorOf` field.
157
- */
158
- filterMirrorTokens = () => new Balances([...this].filter(filterMirrorTokens));
159
-
160
- /**
161
- * Filters this collection to only include balances which are not zero AND have a fiat conversion rate.
162
- */
163
- filterNonZeroFiat = (type, currency) => {
164
- const filter = balance => (balance[type].fiat(currency) ?? 0) > 0;
165
- return this.find(filter);
166
- };
167
-
168
- /**
169
- * Add some balances to this collection.
170
- * Added balances take priority over existing balances.
171
- * The aggregation of the two collections is returned.
172
- * The original collection is not mutated.
173
- *
174
- * @param balances - Either a balance or collection of balances to add.
175
- * @returns The new collection of balances.
176
- */
177
- add = balances => {
178
- // handle single balance
179
- if (balances instanceof Balance) return this.add(new Balances(balances));
180
-
181
- // merge balances
182
- const mergedBalances = Object.fromEntries(this.#balances.map(balance => [balance.id, balance]));
183
- balances.each.forEach(balance => mergedBalances[balance.id] = balance);
184
-
185
- // return new balances
186
- return new Balances(Object.values(mergedBalances));
187
- };
188
-
189
- /**
190
- * Remove balances from this collection by id.
191
- * A new collection without these balances is returned.
192
- * The original collection is not mutated.
193
- *
194
- * @param ids - The id(s) of the balances to remove.
195
- * @returns The new collection of balances.
196
- */
197
- remove = ids => {
198
- // handle single id
199
- if (!Array.isArray(ids)) return this.remove([ids]);
200
-
201
- // merge and return new balances
202
- return new Balances(this.#balances.filter(balance => !ids.includes(balance.id)));
203
- };
204
-
205
- // TODO: Add some more useful aggregator methods
206
-
207
- get each() {
208
- return [...this];
209
- }
210
-
211
- /** @deprecated use each instead */
212
- get sorted() {
213
- return this.each;
214
- }
215
-
216
- /**
217
- * Get the number of balances in this collection.
218
- *
219
- * @returns The number of balances in this collection.
220
- */
221
- get count() {
222
- return [...this].length;
223
- }
224
-
225
- /**
226
- * Get the summed value of balances in this collection.
227
- * TODO: Sum up token amounts AND fiat amounts
228
- *
229
- * @example
230
- * // Get the sum of all transferable balances in usd.
231
- * balances.sum.fiat('usd').transferable
232
- */
233
- get sum() {
234
- return new SumBalancesFormatter(this);
235
- }
236
- }
237
- const getBalanceId = balance => {
238
- const {
239
- address,
240
- tokenId
241
- } = balance;
242
- return [address, tokenId].join("::");
243
- };
244
-
245
- /**
246
- * An individual balance.
247
- */
248
- class Balance {
249
- //
250
- // Properties
251
- //
22
+ const MODULE_TYPE$8 = EvmErc20TokenSchema.shape.type.value;
23
+ const PLATFORM$8 = EvmErc20TokenSchema.shape.platform.value;
252
24
 
253
- /** The underlying data for this balance */
254
- #storage;
255
- #valueGetter;
256
- #db = null;
25
+ const abiMulticall = parseAbi(["struct Call { address target; bytes callData; }", "struct Call3 { address target; bool allowFailure; bytes callData; }", "struct Call3Value { address target; bool allowFailure; uint256 value; bytes callData; }", "struct Result { bool success; bytes returnData; }", "function aggregate(Call[] calldata calls) public payable returns (uint256 blockNumber, bytes[] memory returnData)", "function aggregate3(Call3[] calldata calls) public payable returns (Result[] memory returnData)", "function aggregate3Value(Call3Value[] calldata calls) public payable returns (Result[] memory returnData)", "function blockAndAggregate(Call[] calldata calls) public payable returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData)", "function getBasefee() view returns (uint256 basefee)", "function getBlockHash(uint256 blockNumber) view returns (bytes32 blockHash)", "function getBlockNumber() view returns (uint256 blockNumber)", "function getChainId() view returns (uint256 chainid)", "function getCurrentBlockCoinbase() view returns (address coinbase)", "function getCurrentBlockDifficulty() view returns (uint256 difficulty)", "function getCurrentBlockGasLimit() view returns (uint256 gaslimit)", "function getCurrentBlockTimestamp() view returns (uint256 timestamp)", "function getEthBalance(address addr) view returns (uint256 balance)", "function getLastBlockHash() view returns (bytes32 blockHash)", "function tryAggregate(bool requireSuccess, Call[] calldata calls) public payable returns (Result[] memory returnData)", "function tryBlockAndAggregate(bool requireSuccess, Call[] calldata calls) public payable returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData)"]);
257
26
 
258
- //
259
- // Methods
260
- //
261
-
262
- constructor(storage, hydrate) {
263
- this.#storage = storage;
264
- this.#valueGetter = new BalanceValueGetter(this.#storage);
265
- if (hydrate !== undefined) this.hydrate(hydrate);
266
- }
267
- toJSON = () => this.#storage;
268
- isSource = source => this.#storage.source === source;
269
- // // TODO: Fix this method, the types don't work with our plugin architecture.
270
- // // Specifically, the `BalanceJson` type is compiled down to `IBalance` in the following way:
271
- // //
272
- // // toJSON: () => BalanceJson // works
273
- // // isSource: (source: BalanceSource) => boolean // works
274
- // // asSource: <P extends string>(source: P) => NarrowBalanceType<IBalance, P> | null // Doesn't work! IBalance should just be BalanceJson!
275
- // //
276
- // // `IBalance` won't match the type of `BalanceSource` after `PluginBalanceTypes` has been extended by balance plugins.
277
- // // As a result, typescript will think that the returned #storage is not a BalanceJson.
278
- // asSource = <P extends BalanceSource>(source: P): NarrowBalanceType<BalanceJson, P> | null => {
279
- // if (this.#storage.source === source) return this.#storage as NarrowBalanceType<BalanceJson, P>
280
- // return null
281
- // }
282
-
283
- hydrate = hydrate => {
284
- if (hydrate !== undefined) this.#db = hydrate;
285
- };
286
- #format = balance => new BalanceFormatter(isBigInt(balance) ? balance.toString() : balance, this.decimals || undefined, this.rates);
287
-
288
- //
289
- // Accessors
290
- //
291
-
292
- get id() {
293
- return getBalanceId(this.#storage);
294
- }
295
- get source() {
296
- return this.#storage.source;
297
- }
298
- get status() {
299
- return this.#storage.status;
300
- }
301
- get address() {
302
- return this.#storage.address;
303
- }
304
- get networkId() {
305
- return this.#storage.networkId;
306
- }
307
- get network() {
308
- return this.#db?.networks?.[this.networkId] || null;
309
- }
310
- get tokenId() {
311
- return this.#storage.tokenId;
312
- }
313
- get token() {
314
- return this.#db?.tokens?.[this.tokenId] || null;
315
- }
316
- get decimals() {
317
- return this.token?.decimals || null;
318
- }
319
- get rates() {
320
- // uniswap v2 lp tokens need the rates from the underlying pool assets
321
- //
322
- // To note: `@talismn/token-rates` knows to fetch the `coingeckoId0` and `coingeckoId1` rates for evm-uniswapv2 tokens.
323
- // They are then stored in `this.#db.tokenRates` using the `tokenId0` and `tokenId1` keys.
324
- //
325
- // This means that those rates are always available for calculating the uniswapv2 rates,
326
- // regardless of whether or not the underlying erc20s are actually in chaindata and enabled.
327
- if (this.isSource("evm-uniswapv2") && this.token?.type === "evm-uniswapv2") {
328
- const tokenId0 = evmErc20TokenId(this.networkId, this.token.tokenAddress0);
329
- const tokenId1 = evmErc20TokenId(this.networkId, this.token.tokenAddress1);
330
- const decimals = this.token.decimals;
331
- const decimals0 = this.token.decimals0;
332
- const decimals1 = this.token.decimals1;
333
- const rates0 = this.#db?.tokenRates && this.#db.tokenRates[tokenId0];
334
- const rates1 = this.#db?.tokenRates && this.#db.tokenRates[tokenId1];
335
- if (rates0 === undefined || rates1 === undefined) return null;
336
- const extra = this.#valueGetter.get("extra");
337
- const extras = Array.isArray(extra) ? extra : extra !== undefined ? [extra] : [];
338
- const totalSupply = extras.find(extra => extra.label === "totalSupply")?.amount ?? "0";
339
- const reserve0 = extras.find(extra => extra.label === "reserve0")?.amount ?? "0";
340
- const reserve1 = extras.find(extra => extra.label === "reserve1")?.amount ?? "0";
341
- const totalSupplyTokens = BigNumber(totalSupply).times(Math.pow(10, -1 * decimals));
342
- const reserve0Tokens = BigNumber(reserve0).times(Math.pow(10, -1 * decimals0));
343
- const reserve1Tokens = BigNumber(reserve1).times(Math.pow(10, -1 * decimals1));
344
- const rates0Currencies = new Set(Object.keys(rates0));
345
- const rates1Currencies = new Set(Object.keys(rates1));
346
- // `Set.prototype.intersection` can eventually replace this
347
- // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/intersection
348
- const currencies = [...rates0Currencies].filter(c => rates1Currencies.has(c));
349
- const totalValueLocked = currencies.map(currency => [currency,
350
- // tvl (in a given currency) == reserve0*currencyRate0 + reserve1*currencyRate1
351
- BigNumber.sum(reserve0Tokens.times(rates0[currency]?.price ?? 0), reserve1Tokens.times(rates1[currency]?.price ?? 0))]);
352
- const lpTokenRates = newTokenRates();
353
- totalValueLocked.forEach(([currency, tvl]) => {
354
- // divide `the value of all lp tokens` by `the number of lp tokens` to get `the value per token`
355
- if (!totalSupplyTokens.eq(0)) lpTokenRates[currency] = {
356
- price: tvl.div(totalSupplyTokens).toNumber()
357
- };
358
- });
359
- return lpTokenRates;
360
- }
361
-
362
- // other tokens can just pick from the tokenRates db using the tokenId
363
- return this.#db?.tokenRates && this.#db.tokenRates[this.tokenId] || null;
364
- }
365
-
366
- /**
367
- * A general method to get formatted values matching a certain type from this balance.
368
- * @param valueType - The type of value to get.
369
- * @returns An array of the values matching the type with formatted amounts.
370
- */
371
- getValue(valueType) {
372
- return this.getRawValue(valueType).map(value => ({
373
- ...value,
374
- amount: this.#format(value.amount)
375
- }));
376
- }
377
-
378
- /**
379
- * A general method to get values matching a certain type from this balance.
380
- * @param valueType - The type of value to get.
381
- * @returns An array of the values matching the type.
382
- */
383
- getRawValue(valueType) {
384
- return this.#valueGetter.get(valueType);
385
- }
386
-
387
- /**
388
- * A general method to add a value to the array of values for this balance.
389
- * @param valueType - The type of value to add.
390
- * @returns A function which can be used to add a value to the array of values for this balance.
391
- */
392
- addValue(valueType) {
393
- return value => this.#valueGetter.add(valueType, value);
394
- }
395
- /**
396
- * The total balance of this token.
397
- * Includes the free and the reserved amount.
398
- * The balance will be reaped if this goes below the existential deposit.
399
- */
400
- get total() {
401
- const extra = this.getValue("extra");
402
-
403
- // if there is a DelegatedStaking hold (new model: polkadot, kusama), nom pool staked amount is included in reserved
404
- // if not (old model: vara, avail, cere), staked amount is not in the account and it needs to be added to the total
405
- const nomPoolStakedPlancks = this.locks.some(lock => lock.source === "substrate-native-holds" && lock.label === "DelegatedStaking") ? 0n : this.nompools.map(({
406
- amount
407
- }) => amount.planck).reduce((a, b) => a + b, 0n);
408
- return this.#format(this.free.planck + this.reserved.planck + nomPoolStakedPlancks + this.subtensor.map(({
409
- amount
410
- }) => amount.planck).reduce((a, b) => a + b, 0n) + includeInTotalExtraAmount(extra));
411
- }
412
- /** The non-reserved balance of this token. Includes the frozen amount. Is included in the total. */
413
- get free() {
414
- // for simple balances
415
- if ("value" in this.#storage && this.#storage.value) return this.#format(this.#storage.value);
416
-
417
- // for complex balances
418
- const freeValues = this.getValue("free");
419
- const totalFree = freeValues.map(({
420
- amount
421
- }) => amount.planck).reduce((a, b) => a + b, 0n);
422
- return this.#format(totalFree);
423
- }
424
- /** The reserved balance of this token. Is included in the total. */
425
- get reserved() {
426
- const reservedValues = this.getValue("reserved");
427
- if (reservedValues.length === 0) return this.#format(0n);
428
- return this.#format(reservedValues.map(({
429
- amount
430
- }) => amount.planck).reduce((a, b) => a + b, 0n));
431
- }
432
- get reserves() {
433
- return this.getValue("reserved");
434
- }
435
- /** The frozen balance of this token. Is included in the free amount. */
436
- get locked() {
437
- return this.#format(this.locks.map(({
438
- amount
439
- }) => amount.planck).reduce((a, b) => BigMath.max(a, b), 0n));
440
- }
441
- get locks() {
442
- return this.getValue("locked");
443
- }
444
- get nompools() {
445
- return this.getValue("nompool");
446
- }
447
- get subtensor() {
448
- return this.getValue("subtensor");
449
- }
450
-
451
- /** The extra balance of this token */
452
- get extra() {
453
- const extra = this.getRawValue("extra");
454
- if (extra.length > 0) return extra;
455
- return undefined;
456
- }
457
-
458
- /** @deprecated Use balance.locked */
459
- get frozen() {
460
- return this.locked;
461
- }
462
- /** The transferable balance of this token. Is generally the free amount - the miscFrozen amount. */
463
- get transferable() {
464
- /**
465
- * As you can see here, `locked` is subtracted from `free` in order to derive `transferable`.
466
- *
467
- * |--------------------------total--------------------------|
468
- * |-------------------free-------------------|---reserved---|
469
- * |----locked-----|-------transferable-------|
470
- */
471
- const oldTransferableCalculation = () => {
472
- // if no locks exist, transferable is equal to the free amount
473
- if (this.locks.length === 0) return this.free;
474
-
475
- // find the largest lock (but ignore any locks which are marked as `includeInTransferable`)
476
- const excludeAmount = excludeFromTransferableAmount(this.locks);
477
-
478
- // subtract the lock from the free amount (but don't go below 0)
479
- return this.#format(BigMath.max(this.free.planck - excludeAmount, 0n));
480
- };
481
-
482
- /**
483
- * As you can see here, `locked` is subtracted from `free + reserved` in order to derive `transferable`.
484
- *
485
- * Alternatively, `reserved` is subtracted from `locked` in order to derive `untouchable`,
486
- * which is then subtracted from `free` in order to derive `transferable`.
487
- *
488
- * |--------------------------total--------------------------|
489
- * |---reserved---|-------------------free-------------------|
490
- * |--untouchable--|
491
- * |------------locked------------|-------transferable-------|
492
- */
493
- const newTransferableCalculation = () => {
494
- // if no locks exist, transferable is equal to the free amount
495
- if (this.locks.length === 0) return this.free;
496
-
497
- // find the largest lock (but ignore any locks which are marked as `includeInTransferable`)
498
- // subtract the reserved amount, because locks now act upon the total balance - not just the free balance
499
- const untouchableAmount = BigMath.max(excludeFromTransferableAmount(this.locks) - this.reserved.planck, 0n);
500
-
501
- // subtract the untouchable amount from the free amount (but don't go below 0)
502
- return this.#format(BigMath.max(this.free.planck - untouchableAmount, 0n));
503
- };
504
- if (this.#storage.useLegacyTransferableCalculation) return oldTransferableCalculation();
505
- return newTransferableCalculation();
506
- }
507
- /**
508
- * The unavailable balance of this token.
509
- * Prior to the Fungible trait, this was the locked amount + the reserved amount, i.e. `locked + reserved`.
510
- * Now, it is the bigger of the locked amount and the reserved amounts, i.e. `max(locked, reserved)`.
511
- */
512
- get unavailable() {
513
- const oldCalculation = () => this.locked.planck + this.reserved.planck;
514
- const newCalculation = () => BigMath.max(this.locked.planck, this.reserved.planck);
515
- const baseUnavailable = this.#storage.useLegacyTransferableCalculation ? oldCalculation() : newCalculation();
516
-
517
- // if there is a DelegatedStaking hold (new model: polkadot, kusama), nom pool staked amount is included in reserved
518
- // if not (old model: vara, avail, cere), staked amount is not in the account and it needs to be added to the total
519
- const nomPoolStakedPlancks = this.locks.some(lock => lock.source === "substrate-native-holds" && lock.label === "DelegatedStaking") ? 0n : this.nompools.map(({
520
- amount
521
- }) => amount.planck).reduce((a, b) => a + b, 0n);
522
- const otherUnavailable = nomPoolStakedPlancks + this.subtensor.reduce((total, each) => total + each.amount.planck, 0n);
523
- return this.#format(baseUnavailable + otherUnavailable);
524
- }
525
-
526
- /** The feePayable balance of this token. Is generally the free amount - the feeFrozen amount. */
527
- get feePayable() {
528
- // if no locks exist, feePayable is equal to the free amount
529
- if (this.locks.length === 0) return this.free;
530
-
531
- // find the largest lock which can't be used to pay tx fees
532
- const excludeAmount = excludeFromFeePayableLocks(this.locked.planck.toString()).map(lock => BigInt(lock.amount)).reduce((max, lock) => BigMath.max(max, lock), 0n);
533
-
534
- // subtract the lock from the free amount (but don't go below 0)
535
- return this.#format(BigMath.max(this.free.planck - excludeAmount, 0n));
536
- }
537
- }
538
- class BalanceValueGetter {
539
- #storage;
540
- constructor(storage) {
541
- this.#storage = storage;
542
- }
543
- get(valueType) {
544
- if ("values" in this.#storage && this.#storage.values) return this.#storage.values.filter(({
545
- type
546
- }) => type === valueType);
547
- return [];
548
- }
549
- add(valueType, amount) {
550
- if ("values" in this.#storage && this.#storage.values) this.#storage.values.push({
551
- type: valueType,
552
- ...amount
553
- });
554
- }
555
- }
556
- class BalanceFormatter {
557
- #planck;
558
- #decimals;
559
- #tokenRates;
560
- constructor(planck, decimals, fiatRatios) {
561
- this.#planck = isBigInt(planck) ? planck.toString() : planck ?? "0";
562
- this.#decimals = decimals || 0;
563
- this.#tokenRates = fiatRatios || null;
564
- }
565
- toJSON = () => this.#planck;
566
- get planck() {
567
- return BigInt(this.#planck);
568
- }
569
- get tokens() {
570
- return planckToTokens(this.#planck, this.#decimals);
571
- }
572
- fiat(currency) {
573
- if (!this.#tokenRates) return null;
574
- const ratio = this.#tokenRates[currency];
575
- if (!ratio) return null;
576
- return parseFloat(this.tokens) * ratio.price;
577
- }
578
- }
579
- class PlanckSumBalancesFormatter {
580
- #balances;
581
- constructor(balances) {
582
- this.#balances = balances;
583
- }
584
- #sum = balanceField => {
585
- // a function to get a planck amount from a balance
586
- const planck = balance => balance[balanceField].planck ?? 0n;
587
- return this.#balances.filterMirrorTokens().each.reduce(
588
- // add the total amount to the planck amount of each balance
589
- (total, balance) => total + planck(balance),
590
- // start with a total of 0
591
- 0n);
592
- };
593
-
594
- /**
595
- * The total balance of these tokens. Includes the free and the reserved amount.
596
- */
597
- get total() {
598
- return this.#sum("total");
599
- }
600
- /** The non-reserved balance of these tokens. Includes the frozen amount. Is included in the total. */
601
- get free() {
602
- return this.#sum("free");
603
- }
604
- /** The reserved balance of these tokens. Is included in the total. */
605
- get reserved() {
606
- return this.#sum("reserved");
607
- }
608
- /** The frozen balance of these tokens. Is included in the free amount. */
609
- get locked() {
610
- return this.#sum("locked");
611
- }
612
- /** @deprecated Use balances.locked */
613
- get frozen() {
614
- return this.locked;
615
- }
616
- /** The transferable balance of these tokens. Is generally the free amount - the miscFrozen amount. */
617
- get transferable() {
618
- return this.#sum("transferable");
619
- }
620
- /** The unavailable balance of these tokens. */
621
- get unavailable() {
622
- return this.#sum("unavailable");
623
- }
624
-
625
- /** The feePayable balance of these tokens. Is generally the free amount - the feeFrozen amount. */
626
- get feePayable() {
627
- return this.#sum("feePayable");
628
- }
629
- }
630
- class FiatSumBalancesFormatter {
631
- #balances;
632
- #currency;
633
- constructor(balances, currency) {
634
- this.#balances = balances;
635
- this.#currency = currency;
636
- }
637
- #sum = balanceField => {
638
- // a function to get a fiat amount from a balance
639
- const fiat = balance => balance[balanceField].fiat(this.#currency) ?? 0;
640
- return this.#balances.filterMirrorTokens().each.reduce(
641
- // add the total amount to the fiat amount of each balance
642
- (total, balance) => total + fiat(balance),
643
- // start with a total of 0
644
- 0);
645
- };
646
-
647
- /**
648
- * The total balance of these tokens. Includes the free and the reserved amount.
649
- */
650
- get total() {
651
- return this.#sum("total");
652
- }
653
- /** The non-reserved balance of these tokens. Includes the frozen amount. Is included in the total. */
654
- get free() {
655
- return this.#sum("free");
656
- }
657
- /** The reserved balance of these tokens. Is included in the total. */
658
- get reserved() {
659
- return this.#sum("reserved");
660
- }
661
- /** The frozen balance of these tokens. Is included in the free amount. */
662
- get locked() {
663
- return this.#sum("locked");
664
- }
665
- /** @deprecated Use balances.locked */
666
- get frozen() {
667
- return this.locked;
668
- }
669
- /** The transferable balance of these tokens. Is generally the free amount - the miscFrozen amount. */
670
- get transferable() {
671
- return this.#sum("transferable");
672
- }
673
- /** The unavailable balance of these tokens. */
674
- get unavailable() {
675
- return this.#sum("unavailable");
676
- }
677
- /** The feePayable balance of these tokens. Is generally the free amount - the feeFrozen amount. */
678
- get feePayable() {
679
- return this.#sum("feePayable");
680
- }
681
- }
682
- class SumBalancesFormatter {
683
- #balances;
684
- constructor(balances) {
685
- this.#balances = balances;
686
- }
687
- get planck() {
688
- return new PlanckSumBalancesFormatter(this.#balances);
689
- }
690
- fiat(currency) {
691
- return new FiatSumBalancesFormatter(this.#balances, currency);
692
- }
693
- change24h(currency) {
694
- return new Change24hCurrencyFormatter(this.#balances, currency);
695
- }
696
- }
697
- class Change24hCurrencyFormatter {
698
- #balances;
699
- #currency;
700
- constructor(balances, currency) {
701
- this.#balances = balances;
702
- this.#currency = currency;
703
- }
704
- #change24h = balanceField => {
705
- const output = this.#balances.filterMirrorTokens().each.reduce(
706
- // add the total amount to the fiat amount of each balance
707
- (acc, balance) => {
708
- const change24h = balance.rates?.[this.#currency]?.change24h;
709
- if (typeof change24h !== "number") return acc;
710
- const fiat = balance[balanceField].fiat(this.#currency);
711
- if (!fiat) return acc;
712
- return {
713
- totalFiatDiff: acc.totalFiatDiff + fiat * change24h,
714
- totalFiat: acc.totalFiat + fiat
715
- };
716
- },
717
- // start with a total of 0
718
- {
719
- totalFiatDiff: 0,
720
- totalFiat: 0
721
- });
722
- return output.totalFiat === 0 ? null : {
723
- diff: output.totalFiatDiff / 100,
724
- ratio: output.totalFiatDiff / output.totalFiat
725
- };
726
- };
727
- get total() {
728
- return this.#change24h("total");
729
- }
730
- get free() {
731
- return this.#change24h("free");
732
- }
733
- get reserved() {
734
- return this.#change24h("reserved");
735
- }
736
- get locked() {
737
- return this.#change24h("locked");
738
- }
739
- get frozen() {
740
- return this.#change24h("frozen");
741
- }
742
- get transferable() {
743
- return this.#change24h("transferable");
744
- }
745
- get unavailable() {
746
- return this.#change24h("unavailable");
747
- }
748
- get feePayable() {
749
- return this.#change24h("feePayable");
750
- }
751
- }
752
- const filterMirrorTokens = (balance, i, balances) => {
753
- const mirrorOf = balance.token?.mirrorOf;
754
- return !mirrorOf || !balances.find(b => b.tokenId === mirrorOf);
755
- };
756
-
757
- /** `IBalance` is a common interface which all balance types must implement. */
758
-
759
- /** A collection of `BalanceJson` objects */
760
-
761
- /** An unlabelled amount of a balance */
762
-
763
- /** A labelled amount of a balance */
764
-
765
- const getValueId = amount => {
766
- const getMetaId = () => {
767
- const meta = amount.meta;
768
- if (!meta) return "";
769
- if (amount.type === "nompool") return meta.poolId?.toString() ?? "";
770
- if (amount.type === "subtensor") {
771
- const {
772
- hotkey,
773
- netuid
774
- } = meta;
775
- if (hotkey && netuid !== undefined) return `${hotkey.toString()}${netuid.toString()}`;
776
- }
777
- return "";
778
- };
779
- return [amount.label, amount.type, amount.source, getMetaId()].join("::");
780
- };
781
-
782
- /** A labelled locked amount of a balance */
783
-
784
- /** A labelled extra amount of a balance */
785
-
786
- /** Used by plugins to help define their custom `BalanceType` */
787
-
788
- /** For fast db access, you can calculate the primary key for a miniMetadata using this method */
789
- const deriveMiniMetadataId = ({
790
- source,
791
- chainId,
792
- specVersion
793
- }) => toHex(Twox64Concat(new TextEncoder().encode(`${source}${chainId}${specVersion}${MINIMETADATA_VERSION}`))).slice(-64);
794
-
795
- // for DB version 3, Wallet version 1.21.0
796
- const upgradeRemoveSymbolFromNativeTokenId = async tx => {
797
- const balancesTable = tx.table("balances");
798
- try {
799
- await balancesTable.toCollection().modify(balance => {
800
- if (balance?.tokenId?.includes?.("-substrate-native-")) {
801
- balance.tokenId = balance.tokenId.replace(/-substrate-native-.+$/, "-substrate-native");
802
- balance.id = new Balance(balance).id;
803
- }
804
- if (balance?.tokenId?.includes?.("-evm-native-")) {
805
- balance.tokenId = balance.tokenId.replace(/-evm-native-.+$/, "-evm-native");
806
- balance.id = new Balance(balance).id;
807
- }
808
- });
809
- } catch (err) {
810
- // error most likely due to duplicate ids => clear the db
811
- log.error("Failed to upgrade balances native token ids", {
812
- err
813
- });
814
- await balancesTable.clear();
815
- }
816
- };
817
-
818
- // "locks": [{"label":"fees","amount":"0","includeInTransferable":true,"excludeFromFeePayable":true},{"label":"misc","amount":"0"}]
819
- // "reserves":[{"label":"reserved","amount":"0"}]
820
-
821
- // for DB version 4, Wallet version 1.26.0
822
- // converts the structured balances data to a single compressed string
823
- const upgradeBalancesDataBlob = async tx => {
824
- const balancesTable = tx.table("balances");
825
- const balancesData = await balancesTable.toCollection().toArray();
826
- // migrate to new format
827
- const complexBalanceTypes = ["substrate-native", "substrate-tokens", "substrate-assets"];
828
- const migratedData = balancesData.map(balance => {
829
- if (complexBalanceTypes.includes(balance.source)) {
830
- // too complicated to migrate subsource balances, so just clear them
831
- if ("subSource" in balance) return false;
832
- if (balance.free && balance.free !== "0") balance.values = [{
833
- amount: balance.free,
834
- type: "free",
835
- label: "free"
836
- }];else return false;
837
- } else {
838
- if (balance.free && balance.free !== "0") balance.value = balance.free;else return false;
839
- }
840
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
841
- const {
842
- locks,
843
- reserves,
844
- extra,
845
- ...stuffToKeep
846
- } = balance;
847
- return stuffToKeep;
848
- }).filter(isTruthy);
849
- const output = pako.deflate(JSON.stringify(migratedData));
850
- // now write the compressed data back to the db and clear the old one
851
- await tx.table("balancesBlob").put({
852
- data: output,
853
- id: Date.now().toString()
854
- });
855
- await tx.table("balances").clear();
856
- };
857
-
858
- class TalismanBalancesDatabase extends Dexie {
859
- constructor() {
860
- super("TalismanBalances");
861
- // https://dexie.org/docs/Tutorial/Design#database-versioning
862
- // You only need to specify properties that you wish to index.
863
-
864
- // The object store will allow any properties on your stored objects but you can only query them by indexed properties
865
- // https://dexie.org/docs/API-Reference#declare-database
866
- //
867
- // Never index properties containing images, movies or large (huge) strings. Store them in IndexedDB, yes! but just don’t index them!
868
- // https://dexie.org/docs/Version/Version.stores()#warning
869
-
870
- this.version(5).stores({
871
- balancesBlob: "id",
872
- miniMetadatas: "id, source, chainId, specName, specVersion",
873
- balances: null
874
- });
875
- this.version(4).stores({
876
- balances: "id",
877
- balancesBlob: "id",
878
- miniMetadatas: "id, source, chainId, specName, specVersion"
879
- }).upgrade(upgradeBalancesDataBlob);
880
- this.version(3).stores({
881
- balances: "id, source, address, tokenId",
882
- miniMetadatas: "id, source, chainId, specName, specVersion"
883
- }).upgrade(upgradeRemoveSymbolFromNativeTokenId);
884
- }
885
- }
886
- const db = new TalismanBalancesDatabase();
887
-
888
- const MODULE_TYPE$8 = EvmErc20TokenSchema.shape.type.value;
889
- const PLATFORM$8 = EvmErc20TokenSchema.shape.platform.value;
890
-
891
- const abiMulticall = parseAbi(["struct Call { address target; bytes callData; }", "struct Call3 { address target; bool allowFailure; bytes callData; }", "struct Call3Value { address target; bool allowFailure; uint256 value; bytes callData; }", "struct Result { bool success; bytes returnData; }", "function aggregate(Call[] calldata calls) public payable returns (uint256 blockNumber, bytes[] memory returnData)", "function aggregate3(Call3[] calldata calls) public payable returns (Result[] memory returnData)", "function aggregate3Value(Call3Value[] calldata calls) public payable returns (Result[] memory returnData)", "function blockAndAggregate(Call[] calldata calls) public payable returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData)", "function getBasefee() view returns (uint256 basefee)", "function getBlockHash(uint256 blockNumber) view returns (bytes32 blockHash)", "function getBlockNumber() view returns (uint256 blockNumber)", "function getChainId() view returns (uint256 chainid)", "function getCurrentBlockCoinbase() view returns (address coinbase)", "function getCurrentBlockDifficulty() view returns (uint256 difficulty)", "function getCurrentBlockGasLimit() view returns (uint256 gaslimit)", "function getCurrentBlockTimestamp() view returns (uint256 timestamp)", "function getEthBalance(address addr) view returns (uint256 balance)", "function getLastBlockHash() view returns (bytes32 blockHash)", "function tryAggregate(bool requireSuccess, Call[] calldata calls) public payable returns (Result[] memory returnData)", "function tryBlockAndAggregate(bool requireSuccess, Call[] calldata calls) public payable returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData)"]);
892
-
893
- const erc20BalancesAggregatorAbi = parseAbi(["struct AccountToken {address account; address token;}", "function balances(AccountToken[] memory accountTokens) public view returns (uint256[] memory)"]);
27
+ const erc20BalancesAggregatorAbi = parseAbi(["struct AccountToken {address account; address token;}", "function balances(AccountToken[] memory accountTokens) public view returns (uint256[] memory)"]);
894
28
 
895
29
  const uniswapV2PairAbi = [{
896
30
  anonymous: false,
@@ -1584,6 +718,11 @@ const fetchWithAggregator$1 = async (client, balanceDefs, erc20BalancesAggregato
1584
718
  }
1585
719
  };
1586
720
 
721
+ var packageJson = {
722
+ name: "@talismn/balances"};
723
+
724
+ var log = anylogger(packageJson.name);
725
+
1587
726
  const getErc20ContractData$1 = async (client, contractAddress) => {
1588
727
  try {
1589
728
  const contract = getTypedContract$1(client, erc20Abi, contractAddress);
@@ -1699,20 +838,240 @@ const getTransferCallData$8 = ({
1699
838
  if (!isTokenOfType(token, MODULE_TYPE$8)) throw new Error(`Token type ${token.type} is not ${MODULE_TYPE$8}.`);
1700
839
  if (!isEthereumAddress(from)) throw new Error("Invalid from address");
1701
840
  if (!isEthereumAddress(to)) throw new Error("Invalid to address");
1702
- const data = encodeFunctionData({
1703
- abi: erc20Abi,
1704
- functionName: "transfer",
1705
- args: [to, BigInt(value)]
1706
- });
841
+ const data = encodeFunctionData({
842
+ abi: erc20Abi,
843
+ functionName: "transfer",
844
+ args: [to, BigInt(value)]
845
+ });
846
+ return {
847
+ from,
848
+ to: token.contractAddress,
849
+ data
850
+ };
851
+ };
852
+
853
+ const SUBSCRIPTION_INTERVAL$4 = 6_000;
854
+ const subscribeBalances$8 = ({
855
+ networkId,
856
+ tokensWithAddresses,
857
+ connector
858
+ }) => {
859
+ if (!tokensWithAddresses.length) return of({
860
+ success: [],
861
+ errors: []
862
+ });
863
+ return new Observable(subscriber => {
864
+ const abortController = new AbortController();
865
+ const poll = async () => {
866
+ try {
867
+ if (abortController.signal.aborted) return;
868
+ const balances = await fetchBalances$8({
869
+ networkId,
870
+ tokensWithAddresses: tokensWithAddresses,
871
+ connector
872
+ });
873
+ if (abortController.signal.aborted) return;
874
+ subscriber.next(balances);
875
+ setTimeout(poll, SUBSCRIPTION_INTERVAL$4);
876
+ } catch (error) {
877
+ log.error("Error", {
878
+ module: MODULE_TYPE$8,
879
+ networkId,
880
+ addressesByToken: tokensWithAddresses,
881
+ error
882
+ });
883
+ subscriber.error(error);
884
+ }
885
+ };
886
+ poll();
887
+ return () => {
888
+ abortController.abort();
889
+ };
890
+ }).pipe(distinctUntilChanged(isEqual));
891
+ };
892
+
893
+ const EvmErc20BalanceModule = {
894
+ type: MODULE_TYPE$8,
895
+ platform: PLATFORM$8,
896
+ getMiniMetadata: getMiniMetadata$8,
897
+ fetchTokens: fetchTokens$8,
898
+ fetchBalances: fetchBalances$8,
899
+ subscribeBalances: subscribeBalances$8,
900
+ getTransferCallData: getTransferCallData$8
901
+ };
902
+
903
+ const TokenConfigBaseSchema = TokenBaseSchema.partial().omit({
904
+ id: true
905
+ });
906
+
907
+ // to be used by chaindata too
908
+ const EvmErc20TokenConfigSchema = z.strictObject({
909
+ contractAddress: EvmErc20TokenSchema.shape.contractAddress,
910
+ ...TokenConfigBaseSchema.shape
911
+ });
912
+
913
+ const MODULE_TYPE$7 = EvmNativeTokenSchema.shape.type.value;
914
+ const PLATFORM$7 = EvmNativeTokenSchema.shape.platform.value;
915
+
916
+ const fetchBalances$7 = async ({
917
+ networkId,
918
+ tokensWithAddresses,
919
+ connector
920
+ }) => {
921
+ if (!tokensWithAddresses.length) return {
922
+ success: [],
923
+ errors: []
924
+ };
925
+ const client = await connector.getPublicClientForEvmNetwork(networkId);
926
+ if (!client) throw new Error(`Could not get rpc provider for evm network ${networkId}`);
927
+ for (const [token, addresses] of tokensWithAddresses) {
928
+ if (token.type !== MODULE_TYPE$7 || token.networkId !== networkId) throw new Error(`Invalid token type or networkId for EVM ERC20 balance module: ${token.type} on ${token.networkId}`);
929
+ for (const address of addresses) if (!isEthereumAddress(address)) throw new Error(`Invalid ethereum address for EVM ERC20 balance module: ${address} for token ${token.id}`);
930
+ }
931
+ const balanceDefs = getBalanceDefs(tokensWithAddresses);
932
+ if (client.chain?.contracts?.multicall3 && balanceDefs.length > 1) {
933
+ const multicall3 = client.chain.contracts.multicall3;
934
+ return fetchWithMulticall(client, balanceDefs, multicall3.address);
935
+ }
936
+ return fetchWithoutMulticall(client, balanceDefs);
937
+ };
938
+ const fetchWithoutMulticall = async (client, balanceDefs) => {
939
+ if (balanceDefs.length === 0) return {
940
+ success: [],
941
+ errors: []
942
+ };
943
+ const results = await Promise.allSettled(balanceDefs.map(async ({
944
+ token,
945
+ address
946
+ }) => {
947
+ try {
948
+ const result = await client.getBalance({
949
+ address
950
+ });
951
+ const balance = {
952
+ address,
953
+ tokenId: token.id,
954
+ value: result.toString(),
955
+ source: MODULE_TYPE$7,
956
+ networkId: parseTokenId(token.id).networkId,
957
+ status: "live"
958
+ };
959
+ return balance;
960
+ } catch (err) {
961
+ throw new BalanceFetchError(`Failed to get balance for token ${token.id} and address ${address} on chain ${client.chain?.id}`, token.id, address, err);
962
+ }
963
+ }));
964
+ return results.reduce((acc, result) => {
965
+ if (result.status === "fulfilled") acc.success.push(result.value);else {
966
+ const error = result.reason;
967
+ acc.errors.push({
968
+ tokenId: error.tokenId,
969
+ address: error.address,
970
+ error
971
+ });
972
+ }
973
+ return acc;
974
+ }, {
975
+ success: [],
976
+ errors: []
977
+ });
978
+ };
979
+ const fetchWithMulticall = async (client, balanceDefs, multicall3Address) => {
980
+ if (balanceDefs.length === 0) return {
981
+ success: [],
982
+ errors: []
983
+ };
984
+ try {
985
+ const callResults = await client.multicall({
986
+ contracts: balanceDefs.map(({
987
+ address
988
+ }) => ({
989
+ address: multicall3Address,
990
+ abi: abiMulticall,
991
+ functionName: "getEthBalance",
992
+ args: [address]
993
+ }))
994
+ });
995
+ return callResults.reduce((acc, result, index) => {
996
+ if (result.status === "success") {
997
+ acc.success.push({
998
+ address: balanceDefs[index].address,
999
+ tokenId: balanceDefs[index].token.id,
1000
+ value: result.result.toString(),
1001
+ source: MODULE_TYPE$7,
1002
+ networkId: parseTokenId(balanceDefs[index].token.id).networkId,
1003
+ status: "live"
1004
+ });
1005
+ }
1006
+ if (result.status === "failure") {
1007
+ acc.errors.push({
1008
+ tokenId: balanceDefs[index].token.id,
1009
+ address: balanceDefs[index].address,
1010
+ error: new BalanceFetchError(`Failed to get balance for token ${balanceDefs[index].token.id} and address ${balanceDefs[index].address} on chain ${client.chain?.id}`, balanceDefs[index].token.id, balanceDefs[index].address, result.error)
1011
+ });
1012
+ }
1013
+ return acc;
1014
+ }, {
1015
+ success: [],
1016
+ errors: []
1017
+ });
1018
+ } catch (err) {
1019
+ const errors = balanceDefs.map(balanceDef => ({
1020
+ tokenId: balanceDef.token.id,
1021
+ address: balanceDef.address,
1022
+ error: new BalanceFetchNetworkError(`Failed to get balances for evm-erc20 tokens on chain ${client.chain?.id}`, String(client.chain?.id), err)
1023
+ }));
1024
+ return {
1025
+ success: [],
1026
+ errors
1027
+ };
1028
+ }
1029
+ };
1030
+
1031
+ const fetchTokens$7 = async ({
1032
+ networkId,
1033
+ tokens
1034
+ }) => {
1035
+ // assume there is one and only one token in the array
1036
+ if (tokens.length !== 1) throw new Error("EVM Native module expects the nativeCurrency to be passed as a single token in the array");
1037
+ const token = assign({
1038
+ id: evmNativeTokenId(networkId),
1039
+ type: MODULE_TYPE$7,
1040
+ platform: PLATFORM$7,
1041
+ networkId,
1042
+ isDefault: true
1043
+ }, tokens[0]);
1044
+ const parsed = EvmNativeTokenSchema.safeParse(token);
1045
+ if (!parsed.success) {
1046
+ log.warn("Ignoring token with invalid schema", token);
1047
+ return [];
1048
+ }
1049
+ return [parsed.data];
1050
+ };
1051
+
1052
+ const getMiniMetadata$7 = () => {
1053
+ throw new Error("MiniMetadata is not supported for ethereum tokens");
1054
+ };
1055
+
1056
+ const getTransferCallData$7 = ({
1057
+ from,
1058
+ to,
1059
+ value,
1060
+ token
1061
+ }) => {
1062
+ if (!isTokenOfType(token, MODULE_TYPE$7)) throw new Error(`Token type ${token.type} is not ${MODULE_TYPE$7}.`);
1063
+ if (!isEthereumAddress(from)) throw new Error("Invalid from address");
1064
+ if (!isEthereumAddress(to)) throw new Error("Invalid to address");
1707
1065
  return {
1708
1066
  from,
1709
- to: token.contractAddress,
1710
- data
1067
+ to,
1068
+ value,
1069
+ data: "0x"
1711
1070
  };
1712
1071
  };
1713
1072
 
1714
- const SUBSCRIPTION_INTERVAL$4 = 6_000;
1715
- const subscribeBalances$8 = ({
1073
+ const SUBSCRIPTION_INTERVAL$3 = 6_000;
1074
+ const subscribeBalances$7 = ({
1716
1075
  networkId,
1717
1076
  tokensWithAddresses,
1718
1077
  connector
@@ -1726,17 +1085,17 @@ const subscribeBalances$8 = ({
1726
1085
  const poll = async () => {
1727
1086
  try {
1728
1087
  if (abortController.signal.aborted) return;
1729
- const balances = await fetchBalances$8({
1088
+ const balances = await fetchBalances$7({
1730
1089
  networkId,
1731
1090
  tokensWithAddresses: tokensWithAddresses,
1732
1091
  connector
1733
1092
  });
1734
1093
  if (abortController.signal.aborted) return;
1735
1094
  subscriber.next(balances);
1736
- setTimeout(poll, SUBSCRIPTION_INTERVAL$4);
1095
+ setTimeout(poll, SUBSCRIPTION_INTERVAL$3);
1737
1096
  } catch (error) {
1738
1097
  log.error("Error", {
1739
- module: MODULE_TYPE$8,
1098
+ module: MODULE_TYPE$7,
1740
1099
  networkId,
1741
1100
  addressesByToken: tokensWithAddresses,
1742
1101
  error
@@ -1751,30 +1110,25 @@ const subscribeBalances$8 = ({
1751
1110
  }).pipe(distinctUntilChanged(isEqual));
1752
1111
  };
1753
1112
 
1754
- const EvmErc20BalanceModule = {
1755
- type: MODULE_TYPE$8,
1756
- platform: PLATFORM$8,
1757
- getMiniMetadata: getMiniMetadata$8,
1758
- fetchTokens: fetchTokens$8,
1759
- fetchBalances: fetchBalances$8,
1760
- subscribeBalances: subscribeBalances$8,
1761
- getTransferCallData: getTransferCallData$8
1113
+ const EvmNativeBalanceModule = {
1114
+ type: MODULE_TYPE$7,
1115
+ platform: PLATFORM$7,
1116
+ getMiniMetadata: getMiniMetadata$7,
1117
+ fetchTokens: fetchTokens$7,
1118
+ fetchBalances: fetchBalances$7,
1119
+ subscribeBalances: subscribeBalances$7,
1120
+ getTransferCallData: getTransferCallData$7
1762
1121
  };
1763
1122
 
1764
- const TokenConfigBaseSchema = TokenBaseSchema.partial().omit({
1765
- id: true
1766
- });
1767
-
1768
1123
  // to be used by chaindata too
1769
- const EvmErc20TokenConfigSchema = z.strictObject({
1770
- contractAddress: EvmErc20TokenSchema.shape.contractAddress,
1124
+ const EvmNativeTokenConfigSchema = z.strictObject({
1771
1125
  ...TokenConfigBaseSchema.shape
1772
1126
  });
1773
1127
 
1774
- const MODULE_TYPE$7 = EvmNativeTokenSchema.shape.type.value;
1775
- const PLATFORM$7 = EvmNativeTokenSchema.shape.platform.value;
1128
+ const MODULE_TYPE$6 = EvmUniswapV2TokenSchema.shape.type.value;
1129
+ const PLATFORM$6 = EvmUniswapV2TokenSchema.shape.platform.value;
1776
1130
 
1777
- const fetchBalances$7 = async ({
1131
+ const fetchBalances$6 = async ({
1778
1132
  networkId,
1779
1133
  tokensWithAddresses,
1780
1134
  connector
@@ -1786,17 +1140,17 @@ const fetchBalances$7 = async ({
1786
1140
  const client = await connector.getPublicClientForEvmNetwork(networkId);
1787
1141
  if (!client) throw new Error(`Could not get rpc provider for evm network ${networkId}`);
1788
1142
  for (const [token, addresses] of tokensWithAddresses) {
1789
- if (token.type !== MODULE_TYPE$7 || token.networkId !== networkId) throw new Error(`Invalid token type or networkId for EVM ERC20 balance module: ${token.type} on ${token.networkId}`);
1143
+ if (token.type !== MODULE_TYPE$6 || token.networkId !== networkId) throw new Error(`Invalid token type or networkId for EVM ERC20 balance module: ${token.type} on ${token.networkId}`);
1790
1144
  for (const address of addresses) if (!isEthereumAddress(address)) throw new Error(`Invalid ethereum address for EVM ERC20 balance module: ${address} for token ${token.id}`);
1791
1145
  }
1792
1146
  const balanceDefs = getBalanceDefs(tokensWithAddresses);
1793
- if (client.chain?.contracts?.multicall3 && balanceDefs.length > 1) {
1794
- const multicall3 = client.chain.contracts.multicall3;
1795
- return fetchWithMulticall(client, balanceDefs, multicall3.address);
1147
+ if (client.chain?.contracts?.erc20Aggregator && balanceDefs.length > 1) {
1148
+ const erc20Aggregator = client.chain.contracts.erc20Aggregator;
1149
+ return fetchWithAggregator(client, balanceDefs, erc20Aggregator.address);
1796
1150
  }
1797
- return fetchWithoutMulticall(client, balanceDefs);
1151
+ return fetchWithoutAggregator(client, balanceDefs);
1798
1152
  };
1799
- const fetchWithoutMulticall = async (client, balanceDefs) => {
1153
+ const fetchWithoutAggregator = async (client, balanceDefs) => {
1800
1154
  if (balanceDefs.length === 0) return {
1801
1155
  success: [],
1802
1156
  errors: []
@@ -1806,14 +1160,17 @@ const fetchWithoutMulticall = async (client, balanceDefs) => {
1806
1160
  address
1807
1161
  }) => {
1808
1162
  try {
1809
- const result = await client.getBalance({
1810
- address
1163
+ const result = await client.readContract({
1164
+ abi: erc20Abi,
1165
+ address: token.contractAddress,
1166
+ functionName: "balanceOf",
1167
+ args: [address]
1811
1168
  });
1812
1169
  const balance = {
1813
1170
  address,
1814
1171
  tokenId: token.id,
1815
1172
  value: result.toString(),
1816
- source: MODULE_TYPE$7,
1173
+ source: MODULE_TYPE$6,
1817
1174
  networkId: parseTokenId(token.id).networkId,
1818
1175
  status: "live"
1819
1176
  };
@@ -1837,45 +1194,33 @@ const fetchWithoutMulticall = async (client, balanceDefs) => {
1837
1194
  errors: []
1838
1195
  });
1839
1196
  };
1840
- const fetchWithMulticall = async (client, balanceDefs, multicall3Address) => {
1197
+ const fetchWithAggregator = async (client, balanceDefs, erc20BalancesAggregatorAddress) => {
1841
1198
  if (balanceDefs.length === 0) return {
1842
1199
  success: [],
1843
1200
  errors: []
1844
1201
  };
1845
1202
  try {
1846
- const callResults = await client.multicall({
1847
- contracts: balanceDefs.map(({
1848
- address
1849
- }) => ({
1850
- address: multicall3Address,
1851
- abi: abiMulticall,
1852
- functionName: "getEthBalance",
1853
- args: [address]
1854
- }))
1203
+ const erc20Balances = await client.readContract({
1204
+ abi: erc20BalancesAggregatorAbi,
1205
+ address: erc20BalancesAggregatorAddress,
1206
+ functionName: "balances",
1207
+ args: [balanceDefs.map(b => ({
1208
+ account: b.address,
1209
+ token: b.token.contractAddress
1210
+ }))]
1855
1211
  });
1856
- return callResults.reduce((acc, result, index) => {
1857
- if (result.status === "success") {
1858
- acc.success.push({
1859
- address: balanceDefs[index].address,
1860
- tokenId: balanceDefs[index].token.id,
1861
- value: result.result.toString(),
1862
- source: MODULE_TYPE$7,
1863
- networkId: parseTokenId(balanceDefs[index].token.id).networkId,
1864
- status: "live"
1865
- });
1866
- }
1867
- if (result.status === "failure") {
1868
- acc.errors.push({
1869
- tokenId: balanceDefs[index].token.id,
1870
- address: balanceDefs[index].address,
1871
- error: new BalanceFetchError(`Failed to get balance for token ${balanceDefs[index].token.id} and address ${balanceDefs[index].address} on chain ${client.chain?.id}`, balanceDefs[index].token.id, balanceDefs[index].address, result.error)
1872
- });
1873
- }
1874
- return acc;
1875
- }, {
1876
- success: [],
1212
+ const success = balanceDefs.map((balanceDef, index) => ({
1213
+ address: balanceDef.address,
1214
+ tokenId: balanceDef.token.id,
1215
+ value: erc20Balances[index].toString(),
1216
+ source: MODULE_TYPE$6,
1217
+ networkId: parseTokenId(balanceDef.token.id).networkId,
1218
+ status: "live"
1219
+ }));
1220
+ return {
1221
+ success,
1877
1222
  errors: []
1878
- });
1223
+ };
1879
1224
  } catch (err) {
1880
1225
  const errors = balanceDefs.map(balanceDef => ({
1881
1226
  tokenId: balanceDef.token.id,
@@ -1889,50 +1234,183 @@ const fetchWithMulticall = async (client, balanceDefs, multicall3Address) => {
1889
1234
  }
1890
1235
  };
1891
1236
 
1892
- const fetchTokens$7 = async ({
1893
- networkId,
1894
- tokens
1895
- }) => {
1896
- // assume there is one and only one token in the array
1897
- if (tokens.length !== 1) throw new Error("EVM Native module expects the nativeCurrency to be passed as a single token in the array");
1898
- const token = assign({
1899
- id: evmNativeTokenId(networkId),
1900
- type: MODULE_TYPE$7,
1901
- platform: PLATFORM$7,
1902
- networkId,
1903
- isDefault: true
1904
- }, tokens[0]);
1905
- const parsed = EvmNativeTokenSchema.safeParse(token);
1906
- if (!parsed.success) {
1907
- log.warn("Ignoring token with invalid schema", token);
1908
- return [];
1909
- }
1910
- return [parsed.data];
1911
- };
1912
-
1913
- const getMiniMetadata$7 = () => {
1237
+ const getErc20ContractData = async (client, contractAddress) => {
1238
+ try {
1239
+ const contract = getTypedContract(client, erc20Abi, contractAddress);
1240
+
1241
+ // eslint-disable-next-line no-var
1242
+ var [symbol, decimals, name] = await Promise.all([contract.read.symbol(), contract.read.decimals(), contract.read.name()]);
1243
+ } catch (e) {
1244
+ if (e instanceof ContractFunctionExecutionError) {
1245
+ // try to perform the contract read with bytes32 symbol
1246
+ const contract = getTypedContract(client, erc20Abi_bytes32, contractAddress);
1247
+
1248
+ // eslint-disable-next-line no-var
1249
+ var [bytesSymbol, decimals, nameSymbol] = await Promise.all([contract.read.symbol(), contract.read.decimals(), contract.read.name()]);
1250
+ symbol = hexToString(bytesSymbol).replace(/\0/g, "").trim(); // remove NULL characters
1251
+ name = hexToString(nameSymbol).replace(/\0/g, "").trim(); // remove NULL characters
1252
+ } else throw e;
1253
+ }
1254
+ return {
1255
+ symbol,
1256
+ decimals,
1257
+ name
1258
+ };
1259
+ };
1260
+ const getTypedContract = (client, abi, contractAddress) => getContract({
1261
+ address: contractAddress,
1262
+ abi,
1263
+ client: {
1264
+ public: client
1265
+ }
1266
+ });
1267
+ const getUniswapV2PairContractData = async (client, contractAddress) => {
1268
+ const contract = getTypedContract(client, uniswapV2PairAbi, contractAddress);
1269
+
1270
+ // eslint-disable-next-line no-var
1271
+ var [token0, token1, decimals, name] = await Promise.all([contract.read.token0(), contract.read.token1(), contract.read.decimals(), contract.read.name()]);
1272
+ return {
1273
+ token0,
1274
+ token1,
1275
+ decimals,
1276
+ name
1277
+ };
1278
+ };
1279
+
1280
+ // const TokenCacheSchema = EvmUniswapV2TokenSchema.pick({
1281
+ // id: true,
1282
+ // symbol: true,
1283
+ // decimals: true,
1284
+ // name: true,
1285
+ // tokenAddress0: true,
1286
+ // tokenAddress1: true,
1287
+ // decimals0: true,
1288
+ // decimals1: true,
1289
+ // symbol0: true,
1290
+ // symbol1: true,
1291
+ // })
1292
+
1293
+ const TokenCacheSchema = z.discriminatedUnion("isValid", [z.strictObject({
1294
+ id: EvmUniswapV2TokenSchema.shape.id,
1295
+ isValid: z.literal(true),
1296
+ ...EvmUniswapV2TokenSchema.pick({
1297
+ symbol: true,
1298
+ decimals: true,
1299
+ name: true,
1300
+ tokenAddress0: true,
1301
+ tokenAddress1: true,
1302
+ decimals0: true,
1303
+ decimals1: true,
1304
+ symbol0: true,
1305
+ symbol1: true
1306
+ }).shape
1307
+ }), z.strictObject({
1308
+ id: EvmUniswapV2TokenSchema.shape.id,
1309
+ isValid: z.literal(false)
1310
+ })]);
1311
+ const fetchTokens$6 = async ({
1312
+ networkId,
1313
+ tokens,
1314
+ connector,
1315
+ cache
1316
+ }) => {
1317
+ const result = [];
1318
+ for (const tokenConfig of tokens) {
1319
+ const tokenId = evmUniswapV2TokenId(networkId, tokenConfig.contractAddress);
1320
+ const cached = cache[tokenId] && TokenCacheSchema.safeParse(cache[tokenId]).data;
1321
+ if (!cached) {
1322
+ const client = await connector.getPublicClientForEvmNetwork(networkId);
1323
+ if (!client) {
1324
+ log.warn(`No client found for network ${networkId} while fetching EVM ERC20 tokens`);
1325
+ continue;
1326
+ }
1327
+ try {
1328
+ const {
1329
+ token0,
1330
+ token1,
1331
+ name,
1332
+ decimals
1333
+ } = await getUniswapV2PairContractData(client, tokenConfig.contractAddress);
1334
+ const {
1335
+ symbol: symbol0,
1336
+ decimals: decimals0
1337
+ } = await getErc20ContractData(client, token0);
1338
+ const {
1339
+ symbol: symbol1,
1340
+ decimals: decimals1
1341
+ } = await getErc20ContractData(client, token1);
1342
+ cache[tokenId] = TokenCacheSchema.parse({
1343
+ id: tokenId,
1344
+ symbol: `${symbol0}/${symbol1}`,
1345
+ decimals,
1346
+ name,
1347
+ tokenAddress0: token0,
1348
+ tokenAddress1: token1,
1349
+ decimals0,
1350
+ decimals1,
1351
+ symbol0,
1352
+ symbol1,
1353
+ isValid: true
1354
+ });
1355
+ } catch (err) {
1356
+ const msg = err.shortMessage;
1357
+ if (msg.includes("returned no data") || msg.includes("is out of bounds") || msg.includes("reverted")) {
1358
+ cache[tokenId] = {
1359
+ id: tokenId,
1360
+ isValid: false
1361
+ };
1362
+ } else {
1363
+ log.warn(`Failed to fetch UniswapV2 token data for ${tokenConfig.contractAddress}`, err.shortMessage);
1364
+ }
1365
+ continue;
1366
+ }
1367
+ }
1368
+ const base = {
1369
+ id: tokenId,
1370
+ type: MODULE_TYPE$6,
1371
+ platform: PLATFORM$6,
1372
+ networkId
1373
+ };
1374
+ const cached2 = cache[tokenId] && TokenCacheSchema.safeParse(cache[tokenId]).data;
1375
+ if (cached2?.isValid === false) continue;
1376
+ const token = assign(base, cached2?.isValid ? omit(cached2, ["isValid"]) : {}, tokenConfig);
1377
+ const parsed = EvmUniswapV2TokenSchema.safeParse(token);
1378
+ if (!parsed.success) {
1379
+ log.warn("Ignoring token with invalid schema", token);
1380
+ continue;
1381
+ }
1382
+ result.push(parsed.data);
1383
+ }
1384
+ return result;
1385
+ };
1386
+
1387
+ const getMiniMetadata$6 = () => {
1914
1388
  throw new Error("MiniMetadata is not supported for ethereum tokens");
1915
1389
  };
1916
1390
 
1917
- const getTransferCallData$7 = ({
1391
+ const getTransferCallData$6 = ({
1918
1392
  from,
1919
1393
  to,
1920
1394
  value,
1921
1395
  token
1922
1396
  }) => {
1923
- if (!isTokenOfType(token, MODULE_TYPE$7)) throw new Error(`Token type ${token.type} is not ${MODULE_TYPE$7}.`);
1397
+ if (!isTokenOfType(token, MODULE_TYPE$6)) throw new Error(`Token type ${token.type} is not ${MODULE_TYPE$6}.`);
1924
1398
  if (!isEthereumAddress(from)) throw new Error("Invalid from address");
1925
1399
  if (!isEthereumAddress(to)) throw new Error("Invalid to address");
1400
+ const data = encodeFunctionData({
1401
+ abi: erc20Abi,
1402
+ functionName: "transfer",
1403
+ args: [to, BigInt(value)]
1404
+ });
1926
1405
  return {
1927
1406
  from,
1928
- to,
1929
- value,
1930
- data: "0x"
1407
+ to: token.contractAddress,
1408
+ data
1931
1409
  };
1932
1410
  };
1933
1411
 
1934
- const SUBSCRIPTION_INTERVAL$3 = 6_000;
1935
- const subscribeBalances$7 = ({
1412
+ const SUBSCRIPTION_INTERVAL$2 = 6_000;
1413
+ const subscribeBalances$6 = ({
1936
1414
  networkId,
1937
1415
  tokensWithAddresses,
1938
1416
  connector
@@ -1946,17 +1424,17 @@ const subscribeBalances$7 = ({
1946
1424
  const poll = async () => {
1947
1425
  try {
1948
1426
  if (abortController.signal.aborted) return;
1949
- const balances = await fetchBalances$7({
1427
+ const balances = await fetchBalances$6({
1950
1428
  networkId,
1951
1429
  tokensWithAddresses: tokensWithAddresses,
1952
1430
  connector
1953
1431
  });
1954
1432
  if (abortController.signal.aborted) return;
1955
1433
  subscriber.next(balances);
1956
- setTimeout(poll, SUBSCRIPTION_INTERVAL$3);
1434
+ setTimeout(poll, SUBSCRIPTION_INTERVAL$2);
1957
1435
  } catch (error) {
1958
1436
  log.error("Error", {
1959
- module: MODULE_TYPE$7,
1437
+ module: MODULE_TYPE$6,
1960
1438
  networkId,
1961
1439
  addressesByToken: tokensWithAddresses,
1962
1440
  error
@@ -1964,715 +1442,1142 @@ const subscribeBalances$7 = ({
1964
1442
  subscriber.error(error);
1965
1443
  }
1966
1444
  };
1967
- poll();
1968
- return () => {
1969
- abortController.abort();
1445
+ poll();
1446
+ return () => {
1447
+ abortController.abort();
1448
+ };
1449
+ }).pipe(distinctUntilChanged(isEqual));
1450
+ };
1451
+
1452
+ const EvmUniswapV2BalanceModule = {
1453
+ type: MODULE_TYPE$6,
1454
+ platform: PLATFORM$6,
1455
+ getMiniMetadata: getMiniMetadata$6,
1456
+ fetchTokens: fetchTokens$6,
1457
+ fetchBalances: fetchBalances$6,
1458
+ subscribeBalances: subscribeBalances$6,
1459
+ getTransferCallData: getTransferCallData$6
1460
+ };
1461
+
1462
+ // to be used by chaindata too
1463
+ const EvmUniswapV2TokenConfigSchema = z.strictObject({
1464
+ contractAddress: EvmUniswapV2TokenSchema.shape.contractAddress,
1465
+ ...TokenConfigBaseSchema.shape
1466
+ });
1467
+
1468
+ const MODULE_TYPE$5 = SubAssetsTokenSchema.shape.type.value;
1469
+ const PLATFORM$5 = SubAssetsTokenSchema.shape.platform.value;
1470
+
1471
+ const fetchRpcQueryPack = async (connector, networkId, queries) => {
1472
+ const allStateKeys = queries.flatMap(({
1473
+ stateKeys
1474
+ }) => stateKeys).filter(isNotNil);
1475
+
1476
+ // doing a query without keys would throw an error => return early
1477
+ if (!allStateKeys.length) return queries.map(({
1478
+ stateKeys,
1479
+ decodeResult
1480
+ }) => decodeResult(stateKeys.map(() => null)));
1481
+ const [result] = await connector.send(networkId, "state_queryStorageAt", [allStateKeys]);
1482
+ return decodeRpcQueryPack(queries, result);
1483
+ };
1484
+ const getRpcQueryPack$ = (connector, networkId, queries, timeout = false) => {
1485
+ const allStateKeys = queries.flatMap(({
1486
+ stateKeys
1487
+ }) => stateKeys).filter(isNotNil);
1488
+
1489
+ // doing a query without keys would throw an error => return early
1490
+ if (!allStateKeys.length) return of(queries.map(({
1491
+ stateKeys,
1492
+ decodeResult
1493
+ }) => decodeResult(stateKeys.map(() => null))));
1494
+ return new Observable(subscriber => {
1495
+ const promUnsub = connector.subscribe(networkId, "state_subscribeStorage", "state_storage", [allStateKeys], (error, result) => {
1496
+ if (error) subscriber.error(error);else subscriber.next(decodeRpcQueryPack(queries, result));
1497
+ }, timeout);
1498
+ return () => {
1499
+ promUnsub.then(unsub => unsub("state_unsubscribeStorage"));
1500
+ };
1501
+ });
1502
+ };
1503
+ const decodeRpcQueryPack = (queries, result) => {
1504
+ return queries.reduce((acc, {
1505
+ stateKeys,
1506
+ decodeResult
1507
+ }) => {
1508
+ const changes = stateKeys.map(stateKey => {
1509
+ if (!stateKey) return null;
1510
+ const change = result.changes.find(([key]) => key === stateKey);
1511
+ if (!change) return null;
1512
+ return change[1];
1513
+ });
1514
+ acc.push(decodeResult(changes));
1515
+ return acc;
1516
+ }, []);
1517
+ };
1518
+
1519
+ const fetchRuntimeCallResult = async (connector, networkId, metadataRpc, apiName, method, args) => {
1520
+ const {
1521
+ builder
1522
+ } = parseMetadataRpc(metadataRpc);
1523
+ const call = builder.buildRuntimeCall(apiName, method);
1524
+ const hex = await connector.send(networkId, "state_call", [`${apiName}_${method}`, toHex(call.args.enc(args))]);
1525
+ return call.value.dec(hex);
1526
+ };
1527
+
1528
+ const getConstantValue = (metadataRpc, pallet, constant) => {
1529
+ const {
1530
+ unifiedMetadata,
1531
+ builder
1532
+ } = parseMetadataRpc(metadataRpc);
1533
+ const codec = builder.buildConstant(pallet, constant);
1534
+ const encodedValue = unifiedMetadata.pallets.find(({
1535
+ name
1536
+ }) => name === pallet)?.constants.find(({
1537
+ name
1538
+ }) => name === constant)?.value;
1539
+ if (!encodedValue) throw new Error(`Constant ${pallet}.${constant} not found`);
1540
+ return codec.dec(encodedValue);
1541
+ };
1542
+
1543
+ const tryGetConstantValue = (metadataRpc, pallet, constant) => {
1544
+ const {
1545
+ unifiedMetadata,
1546
+ builder
1547
+ } = parseMetadataRpc(metadataRpc);
1548
+ const encodedValue = unifiedMetadata.pallets.find(({
1549
+ name
1550
+ }) => name === pallet)?.constants.find(({
1551
+ name
1552
+ }) => name === constant)?.value;
1553
+ if (!encodedValue) return null;
1554
+ const codec = builder.buildConstant(pallet, constant);
1555
+ return codec.dec(encodedValue);
1556
+ };
1557
+
1558
+ const hasStorageItem = (metadata, palletName, itemName) => {
1559
+ const pallet = metadata.pallets.find(p => p.name === palletName);
1560
+ if (!pallet || !pallet.storage) return false;
1561
+ return pallet.storage.items.some(item => item.name === itemName);
1562
+ };
1563
+ const hasStorageItems = (metadata, palletName, itemNames) => {
1564
+ const pallet = metadata.pallets.find(p => p.name === palletName);
1565
+ if (!pallet || !pallet.storage) return false;
1566
+ return itemNames.every(itemName => pallet.storage?.items.some(item => item.name === itemName));
1567
+ };
1568
+ const hasRuntimeApi = (metadata, apiName, method) => {
1569
+ const api = metadata.apis.find(api => api.name === apiName);
1570
+ if (!api || !api.methods) return false;
1571
+ return api.methods.some(m => m.name === method);
1572
+ };
1573
+
1574
+ const buildNetworkStorageCoders = (chainId, miniMetadata, coders) => {
1575
+ if (!miniMetadata.data) return null;
1576
+ const metadata = unifyMetadata(decAnyMetadata(miniMetadata.data));
1577
+ try {
1578
+ const scaleBuilder = getDynamicBuilder(getLookupFn(metadata));
1579
+ const builtCoders = Object.fromEntries(Object.entries(coders).flatMap(([key, moduleMethodOrFn]) => {
1580
+ const [module, method] = typeof moduleMethodOrFn === "function" ? moduleMethodOrFn({
1581
+ chainId
1582
+ }) : moduleMethodOrFn;
1583
+ try {
1584
+ return [[key, scaleBuilder.buildStorage(module, method)]];
1585
+ } catch (cause) {
1586
+ log.trace(`Failed to build SCALE coder for chain ${chainId} (${module}::${method})`, cause);
1587
+ return [];
1588
+ }
1589
+ }));
1590
+ return builtCoders;
1591
+ } catch (cause) {
1592
+ log.error(`Failed to build SCALE coders for chain ${chainId} (${JSON.stringify(coders)})`, cause);
1593
+ }
1594
+ return null;
1595
+ };
1596
+
1597
+ const buildQueries$2 = (networkId, balanceDefs, miniMetadata) => {
1598
+ const networkStorageCoders = buildNetworkStorageCoders(networkId, miniMetadata, {
1599
+ storage: ["Assets", "Account"]
1600
+ });
1601
+ return balanceDefs.map(({
1602
+ token,
1603
+ address
1604
+ }) => {
1605
+ const scaleCoder = networkStorageCoders?.storage;
1606
+ const stateKey = tryEncode(scaleCoder, Number(token.assetId), address) ??
1607
+ // Asset Hub
1608
+ tryEncode(scaleCoder, BigInt(token.assetId), address); // Astar
1609
+
1610
+ if (!stateKey) {
1611
+ log.warn(`Invalid assetId / address in ${networkId} storage query ${token.assetId} / ${address}`);
1612
+ return null;
1613
+ }
1614
+ const decodeResult = changes => {
1615
+ /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
1616
+
1617
+ const decoded = decodeScale(scaleCoder, changes[0], `Failed to decode substrate-assets balance on chain ${networkId}`) ?? {
1618
+ balance: 0n,
1619
+ status: {
1620
+ type: "Liquid"
1621
+ }};
1622
+ const isFrozen = decoded?.status?.type === "Frozen";
1623
+ const amount = (decoded?.balance ?? 0n).toString();
1624
+
1625
+ // due to the following balance calculations, which are made in the `Balance` type:
1626
+ //
1627
+ // total balance = (free balance) + (reserved balance)
1628
+ // transferable balance = (free balance) - (frozen balance)
1629
+ //
1630
+ // when `isFrozen` is true we need to set **both** the `free` and `frozen` amounts
1631
+ // of this balance to the value we received from the RPC.
1632
+ //
1633
+ // if we only set the `frozen` amount, then the `total` calculation will be incorrect!
1634
+ const free = amount;
1635
+ const frozen = token.isFrozen || isFrozen ? amount : "0";
1636
+
1637
+ // include balance values even if zero, so that newly-zero values overwrite old values
1638
+ const balanceValues = [{
1639
+ type: "free",
1640
+ label: "free",
1641
+ amount: free.toString()
1642
+ }, {
1643
+ type: "locked",
1644
+ label: "frozen",
1645
+ amount: frozen.toString()
1646
+ }];
1647
+ const balance = {
1648
+ source: "substrate-assets",
1649
+ status: "live",
1650
+ address,
1651
+ networkId,
1652
+ tokenId: token.id,
1653
+ values: balanceValues
1654
+ };
1655
+ return balance;
1656
+ };
1657
+ return {
1658
+ stateKeys: [stateKey],
1659
+ decodeResult
1970
1660
  };
1971
- }).pipe(distinctUntilChanged(isEqual));
1661
+ }).filter(isNotNil);
1972
1662
  };
1973
-
1974
- const EvmNativeBalanceModule = {
1975
- type: MODULE_TYPE$7,
1976
- platform: PLATFORM$7,
1977
- getMiniMetadata: getMiniMetadata$7,
1978
- fetchTokens: fetchTokens$7,
1979
- fetchBalances: fetchBalances$7,
1980
- subscribeBalances: subscribeBalances$7,
1981
- getTransferCallData: getTransferCallData$7
1663
+ const tryEncode = (scaleCoder, ...args) => {
1664
+ try {
1665
+ return scaleCoder?.keys?.enc?.(...args);
1666
+ } catch {
1667
+ return null;
1668
+ }
1982
1669
  };
1983
1670
 
1984
- // to be used by chaindata too
1985
- const EvmNativeTokenConfigSchema = z.strictObject({
1986
- ...TokenConfigBaseSchema.shape
1987
- });
1988
-
1989
- const MODULE_TYPE$6 = EvmUniswapV2TokenSchema.shape.type.value;
1990
- const PLATFORM$6 = EvmUniswapV2TokenSchema.shape.platform.value;
1991
-
1992
- const fetchBalances$6 = async ({
1671
+ const fetchBalances$5 = async ({
1993
1672
  networkId,
1994
1673
  tokensWithAddresses,
1995
- connector
1674
+ connector,
1675
+ miniMetadata
1996
1676
  }) => {
1997
1677
  if (!tokensWithAddresses.length) return {
1998
1678
  success: [],
1999
1679
  errors: []
2000
1680
  };
2001
- const client = await connector.getPublicClientForEvmNetwork(networkId);
2002
- if (!client) throw new Error(`Could not get rpc provider for evm network ${networkId}`);
2003
- for (const [token, addresses] of tokensWithAddresses) {
2004
- if (token.type !== MODULE_TYPE$6 || token.networkId !== networkId) throw new Error(`Invalid token type or networkId for EVM ERC20 balance module: ${token.type} on ${token.networkId}`);
2005
- for (const address of addresses) if (!isEthereumAddress(address)) throw new Error(`Invalid ethereum address for EVM ERC20 balance module: ${address} for token ${token.id}`);
2006
- }
2007
1681
  const balanceDefs = getBalanceDefs(tokensWithAddresses);
2008
- if (client.chain?.contracts?.erc20Aggregator && balanceDefs.length > 1) {
2009
- const erc20Aggregator = client.chain.contracts.erc20Aggregator;
2010
- return fetchWithAggregator(client, balanceDefs, erc20Aggregator.address);
1682
+ if (!miniMetadata?.data) {
1683
+ log.warn(`MiniMetadata is required for fetching ${MODULE_TYPE$5} balances on ${networkId}.`);
1684
+ return {
1685
+ success: [],
1686
+ errors: balanceDefs.map(def => ({
1687
+ tokenId: def.token.id,
1688
+ address: def.address,
1689
+ error: new Error("Minimetadata is required for fetching balances")
1690
+ }))
1691
+ };
2011
1692
  }
2012
- return fetchWithoutAggregator(client, balanceDefs);
2013
- };
2014
- const fetchWithoutAggregator = async (client, balanceDefs) => {
2015
- if (balanceDefs.length === 0) return {
2016
- success: [],
2017
- errors: []
2018
- };
2019
- const results = await Promise.allSettled(balanceDefs.map(async ({
2020
- token,
2021
- address
2022
- }) => {
2023
- try {
2024
- const result = await client.readContract({
2025
- abi: erc20Abi,
2026
- address: token.contractAddress,
2027
- functionName: "balanceOf",
2028
- args: [address]
2029
- });
2030
- const balance = {
2031
- address,
2032
- tokenId: token.id,
2033
- value: result.toString(),
2034
- source: MODULE_TYPE$6,
2035
- networkId: parseTokenId(token.id).networkId,
2036
- status: "live"
2037
- };
2038
- return balance;
2039
- } catch (err) {
2040
- throw new BalanceFetchError(`Failed to get balance for token ${token.id} and address ${address} on chain ${client.chain?.id}`, token.id, address, err);
2041
- }
2042
- }));
2043
- return results.reduce((acc, result) => {
2044
- if (result.status === "fulfilled") acc.success.push(result.value);else {
2045
- const error = result.reason;
2046
- acc.errors.push({
2047
- tokenId: error.tokenId,
2048
- address: error.address,
2049
- error
2050
- });
2051
- }
1693
+ if (miniMetadata.source !== MODULE_TYPE$5) {
1694
+ log.warn(`Ignoring miniMetadata with source ${miniMetadata.source} in ${MODULE_TYPE$5}.`);
1695
+ return {
1696
+ success: [],
1697
+ errors: balanceDefs.map(def => ({
1698
+ tokenId: def.token.id,
1699
+ address: def.address,
1700
+ error: new Error(`Invalid request: miniMetadata source is not ${MODULE_TYPE$5}`)
1701
+ }))
1702
+ };
1703
+ }
1704
+ if (miniMetadata.chainId !== networkId) {
1705
+ log.warn(`Ignoring miniMetadata with chainId ${miniMetadata.chainId} in ${MODULE_TYPE$5}. Expected chainId is ${networkId}`);
1706
+ return {
1707
+ success: [],
1708
+ errors: balanceDefs.map(def => ({
1709
+ tokenId: def.token.id,
1710
+ address: def.address,
1711
+ error: new Error(`Invalid request: Expected chainId is ${networkId}`)
1712
+ }))
1713
+ };
1714
+ }
1715
+ const queries = buildQueries$2(networkId, balanceDefs, miniMetadata);
1716
+ const balances = await fetchRpcQueryPack(connector, networkId, queries);
1717
+ return balanceDefs.reduce((acc, def) => {
1718
+ const balance = balances.find(b => b?.address === def.address && b?.tokenId === def.token.id);
1719
+ if (balance) acc.success.push(balance);
1720
+ //if no entry consider empty balance
1721
+ else acc.success.push({
1722
+ address: def.address,
1723
+ networkId,
1724
+ tokenId: def.token.id,
1725
+ source: MODULE_TYPE$5,
1726
+ status: "live",
1727
+ values: [{
1728
+ type: "free",
1729
+ label: "free",
1730
+ amount: "0"
1731
+ }, {
1732
+ type: "locked",
1733
+ label: "frozen",
1734
+ amount: "0"
1735
+ }]
1736
+ });
2052
1737
  return acc;
2053
1738
  }, {
2054
1739
  success: [],
2055
1740
  errors: []
2056
1741
  });
2057
1742
  };
2058
- const fetchWithAggregator = async (client, balanceDefs, erc20BalancesAggregatorAddress) => {
2059
- if (balanceDefs.length === 0) return {
2060
- success: [],
2061
- errors: []
2062
- };
2063
- try {
2064
- const erc20Balances = await client.readContract({
2065
- abi: erc20BalancesAggregatorAbi,
2066
- address: erc20BalancesAggregatorAddress,
2067
- functionName: "balances",
2068
- args: [balanceDefs.map(b => ({
2069
- account: b.address,
2070
- token: b.token.contractAddress
2071
- }))]
2072
- });
2073
- const success = balanceDefs.map((balanceDef, index) => ({
2074
- address: balanceDef.address,
2075
- tokenId: balanceDef.token.id,
2076
- value: erc20Balances[index].toString(),
2077
- source: MODULE_TYPE$6,
2078
- networkId: parseTokenId(balanceDef.token.id).networkId,
2079
- status: "live"
2080
- }));
1743
+
1744
+ const fetchTokens$5 = async ({
1745
+ networkId,
1746
+ tokens,
1747
+ connector,
1748
+ miniMetadata
1749
+ }) => {
1750
+ const anyMiniMetadata = miniMetadata;
1751
+ if (!anyMiniMetadata?.data) return [];
1752
+ const {
1753
+ builder
1754
+ } = parseMetadataRpc(anyMiniMetadata.data);
1755
+ const assetCodec = builder.buildStorage("Assets", "Asset");
1756
+ const metadataCodec = builder.buildStorage("Assets", "Metadata");
1757
+ const [allAssetStorageKeys, allMetadataStorageKeys] = await Promise.all([connector.send(networkId, "state_getKeys", [getStorageKeyPrefix("Assets", "Asset")]), connector.send(networkId, "state_getKeys", [getStorageKeyPrefix("Assets", "Metadata")])]);
1758
+ const [assetStorageResults, metadataStorageResults] = await Promise.all([connector.send(networkId, "state_queryStorageAt", [allAssetStorageKeys]), connector.send(networkId, "state_queryStorageAt", [allMetadataStorageKeys])]);
1759
+ const assetStorageEntries = assetStorageResults[0].changes;
1760
+ const metadataStorageEntries = metadataStorageResults[0].changes;
1761
+ const assetByAssetId = keyBy(assetStorageEntries.map(([key, value]) => {
1762
+ const [assetId] = assetCodec.keys.dec(key);
1763
+ const asset = assetCodec.value.dec(value);
2081
1764
  return {
2082
- success,
2083
- errors: []
1765
+ assetId,
1766
+ existentialDeposit: asset.min_balance,
1767
+ isSufficient: asset.is_sufficient
2084
1768
  };
2085
- } catch (err) {
2086
- const errors = balanceDefs.map(balanceDef => ({
2087
- tokenId: balanceDef.token.id,
2088
- address: balanceDef.address,
2089
- error: new BalanceFetchNetworkError(`Failed to get balances for evm-erc20 tokens on chain ${client.chain?.id}`, String(client.chain?.id), err)
2090
- }));
1769
+ }), a => a.assetId);
1770
+ const metadataByAssetId = keyBy(metadataStorageEntries.map(([key, value]) => {
1771
+ const [assetId] = metadataCodec.keys.dec(key);
1772
+ const metadata = metadataCodec.value.dec(value);
2091
1773
  return {
2092
- success: [],
2093
- errors
1774
+ assetId,
1775
+ decimals: metadata.decimals,
1776
+ isFrozen: metadata.is_frozen,
1777
+ name: metadata.name?.asText(),
1778
+ symbol: metadata.symbol?.asText()
2094
1779
  };
2095
- }
1780
+ }), a => a.assetId);
1781
+ const allTokens = keys(assetByAssetId).map(assetId => assign({}, assetByAssetId[assetId], metadataByAssetId[assetId] ?? undefined));
1782
+ const configTokenByAssetId = keyBy(tokens, t => t.assetId);
1783
+ return allTokens.map(asset => ({
1784
+ id: subAssetTokenId(networkId, String(asset.assetId)),
1785
+ type: MODULE_TYPE$5,
1786
+ platform: "polkadot",
1787
+ networkId,
1788
+ assetId: String(asset.assetId),
1789
+ isSufficient: asset.isSufficient,
1790
+ isFrozen: asset.isFrozen,
1791
+ name: asset.name,
1792
+ symbol: asset.symbol,
1793
+ decimals: asset.decimals ?? 0,
1794
+ existentialDeposit: String(asset.existentialDeposit),
1795
+ isDefault: true
1796
+ }))
1797
+ // keep all tokens listed in the config + all tokens marked as sufficient
1798
+ .filter(token => {
1799
+ const configToken = configTokenByAssetId[token.assetId];
1800
+ return configToken || token.isSufficient;
1801
+ })
1802
+ // apply config overrides
1803
+ .map(token => {
1804
+ const configToken = configTokenByAssetId[token.assetId];
1805
+ return configToken ? assign({}, token, configToken) : token;
1806
+ })
1807
+ // validate results
1808
+ .filter(t => {
1809
+ const parsed = SubAssetsTokenSchema.safeParse(t);
1810
+ if (!parsed.success) log.warn(`Ignoring invalid token ${MODULE_TYPE$5}`, t);
1811
+ return parsed.success;
1812
+ });
2096
1813
  };
2097
1814
 
2098
- const getErc20ContractData = async (client, contractAddress) => {
2099
- try {
2100
- const contract = getTypedContract(client, erc20Abi, contractAddress);
1815
+ function excludeFromTransferableAmount(locks) {
1816
+ if (typeof locks === "string") return BigInt(locks);
1817
+ if (!Array.isArray(locks)) locks = [locks];
1818
+ return locks.filter(lock => lock.includeInTransferable !== true).map(lock => lock.amount.planck).reduce((max, lock) => BigMath.max(max, lock), 0n);
1819
+ }
1820
+ function excludeFromFeePayableLocks(locks) {
1821
+ if (typeof locks === "string") return [];
1822
+ if (!Array.isArray(locks)) locks = [locks];
1823
+ return locks.filter(lock => lock.excludeFromFeePayable);
1824
+ }
1825
+ function includeInTotalExtraAmount(extra) {
1826
+ if (!extra) return 0n;
1827
+ if (!Array.isArray(extra)) extra = [extra];
1828
+ return extra.filter(extra => extra.includeInTotal).map(extra => extra.amount.planck).reduce((a, b) => a + b, 0n);
1829
+ }
2101
1830
 
2102
- // eslint-disable-next-line no-var
2103
- var [symbol, decimals, name] = await Promise.all([contract.read.symbol(), contract.read.decimals(), contract.read.name()]);
2104
- } catch (e) {
2105
- if (e instanceof ContractFunctionExecutionError) {
2106
- // try to perform the contract read with bytes32 symbol
2107
- const contract = getTypedContract(client, erc20Abi_bytes32, contractAddress);
1831
+ /**
1832
+ * Have the importing library define its Token and BalanceJson enums (as a sum type of all plugins) and pass them into some
1833
+ * internal global typescript context, which is then picked up on by this module.
1834
+ */
2108
1835
 
2109
- // eslint-disable-next-line no-var
2110
- var [bytesSymbol, decimals, nameSymbol] = await Promise.all([contract.read.symbol(), contract.read.decimals(), contract.read.name()]);
2111
- symbol = hexToString(bytesSymbol).replace(/\0/g, "").trim(); // remove NULL characters
2112
- name = hexToString(nameSymbol).replace(/\0/g, "").trim(); // remove NULL characters
2113
- } else throw e;
2114
- }
2115
- return {
2116
- symbol,
2117
- decimals,
2118
- name
2119
- };
2120
- };
2121
- const getTypedContract = (client, abi, contractAddress) => getContract({
2122
- address: contractAddress,
2123
- abi,
2124
- client: {
2125
- public: client
1836
+ /** A utility type used to extract the underlying `BalanceType` of a specific source from a generalised `BalanceJson` */
1837
+
1838
+ /** TODO: Remove this in favour of a frontend-friendly `ChaindataProvider` */
1839
+
1840
+ /**
1841
+ * A collection of balances.
1842
+ */
1843
+ class Balances {
1844
+ //
1845
+ // Properties
1846
+ //
1847
+
1848
+ #balances = [];
1849
+
1850
+ //
1851
+ // Methods
1852
+ //
1853
+
1854
+ constructor(balances, hydrate) {
1855
+ // handle Balances (convert to Balance[])
1856
+ if (balances instanceof Balances) return new Balances(balances.each, hydrate);
1857
+
1858
+ // handle Balance (convert to Balance[])
1859
+ if (balances instanceof Balance) return new Balances([balances], hydrate);
1860
+
1861
+ // handle BalanceJsonList (the only remaining non-array type of balances) (convert to BalanceJson[])
1862
+ if (!Array.isArray(balances)) return new Balances(Object.values(balances), hydrate);
1863
+
1864
+ // handle no balances
1865
+ if (balances.length === 0) return this;
1866
+
1867
+ // handle BalanceJson[]
1868
+ if (!isArrayOf(balances, Balance)) return new Balances(balances.map(storage => new Balance(storage)), hydrate);
1869
+
1870
+ // handle Balance[]
1871
+ this.#balances = balances;
1872
+ if (hydrate !== undefined) this.hydrate(hydrate);
2126
1873
  }
2127
- });
2128
- const getUniswapV2PairContractData = async (client, contractAddress) => {
2129
- const contract = getTypedContract(client, uniswapV2PairAbi, contractAddress);
2130
1874
 
2131
- // eslint-disable-next-line no-var
2132
- var [token0, token1, decimals, name] = await Promise.all([contract.read.token0(), contract.read.token1(), contract.read.decimals(), contract.read.name()]);
2133
- return {
2134
- token0,
2135
- token1,
2136
- decimals,
2137
- name
1875
+ /**
1876
+ * Calling toJSON on a collection of balances will return the underlying BalanceJsonList.
1877
+ */
1878
+ toJSON = () => Object.fromEntries(this.#balances.map(balance => {
1879
+ try {
1880
+ return [balance.id, balance.toJSON()];
1881
+ } catch (error) {
1882
+ log.error("Failed to convert balance to JSON", error, {
1883
+ id: balance.id,
1884
+ balance
1885
+ });
1886
+ return null;
1887
+ }
1888
+ }).filter(Array.isArray));
1889
+
1890
+ /**
1891
+ * Allows the collection to be iterated over.
1892
+ *
1893
+ * @example
1894
+ * [...balances].forEach(balance => { // do something // })
1895
+ *
1896
+ * @example
1897
+ * for (const balance of balances) {
1898
+ * // do something
1899
+ * }
1900
+ */
1901
+ [Symbol.iterator] = () =>
1902
+ // Create an array of the balances in this collection and return the result of its iterator.
1903
+ this.#balances[Symbol.iterator]();
1904
+
1905
+ /**
1906
+ * Hydrates all balances in this collection.
1907
+ *
1908
+ * @param sources - The sources to hydrate from.
1909
+ */
1910
+ hydrate = sources => {
1911
+ this.#balances.map(balance => balance.hydrate(sources));
2138
1912
  };
2139
- };
2140
1913
 
2141
- // const TokenCacheSchema = EvmUniswapV2TokenSchema.pick({
2142
- // id: true,
2143
- // symbol: true,
2144
- // decimals: true,
2145
- // name: true,
2146
- // tokenAddress0: true,
2147
- // tokenAddress1: true,
2148
- // decimals0: true,
2149
- // decimals1: true,
2150
- // symbol0: true,
2151
- // symbol1: true,
2152
- // })
1914
+ /**
1915
+ * Retrieve a balance from this collection by id.
1916
+ *
1917
+ * @param id - The id of the balance to fetch.
1918
+ * @returns The balance if one exists, or none.
1919
+ */
1920
+ get = id => this.#balances.find(balance => balance.id === id) ?? null;
2153
1921
 
2154
- const TokenCacheSchema = z.discriminatedUnion("isValid", [z.strictObject({
2155
- id: EvmUniswapV2TokenSchema.shape.id,
2156
- isValid: z.literal(true),
2157
- ...EvmUniswapV2TokenSchema.pick({
2158
- symbol: true,
2159
- decimals: true,
2160
- name: true,
2161
- tokenAddress0: true,
2162
- tokenAddress1: true,
2163
- decimals0: true,
2164
- decimals1: true,
2165
- symbol0: true,
2166
- symbol1: true
2167
- }).shape
2168
- }), z.strictObject({
2169
- id: EvmUniswapV2TokenSchema.shape.id,
2170
- isValid: z.literal(false)
2171
- })]);
2172
- const fetchTokens$6 = async ({
2173
- networkId,
2174
- tokens,
2175
- connector,
2176
- cache
2177
- }) => {
2178
- const result = [];
2179
- for (const tokenConfig of tokens) {
2180
- const tokenId = evmUniswapV2TokenId(networkId, tokenConfig.contractAddress);
2181
- const cached = cache[tokenId] && TokenCacheSchema.safeParse(cache[tokenId]).data;
2182
- if (!cached) {
2183
- const client = await connector.getPublicClientForEvmNetwork(networkId);
2184
- if (!client) {
2185
- log.warn(`No client found for network ${networkId} while fetching EVM ERC20 tokens`);
2186
- continue;
2187
- }
2188
- try {
2189
- const {
2190
- token0,
2191
- token1,
2192
- name,
2193
- decimals
2194
- } = await getUniswapV2PairContractData(client, tokenConfig.contractAddress);
2195
- const {
2196
- symbol: symbol0,
2197
- decimals: decimals0
2198
- } = await getErc20ContractData(client, token0);
2199
- const {
2200
- symbol: symbol1,
2201
- decimals: decimals1
2202
- } = await getErc20ContractData(client, token1);
2203
- cache[tokenId] = TokenCacheSchema.parse({
2204
- id: tokenId,
2205
- symbol: `${symbol0}/${symbol1}`,
2206
- decimals,
2207
- name,
2208
- tokenAddress0: token0,
2209
- tokenAddress1: token1,
2210
- decimals0,
2211
- decimals1,
2212
- symbol0,
2213
- symbol1,
2214
- isValid: true
2215
- });
2216
- } catch (err) {
2217
- const msg = err.shortMessage;
2218
- if (msg.includes("returned no data") || msg.includes("is out of bounds") || msg.includes("reverted")) {
2219
- cache[tokenId] = {
2220
- id: tokenId,
2221
- isValid: false
2222
- };
2223
- } else {
2224
- log.warn(`Failed to fetch UniswapV2 token data for ${tokenConfig.contractAddress}`, err.shortMessage);
2225
- }
2226
- continue;
2227
- }
2228
- }
2229
- const base = {
2230
- id: tokenId,
2231
- type: MODULE_TYPE$6,
2232
- platform: PLATFORM$6,
2233
- networkId
2234
- };
2235
- const cached2 = cache[tokenId] && TokenCacheSchema.safeParse(cache[tokenId]).data;
2236
- if (cached2?.isValid === false) continue;
2237
- const token = assign(base, cached2?.isValid ? omit(cached2, ["isValid"]) : {}, tokenConfig);
2238
- const parsed = EvmUniswapV2TokenSchema.safeParse(token);
2239
- if (!parsed.success) {
2240
- log.warn("Ignoring token with invalid schema", token);
2241
- continue;
2242
- }
2243
- result.push(parsed.data);
1922
+ /**
1923
+ * Retrieve balances from this collection by search query.
1924
+ *
1925
+ * @param query - The search query.
1926
+ * @returns All balances which match the query.
1927
+ */
1928
+ find = query => {
1929
+ // construct filter
1930
+ const queryArray = Array.isArray(query) ? query : [query];
1931
+ const orQueries = queryArray.map(query => typeof query === "function" ? query : Object.entries(query));
1932
+
1933
+ // filter balances
1934
+ const filter = balance => orQueries.some(query => typeof query === "function" ? query(balance) : query.every(([key, value]) => balance[key] === value));
1935
+
1936
+ // return filter matches
1937
+ return new Balances([...this].filter(filter));
1938
+ };
1939
+
1940
+ /**
1941
+ * Filters this collection to exclude token balances where the token has a `mirrorOf` field
1942
+ * and another balance exists in this collection for the token specified by the `mirrorOf` field.
1943
+ */
1944
+ filterMirrorTokens = () => new Balances([...this].filter(filterMirrorTokens));
1945
+
1946
+ /**
1947
+ * Filters this collection to only include balances which are not zero AND have a fiat conversion rate.
1948
+ */
1949
+ filterNonZeroFiat = (type, currency) => {
1950
+ const filter = balance => (balance[type].fiat(currency) ?? 0) > 0;
1951
+ return this.find(filter);
1952
+ };
1953
+
1954
+ /**
1955
+ * Add some balances to this collection.
1956
+ * Added balances take priority over existing balances.
1957
+ * The aggregation of the two collections is returned.
1958
+ * The original collection is not mutated.
1959
+ *
1960
+ * @param balances - Either a balance or collection of balances to add.
1961
+ * @returns The new collection of balances.
1962
+ */
1963
+ add = balances => {
1964
+ // handle single balance
1965
+ if (balances instanceof Balance) return this.add(new Balances(balances));
1966
+
1967
+ // merge balances
1968
+ const mergedBalances = Object.fromEntries(this.#balances.map(balance => [balance.id, balance]));
1969
+ balances.each.forEach(balance => mergedBalances[balance.id] = balance);
1970
+
1971
+ // return new balances
1972
+ return new Balances(Object.values(mergedBalances));
1973
+ };
1974
+
1975
+ /**
1976
+ * Remove balances from this collection by id.
1977
+ * A new collection without these balances is returned.
1978
+ * The original collection is not mutated.
1979
+ *
1980
+ * @param ids - The id(s) of the balances to remove.
1981
+ * @returns The new collection of balances.
1982
+ */
1983
+ remove = ids => {
1984
+ // handle single id
1985
+ if (!Array.isArray(ids)) return this.remove([ids]);
1986
+
1987
+ // merge and return new balances
1988
+ return new Balances(this.#balances.filter(balance => !ids.includes(balance.id)));
1989
+ };
1990
+
1991
+ // TODO: Add some more useful aggregator methods
1992
+
1993
+ get each() {
1994
+ return [...this];
2244
1995
  }
2245
- return result;
2246
- };
2247
1996
 
2248
- const getMiniMetadata$6 = () => {
2249
- throw new Error("MiniMetadata is not supported for ethereum tokens");
1997
+ /** @deprecated use each instead */
1998
+ get sorted() {
1999
+ return this.each;
2000
+ }
2001
+
2002
+ /**
2003
+ * Get the number of balances in this collection.
2004
+ *
2005
+ * @returns The number of balances in this collection.
2006
+ */
2007
+ get count() {
2008
+ return [...this].length;
2009
+ }
2010
+
2011
+ /**
2012
+ * Get the summed value of balances in this collection.
2013
+ * TODO: Sum up token amounts AND fiat amounts
2014
+ *
2015
+ * @example
2016
+ * // Get the sum of all transferable balances in usd.
2017
+ * balances.sum.fiat('usd').transferable
2018
+ */
2019
+ get sum() {
2020
+ return new SumBalancesFormatter(this);
2021
+ }
2022
+ }
2023
+ const getBalanceId = balance => {
2024
+ const {
2025
+ address,
2026
+ tokenId
2027
+ } = balance;
2028
+ return [address, tokenId].join("::");
2250
2029
  };
2251
2030
 
2252
- const getTransferCallData$6 = ({
2253
- from,
2254
- to,
2255
- value,
2256
- token
2257
- }) => {
2258
- if (!isTokenOfType(token, MODULE_TYPE$6)) throw new Error(`Token type ${token.type} is not ${MODULE_TYPE$6}.`);
2259
- if (!isEthereumAddress(from)) throw new Error("Invalid from address");
2260
- if (!isEthereumAddress(to)) throw new Error("Invalid to address");
2261
- const data = encodeFunctionData({
2262
- abi: erc20Abi,
2263
- functionName: "transfer",
2264
- args: [to, BigInt(value)]
2265
- });
2266
- return {
2267
- from,
2268
- to: token.contractAddress,
2269
- data
2031
+ /**
2032
+ * An individual balance.
2033
+ */
2034
+ class Balance {
2035
+ //
2036
+ // Properties
2037
+ //
2038
+
2039
+ /** The underlying data for this balance */
2040
+ #storage;
2041
+ #valueGetter;
2042
+ #db = null;
2043
+
2044
+ //
2045
+ // Methods
2046
+ //
2047
+
2048
+ constructor(storage, hydrate) {
2049
+ this.#storage = storage;
2050
+ this.#valueGetter = new BalanceValueGetter(this.#storage);
2051
+ if (hydrate !== undefined) this.hydrate(hydrate);
2052
+ }
2053
+ toJSON = () => this.#storage;
2054
+ isSource = source => this.#storage.source === source;
2055
+ // // TODO: Fix this method, the types don't work with our plugin architecture.
2056
+ // // Specifically, the `BalanceJson` type is compiled down to `IBalance` in the following way:
2057
+ // //
2058
+ // // toJSON: () => BalanceJson // works
2059
+ // // isSource: (source: BalanceSource) => boolean // works
2060
+ // // asSource: <P extends string>(source: P) => NarrowBalanceType<IBalance, P> | null // Doesn't work! IBalance should just be BalanceJson!
2061
+ // //
2062
+ // // `IBalance` won't match the type of `BalanceSource` after `PluginBalanceTypes` has been extended by balance plugins.
2063
+ // // As a result, typescript will think that the returned #storage is not a BalanceJson.
2064
+ // asSource = <P extends BalanceSource>(source: P): NarrowBalanceType<BalanceJson, P> | null => {
2065
+ // if (this.#storage.source === source) return this.#storage as NarrowBalanceType<BalanceJson, P>
2066
+ // return null
2067
+ // }
2068
+
2069
+ hydrate = hydrate => {
2070
+ if (hydrate !== undefined) this.#db = hydrate;
2270
2071
  };
2271
- };
2072
+ #format = balance => new BalanceFormatter(isBigInt(balance) ? balance.toString() : balance, this.decimals || undefined, this.rates);
2272
2073
 
2273
- const SUBSCRIPTION_INTERVAL$2 = 6_000;
2274
- const subscribeBalances$6 = ({
2275
- networkId,
2276
- tokensWithAddresses,
2277
- connector
2278
- }) => {
2279
- if (!tokensWithAddresses.length) return of({
2280
- success: [],
2281
- errors: []
2282
- });
2283
- return new Observable(subscriber => {
2284
- const abortController = new AbortController();
2285
- const poll = async () => {
2286
- try {
2287
- if (abortController.signal.aborted) return;
2288
- const balances = await fetchBalances$6({
2289
- networkId,
2290
- tokensWithAddresses: tokensWithAddresses,
2291
- connector
2292
- });
2293
- if (abortController.signal.aborted) return;
2294
- subscriber.next(balances);
2295
- setTimeout(poll, SUBSCRIPTION_INTERVAL$2);
2296
- } catch (error) {
2297
- log.error("Error", {
2298
- module: MODULE_TYPE$6,
2299
- networkId,
2300
- addressesByToken: tokensWithAddresses,
2301
- error
2302
- });
2303
- subscriber.error(error);
2304
- }
2305
- };
2306
- poll();
2307
- return () => {
2308
- abortController.abort();
2309
- };
2310
- }).pipe(distinctUntilChanged(isEqual));
2311
- };
2074
+ //
2075
+ // Accessors
2076
+ //
2077
+
2078
+ get id() {
2079
+ return getBalanceId(this.#storage);
2080
+ }
2081
+ get source() {
2082
+ return this.#storage.source;
2083
+ }
2084
+ get status() {
2085
+ return this.#storage.status;
2086
+ }
2087
+ get address() {
2088
+ return this.#storage.address;
2089
+ }
2090
+ get networkId() {
2091
+ return this.#storage.networkId;
2092
+ }
2093
+ get network() {
2094
+ return this.#db?.networks?.[this.networkId] || null;
2095
+ }
2096
+ get tokenId() {
2097
+ return this.#storage.tokenId;
2098
+ }
2099
+ get token() {
2100
+ return this.#db?.tokens?.[this.tokenId] || null;
2101
+ }
2102
+ get decimals() {
2103
+ return this.token?.decimals || null;
2104
+ }
2105
+ get rates() {
2106
+ // uniswap v2 lp tokens need the rates from the underlying pool assets
2107
+ //
2108
+ // To note: `@talismn/token-rates` knows to fetch the `coingeckoId0` and `coingeckoId1` rates for evm-uniswapv2 tokens.
2109
+ // They are then stored in `this.#db.tokenRates` using the `tokenId0` and `tokenId1` keys.
2110
+ //
2111
+ // This means that those rates are always available for calculating the uniswapv2 rates,
2112
+ // regardless of whether or not the underlying erc20s are actually in chaindata and enabled.
2113
+ if (this.isSource("evm-uniswapv2") && this.token?.type === "evm-uniswapv2") {
2114
+ const tokenId0 = evmErc20TokenId(this.networkId, this.token.tokenAddress0);
2115
+ const tokenId1 = evmErc20TokenId(this.networkId, this.token.tokenAddress1);
2116
+ const decimals = this.token.decimals;
2117
+ const decimals0 = this.token.decimals0;
2118
+ const decimals1 = this.token.decimals1;
2119
+ const rates0 = this.#db?.tokenRates && this.#db.tokenRates[tokenId0];
2120
+ const rates1 = this.#db?.tokenRates && this.#db.tokenRates[tokenId1];
2121
+ if (rates0 === undefined || rates1 === undefined) return null;
2122
+ const extra = this.#valueGetter.get("extra");
2123
+ const extras = Array.isArray(extra) ? extra : extra !== undefined ? [extra] : [];
2124
+ const totalSupply = extras.find(extra => extra.label === "totalSupply")?.amount ?? "0";
2125
+ const reserve0 = extras.find(extra => extra.label === "reserve0")?.amount ?? "0";
2126
+ const reserve1 = extras.find(extra => extra.label === "reserve1")?.amount ?? "0";
2127
+ const totalSupplyTokens = BigNumber(totalSupply).times(Math.pow(10, -1 * decimals));
2128
+ const reserve0Tokens = BigNumber(reserve0).times(Math.pow(10, -1 * decimals0));
2129
+ const reserve1Tokens = BigNumber(reserve1).times(Math.pow(10, -1 * decimals1));
2130
+ const rates0Currencies = new Set(Object.keys(rates0));
2131
+ const rates1Currencies = new Set(Object.keys(rates1));
2132
+ // `Set.prototype.intersection` can eventually replace this
2133
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/intersection
2134
+ const currencies = [...rates0Currencies].filter(c => rates1Currencies.has(c));
2135
+ const totalValueLocked = currencies.map(currency => [currency,
2136
+ // tvl (in a given currency) == reserve0*currencyRate0 + reserve1*currencyRate1
2137
+ BigNumber.sum(reserve0Tokens.times(rates0[currency]?.price ?? 0), reserve1Tokens.times(rates1[currency]?.price ?? 0))]);
2138
+ const lpTokenRates = newTokenRates();
2139
+ totalValueLocked.forEach(([currency, tvl]) => {
2140
+ // divide `the value of all lp tokens` by `the number of lp tokens` to get `the value per token`
2141
+ if (!totalSupplyTokens.eq(0)) lpTokenRates[currency] = {
2142
+ price: tvl.div(totalSupplyTokens).toNumber()
2143
+ };
2144
+ });
2145
+ return lpTokenRates;
2146
+ }
2312
2147
 
2313
- const EvmUniswapV2BalanceModule = {
2314
- type: MODULE_TYPE$6,
2315
- platform: PLATFORM$6,
2316
- getMiniMetadata: getMiniMetadata$6,
2317
- fetchTokens: fetchTokens$6,
2318
- fetchBalances: fetchBalances$6,
2319
- subscribeBalances: subscribeBalances$6,
2320
- getTransferCallData: getTransferCallData$6
2321
- };
2148
+ // other tokens can just pick from the tokenRates db using the tokenId
2149
+ return this.#db?.tokenRates && this.#db.tokenRates[this.tokenId] || null;
2150
+ }
2322
2151
 
2323
- // to be used by chaindata too
2324
- const EvmUniswapV2TokenConfigSchema = z.strictObject({
2325
- contractAddress: EvmUniswapV2TokenSchema.shape.contractAddress,
2326
- ...TokenConfigBaseSchema.shape
2327
- });
2152
+ /**
2153
+ * A general method to get formatted values matching a certain type from this balance.
2154
+ * @param valueType - The type of value to get.
2155
+ * @returns An array of the values matching the type with formatted amounts.
2156
+ */
2157
+ getValue(valueType) {
2158
+ return this.getRawValue(valueType).map(value => ({
2159
+ ...value,
2160
+ amount: this.#format(value.amount)
2161
+ }));
2162
+ }
2328
2163
 
2329
- const MODULE_TYPE$5 = SubAssetsTokenSchema.shape.type.value;
2330
- const PLATFORM$5 = SubAssetsTokenSchema.shape.platform.value;
2164
+ /**
2165
+ * A general method to get values matching a certain type from this balance.
2166
+ * @param valueType - The type of value to get.
2167
+ * @returns An array of the values matching the type.
2168
+ */
2169
+ getRawValue(valueType) {
2170
+ return this.#valueGetter.get(valueType);
2171
+ }
2331
2172
 
2332
- const fetchRpcQueryPack = async (connector, networkId, queries) => {
2333
- const allStateKeys = queries.flatMap(({
2334
- stateKeys
2335
- }) => stateKeys).filter(isNotNil);
2173
+ /**
2174
+ * A general method to add a value to the array of values for this balance.
2175
+ * @param valueType - The type of value to add.
2176
+ * @returns A function which can be used to add a value to the array of values for this balance.
2177
+ */
2178
+ addValue(valueType) {
2179
+ return value => this.#valueGetter.add(valueType, value);
2180
+ }
2181
+ /**
2182
+ * The total balance of this token.
2183
+ * Includes the free and the reserved amount.
2184
+ * The balance will be reaped if this goes below the existential deposit.
2185
+ */
2186
+ get total() {
2187
+ const extra = this.getValue("extra");
2336
2188
 
2337
- // doing a query without keys would throw an error => return early
2338
- if (!allStateKeys.length) return queries.map(({
2339
- stateKeys,
2340
- decodeResult
2341
- }) => decodeResult(stateKeys.map(() => null)));
2342
- const [result] = await connector.send(networkId, "state_queryStorageAt", [allStateKeys]);
2343
- return decodeRpcQueryPack(queries, result);
2344
- };
2345
- const getRpcQueryPack$ = (connector, networkId, queries, timeout = false) => {
2346
- const allStateKeys = queries.flatMap(({
2347
- stateKeys
2348
- }) => stateKeys).filter(isNotNil);
2189
+ // if there is a DelegatedStaking hold (new model: polkadot, kusama), nom pool staked amount is included in reserved
2190
+ // if not (old model: vara, avail, cere), staked amount is not in the account and it needs to be added to the total
2191
+ const nomPoolStakedPlancks = this.locks.some(lock => lock.source === "substrate-native-holds" && lock.label === "DelegatedStaking") ? 0n : this.nompools.map(({
2192
+ amount
2193
+ }) => amount.planck).reduce((a, b) => a + b, 0n);
2194
+ return this.#format(this.free.planck + this.reserved.planck + nomPoolStakedPlancks + this.subtensor.map(({
2195
+ amount
2196
+ }) => amount.planck).reduce((a, b) => a + b, 0n) + includeInTotalExtraAmount(extra));
2197
+ }
2198
+ /** The non-reserved balance of this token. Includes the frozen amount. Is included in the total. */
2199
+ get free() {
2200
+ // for simple balances
2201
+ if ("value" in this.#storage && this.#storage.value) return this.#format(this.#storage.value);
2349
2202
 
2350
- // doing a query without keys would throw an error => return early
2351
- if (!allStateKeys.length) return of(queries.map(({
2352
- stateKeys,
2353
- decodeResult
2354
- }) => decodeResult(stateKeys.map(() => null))));
2355
- return new Observable(subscriber => {
2356
- const promUnsub = connector.subscribe(networkId, "state_subscribeStorage", "state_storage", [allStateKeys], (error, result) => {
2357
- if (error) subscriber.error(error);else subscriber.next(decodeRpcQueryPack(queries, result));
2358
- }, timeout);
2359
- return () => {
2360
- promUnsub.then(unsub => unsub("state_unsubscribeStorage"));
2361
- };
2362
- });
2363
- };
2364
- const decodeRpcQueryPack = (queries, result) => {
2365
- return queries.reduce((acc, {
2366
- stateKeys,
2367
- decodeResult
2368
- }) => {
2369
- const changes = stateKeys.map(stateKey => {
2370
- if (!stateKey) return null;
2371
- const change = result.changes.find(([key]) => key === stateKey);
2372
- if (!change) return null;
2373
- return change[1];
2374
- });
2375
- acc.push(decodeResult(changes));
2376
- return acc;
2377
- }, []);
2378
- };
2203
+ // for complex balances
2204
+ const freeValues = this.getValue("free");
2205
+ const totalFree = freeValues.map(({
2206
+ amount
2207
+ }) => amount.planck).reduce((a, b) => a + b, 0n);
2208
+ return this.#format(totalFree);
2209
+ }
2210
+ /** The reserved balance of this token. Is included in the total. */
2211
+ get reserved() {
2212
+ const reservedValues = this.getValue("reserved");
2213
+ if (reservedValues.length === 0) return this.#format(0n);
2214
+ return this.#format(reservedValues.map(({
2215
+ amount
2216
+ }) => amount.planck).reduce((a, b) => a + b, 0n));
2217
+ }
2218
+ get reserves() {
2219
+ return this.getValue("reserved");
2220
+ }
2221
+ /** The frozen balance of this token. Is included in the free amount. */
2222
+ get locked() {
2223
+ return this.#format(this.locks.map(({
2224
+ amount
2225
+ }) => amount.planck).reduce((a, b) => BigMath.max(a, b), 0n));
2226
+ }
2227
+ get locks() {
2228
+ return this.getValue("locked");
2229
+ }
2230
+ get nompools() {
2231
+ return this.getValue("nompool");
2232
+ }
2233
+ get subtensor() {
2234
+ return this.getValue("subtensor");
2235
+ }
2379
2236
 
2380
- const fetchRuntimeCallResult = async (connector, networkId, metadataRpc, apiName, method, args) => {
2381
- const {
2382
- builder
2383
- } = parseMetadataRpc(metadataRpc);
2384
- const call = builder.buildRuntimeCall(apiName, method);
2385
- const hex = await connector.send(networkId, "state_call", [`${apiName}_${method}`, toHex(call.args.enc(args))]);
2386
- return call.value.dec(hex);
2387
- };
2237
+ /** The extra balance of this token */
2238
+ get extra() {
2239
+ const extra = this.getRawValue("extra");
2240
+ if (extra.length > 0) return extra;
2241
+ return undefined;
2242
+ }
2388
2243
 
2389
- const getConstantValue = (metadataRpc, pallet, constant) => {
2390
- const {
2391
- unifiedMetadata,
2392
- builder
2393
- } = parseMetadataRpc(metadataRpc);
2394
- const codec = builder.buildConstant(pallet, constant);
2395
- const encodedValue = unifiedMetadata.pallets.find(({
2396
- name
2397
- }) => name === pallet)?.constants.find(({
2398
- name
2399
- }) => name === constant)?.value;
2400
- if (!encodedValue) throw new Error(`Constant ${pallet}.${constant} not found`);
2401
- return codec.dec(encodedValue);
2402
- };
2244
+ /** @deprecated Use balance.locked */
2245
+ get frozen() {
2246
+ return this.locked;
2247
+ }
2248
+ /** The transferable balance of this token. Is generally the free amount - the miscFrozen amount. */
2249
+ get transferable() {
2250
+ /**
2251
+ * As you can see here, `locked` is subtracted from `free` in order to derive `transferable`.
2252
+ *
2253
+ * |--------------------------total--------------------------|
2254
+ * |-------------------free-------------------|---reserved---|
2255
+ * |----locked-----|-------transferable-------|
2256
+ */
2257
+ const oldTransferableCalculation = () => {
2258
+ // if no locks exist, transferable is equal to the free amount
2259
+ if (this.locks.length === 0) return this.free;
2403
2260
 
2404
- const tryGetConstantValue = (metadataRpc, pallet, constant) => {
2405
- const {
2406
- unifiedMetadata,
2407
- builder
2408
- } = parseMetadataRpc(metadataRpc);
2409
- const encodedValue = unifiedMetadata.pallets.find(({
2410
- name
2411
- }) => name === pallet)?.constants.find(({
2412
- name
2413
- }) => name === constant)?.value;
2414
- if (!encodedValue) return null;
2415
- const codec = builder.buildConstant(pallet, constant);
2416
- return codec.dec(encodedValue);
2417
- };
2261
+ // find the largest lock (but ignore any locks which are marked as `includeInTransferable`)
2262
+ const excludeAmount = excludeFromTransferableAmount(this.locks);
2418
2263
 
2419
- const hasStorageItem = (metadata, palletName, itemName) => {
2420
- const pallet = metadata.pallets.find(p => p.name === palletName);
2421
- if (!pallet || !pallet.storage) return false;
2422
- return pallet.storage.items.some(item => item.name === itemName);
2423
- };
2424
- const hasStorageItems = (metadata, palletName, itemNames) => {
2425
- const pallet = metadata.pallets.find(p => p.name === palletName);
2426
- if (!pallet || !pallet.storage) return false;
2427
- return itemNames.every(itemName => pallet.storage?.items.some(item => item.name === itemName));
2428
- };
2429
- const hasRuntimeApi = (metadata, apiName, method) => {
2430
- const api = metadata.apis.find(api => api.name === apiName);
2431
- if (!api || !api.methods) return false;
2432
- return api.methods.some(m => m.name === method);
2433
- };
2264
+ // subtract the lock from the free amount (but don't go below 0)
2265
+ return this.#format(BigMath.max(this.free.planck - excludeAmount, 0n));
2266
+ };
2267
+
2268
+ /**
2269
+ * As you can see here, `locked` is subtracted from `free + reserved` in order to derive `transferable`.
2270
+ *
2271
+ * Alternatively, `reserved` is subtracted from `locked` in order to derive `untouchable`,
2272
+ * which is then subtracted from `free` in order to derive `transferable`.
2273
+ *
2274
+ * |--------------------------total--------------------------|
2275
+ * |---reserved---|-------------------free-------------------|
2276
+ * |--untouchable--|
2277
+ * |------------locked------------|-------transferable-------|
2278
+ */
2279
+ const newTransferableCalculation = () => {
2280
+ // if no locks exist, transferable is equal to the free amount
2281
+ if (this.locks.length === 0) return this.free;
2282
+
2283
+ // find the largest lock (but ignore any locks which are marked as `includeInTransferable`)
2284
+ // subtract the reserved amount, because locks now act upon the total balance - not just the free balance
2285
+ const untouchableAmount = BigMath.max(excludeFromTransferableAmount(this.locks) - this.reserved.planck, 0n);
2286
+
2287
+ // subtract the untouchable amount from the free amount (but don't go below 0)
2288
+ return this.#format(BigMath.max(this.free.planck - untouchableAmount, 0n));
2289
+ };
2290
+ if (this.#storage.useLegacyTransferableCalculation) return oldTransferableCalculation();
2291
+ return newTransferableCalculation();
2292
+ }
2293
+ /**
2294
+ * The unavailable balance of this token.
2295
+ * Prior to the Fungible trait, this was the locked amount + the reserved amount, i.e. `locked + reserved`.
2296
+ * Now, it is the bigger of the locked amount and the reserved amounts, i.e. `max(locked, reserved)`.
2297
+ */
2298
+ get unavailable() {
2299
+ const oldCalculation = () => this.locked.planck + this.reserved.planck;
2300
+ const newCalculation = () => BigMath.max(this.locked.planck, this.reserved.planck);
2301
+ const baseUnavailable = this.#storage.useLegacyTransferableCalculation ? oldCalculation() : newCalculation();
2434
2302
 
2435
- const buildNetworkStorageCoders = (chainId, miniMetadata, coders) => {
2436
- if (!miniMetadata.data) return null;
2437
- const metadata = unifyMetadata(decAnyMetadata(miniMetadata.data));
2438
- try {
2439
- const scaleBuilder = getDynamicBuilder(getLookupFn(metadata));
2440
- const builtCoders = Object.fromEntries(Object.entries(coders).flatMap(([key, moduleMethodOrFn]) => {
2441
- const [module, method] = typeof moduleMethodOrFn === "function" ? moduleMethodOrFn({
2442
- chainId
2443
- }) : moduleMethodOrFn;
2444
- try {
2445
- return [[key, scaleBuilder.buildStorage(module, method)]];
2446
- } catch (cause) {
2447
- log.trace(`Failed to build SCALE coder for chain ${chainId} (${module}::${method})`, cause);
2448
- return [];
2449
- }
2450
- }));
2451
- return builtCoders;
2452
- } catch (cause) {
2453
- log.error(`Failed to build SCALE coders for chain ${chainId} (${JSON.stringify(coders)})`, cause);
2303
+ // if there is a DelegatedStaking hold (new model: polkadot, kusama), nom pool staked amount is included in reserved
2304
+ // if not (old model: vara, avail, cere), staked amount is not in the account and it needs to be added to the total
2305
+ const nomPoolStakedPlancks = this.locks.some(lock => lock.source === "substrate-native-holds" && lock.label === "DelegatedStaking") ? 0n : this.nompools.map(({
2306
+ amount
2307
+ }) => amount.planck).reduce((a, b) => a + b, 0n);
2308
+ const otherUnavailable = nomPoolStakedPlancks + this.subtensor.reduce((total, each) => total + each.amount.planck, 0n);
2309
+ return this.#format(baseUnavailable + otherUnavailable);
2454
2310
  }
2455
- return null;
2456
- };
2457
2311
 
2458
- const buildQueries$2 = (networkId, balanceDefs, miniMetadata) => {
2459
- const networkStorageCoders = buildNetworkStorageCoders(networkId, miniMetadata, {
2460
- storage: ["Assets", "Account"]
2461
- });
2462
- return balanceDefs.map(({
2463
- token,
2464
- address
2465
- }) => {
2466
- const scaleCoder = networkStorageCoders?.storage;
2467
- const stateKey = tryEncode(scaleCoder, Number(token.assetId), address) ??
2468
- // Asset Hub
2469
- tryEncode(scaleCoder, BigInt(token.assetId), address); // Astar
2312
+ /** The feePayable balance of this token. Is generally the free amount - the feeFrozen amount. */
2313
+ get feePayable() {
2314
+ // if no locks exist, feePayable is equal to the free amount
2315
+ if (this.locks.length === 0) return this.free;
2470
2316
 
2471
- if (!stateKey) {
2472
- log.warn(`Invalid assetId / address in ${networkId} storage query ${token.assetId} / ${address}`);
2473
- return null;
2474
- }
2475
- const decodeResult = changes => {
2476
- /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
2317
+ // find the largest lock which can't be used to pay tx fees
2318
+ const excludeAmount = excludeFromFeePayableLocks(this.locked.planck.toString()).map(lock => BigInt(lock.amount)).reduce((max, lock) => BigMath.max(max, lock), 0n);
2477
2319
 
2478
- const decoded = decodeScale(scaleCoder, changes[0], `Failed to decode substrate-assets balance on chain ${networkId}`) ?? {
2479
- balance: 0n,
2480
- status: {
2481
- type: "Liquid"
2482
- }};
2483
- const isFrozen = decoded?.status?.type === "Frozen";
2484
- const amount = (decoded?.balance ?? 0n).toString();
2320
+ // subtract the lock from the free amount (but don't go below 0)
2321
+ return this.#format(BigMath.max(this.free.planck - excludeAmount, 0n));
2322
+ }
2323
+ }
2324
+ class BalanceValueGetter {
2325
+ #storage;
2326
+ constructor(storage) {
2327
+ this.#storage = storage;
2328
+ }
2329
+ get(valueType) {
2330
+ if ("values" in this.#storage && this.#storage.values) return this.#storage.values.filter(({
2331
+ type
2332
+ }) => type === valueType);
2333
+ return [];
2334
+ }
2335
+ add(valueType, amount) {
2336
+ if ("values" in this.#storage && this.#storage.values) this.#storage.values.push({
2337
+ type: valueType,
2338
+ ...amount
2339
+ });
2340
+ }
2341
+ }
2342
+ class BalanceFormatter {
2343
+ #planck;
2344
+ #decimals;
2345
+ #tokenRates;
2346
+ constructor(planck, decimals, fiatRatios) {
2347
+ this.#planck = isBigInt(planck) ? planck.toString() : planck ?? "0";
2348
+ this.#decimals = decimals || 0;
2349
+ this.#tokenRates = fiatRatios || null;
2350
+ }
2351
+ toJSON = () => this.#planck;
2352
+ get planck() {
2353
+ return BigInt(this.#planck);
2354
+ }
2355
+ get tokens() {
2356
+ return planckToTokens(this.#planck, this.#decimals);
2357
+ }
2358
+ fiat(currency) {
2359
+ if (!this.#tokenRates) return null;
2360
+ const ratio = this.#tokenRates[currency];
2361
+ if (!ratio) return null;
2362
+ return parseFloat(this.tokens) * ratio.price;
2363
+ }
2364
+ }
2365
+ class PlanckSumBalancesFormatter {
2366
+ #balances;
2367
+ constructor(balances) {
2368
+ this.#balances = balances;
2369
+ }
2370
+ #sum = balanceField => {
2371
+ // a function to get a planck amount from a balance
2372
+ const planck = balance => balance[balanceField].planck ?? 0n;
2373
+ return this.#balances.filterMirrorTokens().each.reduce(
2374
+ // add the total amount to the planck amount of each balance
2375
+ (total, balance) => total + planck(balance),
2376
+ // start with a total of 0
2377
+ 0n);
2378
+ };
2485
2379
 
2486
- // due to the following balance calculations, which are made in the `Balance` type:
2487
- //
2488
- // total balance = (free balance) + (reserved balance)
2489
- // transferable balance = (free balance) - (frozen balance)
2490
- //
2491
- // when `isFrozen` is true we need to set **both** the `free` and `frozen` amounts
2492
- // of this balance to the value we received from the RPC.
2493
- //
2494
- // if we only set the `frozen` amount, then the `total` calculation will be incorrect!
2495
- const free = amount;
2496
- const frozen = token.isFrozen || isFrozen ? amount : "0";
2380
+ /**
2381
+ * The total balance of these tokens. Includes the free and the reserved amount.
2382
+ */
2383
+ get total() {
2384
+ return this.#sum("total");
2385
+ }
2386
+ /** The non-reserved balance of these tokens. Includes the frozen amount. Is included in the total. */
2387
+ get free() {
2388
+ return this.#sum("free");
2389
+ }
2390
+ /** The reserved balance of these tokens. Is included in the total. */
2391
+ get reserved() {
2392
+ return this.#sum("reserved");
2393
+ }
2394
+ /** The frozen balance of these tokens. Is included in the free amount. */
2395
+ get locked() {
2396
+ return this.#sum("locked");
2397
+ }
2398
+ /** @deprecated Use balances.locked */
2399
+ get frozen() {
2400
+ return this.locked;
2401
+ }
2402
+ /** The transferable balance of these tokens. Is generally the free amount - the miscFrozen amount. */
2403
+ get transferable() {
2404
+ return this.#sum("transferable");
2405
+ }
2406
+ /** The unavailable balance of these tokens. */
2407
+ get unavailable() {
2408
+ return this.#sum("unavailable");
2409
+ }
2497
2410
 
2498
- // include balance values even if zero, so that newly-zero values overwrite old values
2499
- const balanceValues = [{
2500
- type: "free",
2501
- label: "free",
2502
- amount: free.toString()
2503
- }, {
2504
- type: "locked",
2505
- label: "frozen",
2506
- amount: frozen.toString()
2507
- }];
2508
- const balance = {
2509
- source: "substrate-assets",
2510
- status: "live",
2511
- address,
2512
- networkId,
2513
- tokenId: token.id,
2514
- values: balanceValues
2411
+ /** The feePayable balance of these tokens. Is generally the free amount - the feeFrozen amount. */
2412
+ get feePayable() {
2413
+ return this.#sum("feePayable");
2414
+ }
2415
+ }
2416
+ class FiatSumBalancesFormatter {
2417
+ #balances;
2418
+ #currency;
2419
+ constructor(balances, currency) {
2420
+ this.#balances = balances;
2421
+ this.#currency = currency;
2422
+ }
2423
+ #sum = balanceField => {
2424
+ // a function to get a fiat amount from a balance
2425
+ const fiat = balance => balance[balanceField].fiat(this.#currency) ?? 0;
2426
+ return this.#balances.filterMirrorTokens().each.reduce(
2427
+ // add the total amount to the fiat amount of each balance
2428
+ (total, balance) => total + fiat(balance),
2429
+ // start with a total of 0
2430
+ 0);
2431
+ };
2432
+
2433
+ /**
2434
+ * The total balance of these tokens. Includes the free and the reserved amount.
2435
+ */
2436
+ get total() {
2437
+ return this.#sum("total");
2438
+ }
2439
+ /** The non-reserved balance of these tokens. Includes the frozen amount. Is included in the total. */
2440
+ get free() {
2441
+ return this.#sum("free");
2442
+ }
2443
+ /** The reserved balance of these tokens. Is included in the total. */
2444
+ get reserved() {
2445
+ return this.#sum("reserved");
2446
+ }
2447
+ /** The frozen balance of these tokens. Is included in the free amount. */
2448
+ get locked() {
2449
+ return this.#sum("locked");
2450
+ }
2451
+ /** @deprecated Use balances.locked */
2452
+ get frozen() {
2453
+ return this.locked;
2454
+ }
2455
+ /** The transferable balance of these tokens. Is generally the free amount - the miscFrozen amount. */
2456
+ get transferable() {
2457
+ return this.#sum("transferable");
2458
+ }
2459
+ /** The unavailable balance of these tokens. */
2460
+ get unavailable() {
2461
+ return this.#sum("unavailable");
2462
+ }
2463
+ /** The feePayable balance of these tokens. Is generally the free amount - the feeFrozen amount. */
2464
+ get feePayable() {
2465
+ return this.#sum("feePayable");
2466
+ }
2467
+ }
2468
+ class SumBalancesFormatter {
2469
+ #balances;
2470
+ constructor(balances) {
2471
+ this.#balances = balances;
2472
+ }
2473
+ get planck() {
2474
+ return new PlanckSumBalancesFormatter(this.#balances);
2475
+ }
2476
+ fiat(currency) {
2477
+ return new FiatSumBalancesFormatter(this.#balances, currency);
2478
+ }
2479
+ change24h(currency) {
2480
+ return new Change24hCurrencyFormatter(this.#balances, currency);
2481
+ }
2482
+ }
2483
+ class Change24hCurrencyFormatter {
2484
+ #balances;
2485
+ #currency;
2486
+ constructor(balances, currency) {
2487
+ this.#balances = balances;
2488
+ this.#currency = currency;
2489
+ }
2490
+ #change24h = balanceField => {
2491
+ const output = this.#balances.filterMirrorTokens().each.reduce(
2492
+ // add the total amount to the fiat amount of each balance
2493
+ (acc, balance) => {
2494
+ const change24h = balance.rates?.[this.#currency]?.change24h;
2495
+ if (typeof change24h !== "number") return acc;
2496
+ const fiat = balance[balanceField].fiat(this.#currency);
2497
+ if (!fiat) return acc;
2498
+ return {
2499
+ totalFiatDiff: acc.totalFiatDiff + fiat * change24h,
2500
+ totalFiat: acc.totalFiat + fiat
2515
2501
  };
2516
- return balance;
2517
- };
2518
- return {
2519
- stateKeys: [stateKey],
2520
- decodeResult
2502
+ },
2503
+ // start with a total of 0
2504
+ {
2505
+ totalFiatDiff: 0,
2506
+ totalFiat: 0
2507
+ });
2508
+ return output.totalFiat === 0 ? null : {
2509
+ diff: output.totalFiatDiff / 100,
2510
+ ratio: output.totalFiatDiff / output.totalFiat
2521
2511
  };
2522
- }).filter(isNotNil);
2523
- };
2524
- const tryEncode = (scaleCoder, ...args) => {
2525
- try {
2526
- return scaleCoder?.keys?.enc?.(...args);
2527
- } catch {
2528
- return null;
2529
- }
2530
- };
2531
-
2532
- const fetchBalances$5 = async ({
2533
- networkId,
2534
- tokensWithAddresses,
2535
- connector,
2536
- miniMetadata
2537
- }) => {
2538
- if (!tokensWithAddresses.length) return {
2539
- success: [],
2540
- errors: []
2541
2512
  };
2542
- const balanceDefs = getBalanceDefs(tokensWithAddresses);
2543
- if (!miniMetadata?.data) {
2544
- log.warn(`MiniMetadata is required for fetching ${MODULE_TYPE$5} balances on ${networkId}.`);
2545
- return {
2546
- success: [],
2547
- errors: balanceDefs.map(def => ({
2548
- tokenId: def.token.id,
2549
- address: def.address,
2550
- error: new Error("Minimetadata is required for fetching balances")
2551
- }))
2552
- };
2513
+ get total() {
2514
+ return this.#change24h("total");
2553
2515
  }
2554
- if (miniMetadata.source !== MODULE_TYPE$5) {
2555
- log.warn(`Ignoring miniMetadata with source ${miniMetadata.source} in ${MODULE_TYPE$5}.`);
2556
- return {
2557
- success: [],
2558
- errors: balanceDefs.map(def => ({
2559
- tokenId: def.token.id,
2560
- address: def.address,
2561
- error: new Error(`Invalid request: miniMetadata source is not ${MODULE_TYPE$5}`)
2562
- }))
2563
- };
2516
+ get free() {
2517
+ return this.#change24h("free");
2564
2518
  }
2565
- if (miniMetadata.chainId !== networkId) {
2566
- log.warn(`Ignoring miniMetadata with chainId ${miniMetadata.chainId} in ${MODULE_TYPE$5}. Expected chainId is ${networkId}`);
2567
- return {
2568
- success: [],
2569
- errors: balanceDefs.map(def => ({
2570
- tokenId: def.token.id,
2571
- address: def.address,
2572
- error: new Error(`Invalid request: Expected chainId is ${networkId}`)
2573
- }))
2574
- };
2519
+ get reserved() {
2520
+ return this.#change24h("reserved");
2575
2521
  }
2576
- const queries = buildQueries$2(networkId, balanceDefs, miniMetadata);
2577
- const balances = await fetchRpcQueryPack(connector, networkId, queries);
2578
- return balanceDefs.reduce((acc, def) => {
2579
- const balance = balances.find(b => b?.address === def.address && b?.tokenId === def.token.id);
2580
- if (balance) acc.success.push(balance);
2581
- //if no entry consider empty balance
2582
- else acc.success.push({
2583
- address: def.address,
2584
- networkId,
2585
- tokenId: def.token.id,
2586
- source: MODULE_TYPE$5,
2587
- status: "live",
2588
- values: [{
2589
- type: "free",
2590
- label: "free",
2591
- amount: "0"
2592
- }, {
2593
- type: "locked",
2594
- label: "frozen",
2595
- amount: "0"
2596
- }]
2597
- });
2598
- return acc;
2599
- }, {
2600
- success: [],
2601
- errors: []
2602
- });
2522
+ get locked() {
2523
+ return this.#change24h("locked");
2524
+ }
2525
+ get frozen() {
2526
+ return this.#change24h("frozen");
2527
+ }
2528
+ get transferable() {
2529
+ return this.#change24h("transferable");
2530
+ }
2531
+ get unavailable() {
2532
+ return this.#change24h("unavailable");
2533
+ }
2534
+ get feePayable() {
2535
+ return this.#change24h("feePayable");
2536
+ }
2537
+ }
2538
+ const filterMirrorTokens = (balance, i, balances) => {
2539
+ const mirrorOf = balance.token?.mirrorOf;
2540
+ return !mirrorOf || !balances.find(b => b.tokenId === mirrorOf);
2603
2541
  };
2604
2542
 
2605
- const fetchTokens$5 = async ({
2606
- networkId,
2607
- tokens,
2608
- connector,
2609
- miniMetadata
2610
- }) => {
2611
- const anyMiniMetadata = miniMetadata;
2612
- if (!anyMiniMetadata?.data) return [];
2613
- const {
2614
- builder
2615
- } = parseMetadataRpc(anyMiniMetadata.data);
2616
- const assetCodec = builder.buildStorage("Assets", "Asset");
2617
- const metadataCodec = builder.buildStorage("Assets", "Metadata");
2618
- const [allAssetStorageKeys, allMetadataStorageKeys] = await Promise.all([connector.send(networkId, "state_getKeys", [getStorageKeyPrefix("Assets", "Asset")]), connector.send(networkId, "state_getKeys", [getStorageKeyPrefix("Assets", "Metadata")])]);
2619
- const [assetStorageResults, metadataStorageResults] = await Promise.all([connector.send(networkId, "state_queryStorageAt", [allAssetStorageKeys]), connector.send(networkId, "state_queryStorageAt", [allMetadataStorageKeys])]);
2620
- const assetStorageEntries = assetStorageResults[0].changes;
2621
- const metadataStorageEntries = metadataStorageResults[0].changes;
2622
- const assetByAssetId = keyBy(assetStorageEntries.map(([key, value]) => {
2623
- const [assetId] = assetCodec.keys.dec(key);
2624
- const asset = assetCodec.value.dec(value);
2625
- return {
2626
- assetId,
2627
- existentialDeposit: asset.min_balance,
2628
- isSufficient: asset.is_sufficient
2629
- };
2630
- }), a => a.assetId);
2631
- const metadataByAssetId = keyBy(metadataStorageEntries.map(([key, value]) => {
2632
- const [assetId] = metadataCodec.keys.dec(key);
2633
- const metadata = metadataCodec.value.dec(value);
2634
- return {
2635
- assetId,
2636
- decimals: metadata.decimals,
2637
- isFrozen: metadata.is_frozen,
2638
- name: metadata.name?.asText(),
2639
- symbol: metadata.symbol?.asText()
2640
- };
2641
- }), a => a.assetId);
2642
- const allTokens = keys(assetByAssetId).map(assetId => assign({}, assetByAssetId[assetId], metadataByAssetId[assetId] ?? undefined));
2643
- const configTokenByAssetId = keyBy(tokens, t => t.assetId);
2644
- return allTokens.map(asset => ({
2645
- id: subAssetTokenId(networkId, String(asset.assetId)),
2646
- type: MODULE_TYPE$5,
2647
- platform: "polkadot",
2648
- networkId,
2649
- assetId: String(asset.assetId),
2650
- isSufficient: asset.isSufficient,
2651
- isFrozen: asset.isFrozen,
2652
- name: asset.name,
2653
- symbol: asset.symbol,
2654
- decimals: asset.decimals ?? 0,
2655
- existentialDeposit: String(asset.existentialDeposit),
2656
- isDefault: true
2657
- }))
2658
- // keep all tokens listed in the config + all tokens marked as sufficient
2659
- .filter(token => {
2660
- const configToken = configTokenByAssetId[token.assetId];
2661
- return configToken || token.isSufficient;
2662
- })
2663
- // apply config overrides
2664
- .map(token => {
2665
- const configToken = configTokenByAssetId[token.assetId];
2666
- return configToken ? assign({}, token, configToken) : token;
2667
- })
2668
- // validate results
2669
- .filter(t => {
2670
- const parsed = SubAssetsTokenSchema.safeParse(t);
2671
- if (!parsed.success) log.warn(`Ignoring invalid token ${MODULE_TYPE$5}`, t);
2672
- return parsed.success;
2673
- });
2543
+ /** `IBalance` is a common interface which all balance types must implement. */
2544
+
2545
+ /** A collection of `BalanceJson` objects */
2546
+
2547
+ /** An unlabelled amount of a balance */
2548
+
2549
+ /** A labelled amount of a balance */
2550
+
2551
+ const getValueId = amount => {
2552
+ const getMetaId = () => {
2553
+ const meta = amount.meta;
2554
+ if (!meta) return "";
2555
+ if (amount.type === "nompool") return meta.poolId?.toString() ?? "";
2556
+ if (amount.type === "subtensor") {
2557
+ const {
2558
+ hotkey,
2559
+ netuid
2560
+ } = meta;
2561
+ if (hotkey && netuid !== undefined) return `${hotkey.toString()}${netuid.toString()}`;
2562
+ }
2563
+ return "";
2564
+ };
2565
+ return [amount.label, amount.type, amount.source, getMetaId()].join("::");
2674
2566
  };
2675
2567
 
2568
+ /** A labelled locked amount of a balance */
2569
+
2570
+ /** A labelled extra amount of a balance */
2571
+
2572
+ /** Used by plugins to help define their custom `BalanceType` */
2573
+
2574
+ /** For fast db access, you can calculate the primary key for a miniMetadata using this method */
2575
+ const deriveMiniMetadataId = ({
2576
+ source,
2577
+ chainId,
2578
+ specVersion
2579
+ }) => toHex(Twox64Concat(new TextEncoder().encode(`${source}${chainId}${specVersion}${MINIMETADATA_VERSION}`))).slice(-64);
2580
+
2676
2581
  const getMiniMetadata$5 = ({
2677
2582
  networkId,
2678
2583
  specVersion,
@@ -6484,7 +6389,7 @@ const POOL = new PQueue({
6484
6389
  });
6485
6390
  const getMiniMetadatas = async (chainConnector, chaindataProvider, networkId, specVersion, signal) => {
6486
6391
  if (CACHE.has(networkId)) return CACHE.get(networkId);
6487
- log.warn("[miniMetadata] getMiniMetadatas called without signal, this may hang the updates", new Error("No signal provided") // this will show the stack trace
6392
+ if (!signal) log.warn("[miniMetadata] getMiniMetadatas called without signal, this may hang the updates", new Error("No signal provided") // this will show the stack trace so the culprit can fix it
6488
6393
  );
6489
6394
  if (specVersion === undefined) specVersion = await getSpecVersion(chainConnector, networkId);
6490
6395
  const pResult = POOL.add(() => fetchMiniMetadatas(chainConnector, chaindataProvider, networkId, specVersion), {
@@ -6553,24 +6458,31 @@ class BalancesProvider {
6553
6458
  // TODO move the getSharedObservable caching down to this.getNetworkBalances$ to prevent network-level subscriptions to restart when enabling/disabling other networks
6554
6459
  // this will require addressesByTokenId arg to be normalized/sorted so the cache key can be compared properly, seems a bit random atm
6555
6460
  return getSharedObservable("BalancesProvider.getBalances$", addressesByTokenId, () => {
6461
+ return this.cleanupAddressesByTokenId(addressesByTokenId).pipe(map(
6556
6462
  // split by network
6557
- const addressesByTokenIdByNetworkId = toPairs(addressesByTokenId).reduce((acc, [tokenId, addresses]) => {
6463
+ addressesByTokenId => toPairs(addressesByTokenId).reduce((acc, [tokenId, addresses]) => {
6558
6464
  const networkId = parseTokenId(tokenId).networkId;
6559
6465
  if (!acc[networkId]) acc[networkId] = {};
6560
6466
  acc[networkId][tokenId] = addresses;
6561
6467
  return acc;
6562
- }, {});
6563
- return combineLatest({
6468
+ }, {})), switchMap(addressesByTokenIdByNetworkId =>
6469
+ // fetch balances and start a 30s timer to mark the whole subscription live after 30s
6470
+ combineLatest({
6564
6471
  isStale: timer(30_000).pipe(map(() => true), startWith(false)),
6565
6472
  results: combineLatest(toPairs(addressesByTokenIdByNetworkId).map(([networkId]) => this.getNetworkBalances$(networkId, addressesByTokenIdByNetworkId[networkId])))
6566
- }).pipe(map(({
6473
+ })), map(
6474
+ // combine
6475
+ ({
6567
6476
  isStale,
6568
6477
  results
6569
6478
  }) => ({
6570
6479
  status: !isStale && results.some(({
6571
6480
  status
6572
6481
  }) => status === "initialising") ? "initialising" : "live",
6573
- balances: results.flatMap(result => result.balances).sort((a, b) => getBalanceId(a).localeCompare(getBalanceId(b)))
6482
+ balances: results.flatMap(result => result.balances.map(b => isStale && b.status !== "live" ? {
6483
+ ...b,
6484
+ status: "stale"
6485
+ } : b)).sort((a, b) => getBalanceId(a).localeCompare(getBalanceId(b)))
6574
6486
  })), startWith({
6575
6487
  status: "initialising",
6576
6488
  balances: this.getStoredBalances(addressesByTokenId)
@@ -6612,7 +6524,12 @@ class BalancesProvider {
6612
6524
  const storage = this.#storage.getValue();
6613
6525
  const balances = assign({}, storage.balances,
6614
6526
  // delete all balances expected in the result set. because if they are not present it means they are empty.
6615
- fromPairs(balanceIds.map(balanceId => [balanceId, undefined])), keyBy(results.balances, b => getBalanceId(b)));
6527
+ fromPairs(balanceIds.map(balanceId => [balanceId, undefined])), keyBy(
6528
+ // storage balances must have status "cache", because they are used as start value when initialising subsequent subscriptions
6529
+ results.balances.map(b => ({
6530
+ ...b,
6531
+ status: "cache"
6532
+ })), b => getBalanceId(b)));
6616
6533
  this.#storage.next(assign({}, storage, {
6617
6534
  balances
6618
6535
  }));
@@ -6655,39 +6572,49 @@ class BalancesProvider {
6655
6572
  }) => status === "initialising") ? "initialising" : "live",
6656
6573
  balances: results.flatMap(result => result.balances)
6657
6574
  };
6575
+ }), startWith({
6576
+ status: "initialising",
6577
+ balances: this.getStoredBalances(addressesByTokenId)
6658
6578
  }));
6659
6579
  }
6660
6580
  getNetworkMiniMetadatas$(networkId) {
6661
6581
  return this.#chaindataProvider.getNetworkById$(networkId).pipe(switchMap(network => isNetworkDot(network) && this.#chainConnectors.substrate ? from(getSpecVersion(this.#chainConnectors.substrate, networkId)).pipe(switchMap(specVersion => this.getMiniMetadatas$(networkId, specVersion))) : of([])));
6662
6582
  }
6663
6583
  getMiniMetadatas$(networkId, specVersion) {
6664
- return combineLatest({
6665
- defaultMiniMetadatas: this.getDefaultMiniMetadatas$(networkId, specVersion),
6666
- storedMiniMetadatas: this.getStoredMiniMetadatas$(networkId, specVersion)
6667
- }).pipe(switchMap(({
6668
- storedMiniMetadatas,
6669
- defaultMiniMetadatas
6670
- }) => {
6671
- if (defaultMiniMetadatas.length) return of(defaultMiniMetadatas);
6672
- if (storedMiniMetadatas.length) return of(storedMiniMetadatas);
6673
- if (!this.#chainConnectors.substrate) return of([]);
6674
- return from(
6675
- // fetch them from the chain
6676
- getMiniMetadatas(this.#chainConnectors.substrate, this.#chaindataProvider, networkId)).pipe(
6677
- // and persist in storage for later reuse
6678
- tap(newMiniMetadatas => {
6679
- if (!newMiniMetadatas.length) return;
6680
- const storage = this.#storage.getValue();
6681
- const miniMetadatas = assign(
6682
- // keep minimetadatas of other networks
6683
- keyBy(values(storage.miniMetadatas).filter(m => m.chainId !== networkId), m => m.id),
6684
- // add the ones for our network
6685
- keyBy(newMiniMetadatas, m => m.id));
6686
- this.#storage.next(assign({}, storage, {
6687
- miniMetadatas
6584
+ return new Observable(subscriber => {
6585
+ const controller = new AbortController();
6586
+ const subscription = combineLatest({
6587
+ defaultMiniMetadatas: this.getDefaultMiniMetadatas$(networkId, specVersion),
6588
+ storedMiniMetadatas: this.getStoredMiniMetadatas$(networkId, specVersion)
6589
+ }).pipe(switchMap(({
6590
+ storedMiniMetadatas,
6591
+ defaultMiniMetadatas
6592
+ }) => {
6593
+ if (defaultMiniMetadatas.length) return of(defaultMiniMetadatas);
6594
+ if (storedMiniMetadatas.length) return of(storedMiniMetadatas);
6595
+ if (!this.#chainConnectors.substrate) return of([]);
6596
+ return from(
6597
+ // fetch them from the chain
6598
+ getMiniMetadatas(this.#chainConnectors.substrate, this.#chaindataProvider, networkId, specVersion, controller.signal)).pipe(
6599
+ // and persist in storage for later reuse
6600
+ tap(newMiniMetadatas => {
6601
+ if (!newMiniMetadatas.length) return;
6602
+ const storage = this.#storage.getValue();
6603
+ const miniMetadatas = assign(
6604
+ // keep minimetadatas of other networks
6605
+ keyBy(values(storage.miniMetadatas).filter(m => m.chainId !== networkId), m => m.id),
6606
+ // add the ones for our network
6607
+ keyBy(newMiniMetadatas, m => m.id));
6608
+ this.#storage.next(assign({}, storage, {
6609
+ miniMetadatas
6610
+ }));
6688
6611
  }));
6689
- }));
6690
- }));
6612
+ })).subscribe(subscriber);
6613
+ return () => {
6614
+ subscription.unsubscribe();
6615
+ controller.abort();
6616
+ };
6617
+ });
6691
6618
  }
6692
6619
  getStoredMiniMetadatas$(networkId, specVersion) {
6693
6620
  return this.storage$.pipe(map(storage => storage.miniMetadatas.filter(m => m.chainId === networkId && m.specVersion === specVersion && m.version === MINIMETADATA_VERSION)), distinctUntilChanged(isEqual));
@@ -6702,6 +6629,26 @@ class BalancesProvider {
6702
6629
  tokenId
6703
6630
  })]).filter(isNotNil).sort((a, b) => getBalanceId(a).localeCompare(getBalanceId(b)));
6704
6631
  }
6632
+ cleanupAddressesByTokenId(addressesByTokenId) {
6633
+ return this.#chaindataProvider.getNetworksMapById$().pipe(map(networksById => {
6634
+ return fromPairs(toPairs(addressesByTokenId).map(([tokenId, addresses]) => {
6635
+ const networkId = parseTokenId(tokenId).networkId;
6636
+ const network = networksById[networkId];
6637
+ return [tokenId, addresses.filter(address => network && isAddressCompatibleWithNetwork(network, address))];
6638
+ }).filter(([, addresses]) => addresses.length > 0));
6639
+ }));
6640
+ }
6705
6641
  }
6642
+ const isAddressCompatibleWithNetwork = (network, address) => {
6643
+ switch (network.platform) {
6644
+ case "ethereum":
6645
+ return isEthereumAddress(address);
6646
+ case "polkadot":
6647
+ return isEthereumAddress(address) ? network.account === "secp256k1" : network.account !== "secp256k1";
6648
+ default:
6649
+ log.warn("Unsupported network platform", network);
6650
+ throw new Error("Unsupported network platform");
6651
+ }
6652
+ };
6706
6653
 
6707
- export { BALANCE_MODULES, Balance, BalanceFormatter, BalanceValueGetter, Balances, BalancesProvider, Change24hCurrencyFormatter, EvmErc20BalanceModule, EvmErc20TokenConfigSchema, EvmNativeBalanceModule, EvmNativeTokenConfigSchema, EvmUniswapV2BalanceModule, EvmUniswapV2TokenConfigSchema, FiatSumBalancesFormatter, ONE_ALPHA_TOKEN, PlanckSumBalancesFormatter, SCALE_FACTOR, SUBTENSOR_MIN_STAKE_AMOUNT_PLANK, SUBTENSOR_ROOT_NETUID, SubAssetsBalanceModule, SubAssetsTokenConfigSchema, SubForeignAssetsBalanceModule, SubForeignAssetsTokenConfigSchema, SubHydrationBalanceModule, SubHydrationTokenConfigSchema, SubNativeBalanceModule, SubNativeMiniMetadataExtraSchema, SubNativeModuleConfigSchema, SubNativeTokenConfigSchema, SubPsp22BalanceModule, SubPsp22TokenConfigSchema, SubTokensBalanceModule, SubTokensMiniMetadataExtraSchema, SubTokensModuleConfigSchema, SubTokensTokenConfigSchema, SumBalancesFormatter, TalismanBalancesDatabase, abiMulticall, calculateAlphaPrice, calculateTaoAmountFromAlpha, calculateTaoFromDynamicInfo, db, deriveMiniMetadataId, erc20BalancesAggregatorAbi, excludeFromFeePayableLocks, excludeFromTransferableAmount, filterBaseLocks, filterMirrorTokens, getBalanceId, getLockTitle, getLockedType, getValueId, includeInTotalExtraAmount, uniswapV2PairAbi };
6654
+ export { BALANCE_MODULES, Balance, BalanceFormatter, BalanceValueGetter, Balances, BalancesProvider, Change24hCurrencyFormatter, EvmErc20BalanceModule, EvmErc20TokenConfigSchema, EvmNativeBalanceModule, EvmNativeTokenConfigSchema, EvmUniswapV2BalanceModule, EvmUniswapV2TokenConfigSchema, FiatSumBalancesFormatter, ONE_ALPHA_TOKEN, PlanckSumBalancesFormatter, SCALE_FACTOR, SUBTENSOR_MIN_STAKE_AMOUNT_PLANK, SUBTENSOR_ROOT_NETUID, SubAssetsBalanceModule, SubAssetsTokenConfigSchema, SubForeignAssetsBalanceModule, SubForeignAssetsTokenConfigSchema, SubHydrationBalanceModule, SubHydrationTokenConfigSchema, SubNativeBalanceModule, SubNativeMiniMetadataExtraSchema, SubNativeModuleConfigSchema, SubNativeTokenConfigSchema, SubPsp22BalanceModule, SubPsp22TokenConfigSchema, SubTokensBalanceModule, SubTokensMiniMetadataExtraSchema, SubTokensModuleConfigSchema, SubTokensTokenConfigSchema, SumBalancesFormatter, abiMulticall, calculateAlphaPrice, calculateTaoAmountFromAlpha, calculateTaoFromDynamicInfo, deriveMiniMetadataId, erc20BalancesAggregatorAbi, excludeFromFeePayableLocks, excludeFromTransferableAmount, filterBaseLocks, filterMirrorTokens, getBalanceId, getLockTitle, getLockedType, getValueId, includeInTotalExtraAmount, isAddressCompatibleWithNetwork, uniswapV2PairAbi };