@talismn/balances 0.0.0-pr2067-20250630033435 → 0.0.0-pr2075-20250703111149
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/declarations/src/BalanceModule.d.ts +23 -12
- package/dist/declarations/src/getMiniMetadata/getMetadataRpc.d.ts +3 -0
- package/dist/declarations/src/getMiniMetadata/getMiniMetadatas.d.ts +4 -0
- package/dist/declarations/src/getMiniMetadata/getSpecVersion.d.ts +6 -0
- package/dist/declarations/src/getMiniMetadata/getUpdatedMiniMetadatas.d.ts +4 -0
- package/dist/declarations/src/getMiniMetadata/index.d.ts +5 -0
- package/dist/declarations/src/index.d.ts +0 -3
- package/dist/declarations/src/libVersion.d.ts +1 -0
- package/dist/declarations/src/modules/EvmErc20Module.d.ts +20 -23
- package/dist/declarations/src/modules/EvmNativeModule.d.ts +19 -18
- package/dist/declarations/src/modules/EvmUniswapV2Module.d.ts +28 -30
- package/dist/declarations/src/modules/SubstrateAssetsModule.d.ts +20 -18
- package/dist/declarations/src/modules/SubstrateForeignAssetsModule.d.ts +20 -18
- package/dist/declarations/src/modules/SubstrateNativeModule/index.d.ts +17 -4
- package/dist/declarations/src/modules/SubstrateNativeModule/subscribeNompoolStaking.d.ts +3 -3
- package/dist/declarations/src/modules/SubstrateNativeModule/subscribeSubtensorStaking.d.ts +3 -3
- package/dist/declarations/src/modules/SubstrateNativeModule/types.d.ts +6 -23
- package/dist/declarations/src/modules/SubstrateNativeModule/util/QueryCache.d.ts +6 -6
- package/dist/declarations/src/modules/SubstrateNativeModule/util/buildQueries.d.ts +5 -4
- package/dist/declarations/src/modules/SubstrateNativeModule/util/sortChains.d.ts +1 -1
- package/dist/declarations/src/modules/SubstrateNativeModule/util/systemProperties.d.ts +5 -0
- package/dist/declarations/src/modules/SubstratePsp22Module.d.ts +20 -19
- package/dist/declarations/src/modules/SubstrateTokensModule.d.ts +22 -22
- package/dist/declarations/src/modules/index.d.ts +213 -38
- package/dist/declarations/src/modules/util/InferBalanceModuleTypes.d.ts +5 -3
- package/dist/declarations/src/modules/util/buildStorageCoders.d.ts +16 -7
- package/dist/declarations/src/modules/util/getAddresssesByTokenByNetwork.d.ts +3 -0
- package/dist/declarations/src/modules/util/getUniqueChainIds.d.ts +2 -2
- package/dist/declarations/src/modules/util/index.d.ts +0 -1
- package/dist/declarations/src/types/balances.d.ts +220 -72
- package/dist/declarations/src/types/balancetypes.d.ts +8 -18
- package/dist/declarations/src/types/minimetadatas.d.ts +6 -24
- package/dist/declarations/src/types/tokens.d.ts +11 -0
- package/dist/talismn-balances.cjs.dev.js +1415 -2090
- package/dist/talismn-balances.cjs.prod.js +1415 -2090
- package/dist/talismn-balances.esm.js +1417 -2086
- package/package.json +9 -8
- package/dist/declarations/src/EvmTokenFetcher.d.ts +0 -11
- package/dist/declarations/src/MiniMetadataUpdater.d.ts +0 -43
- package/dist/declarations/src/modules/SubstrateEquilibriumModule.d.ts +0 -39
- package/dist/declarations/src/modules/SubstrateNativeModule/subscribeCrowdloans.d.ts +0 -5
- package/dist/declarations/src/modules/SubstrateNativeModule/util/crowdloanFundContributionsChildKey.d.ts +0 -4
- package/dist/declarations/src/modules/SubstrateNativeModule/util/detectMiniMetadataChanges.d.ts +0 -2
- package/dist/declarations/src/modules/util/findChainMeta.d.ts +0 -8
- package/dist/declarations/src/util/hydrateChaindata.d.ts +0 -8
- package/dist/declarations/src/util/index.d.ts +0 -1
@@ -1,29 +1,30 @@
|
|
1
|
-
import
|
2
|
-
import { fetchMiniMetadatas, fetchInitMiniMetadatas, availableTokenLogoFilenames, githubTokenLogoUrl } from '@talismn/chaindata-provider';
|
3
|
-
import { fetchBestMetadata, getScaleApi } from '@talismn/sapi';
|
4
|
-
import { Dexie, liveQuery } from 'dexie';
|
5
|
-
import isEqual from 'lodash/isEqual';
|
6
|
-
import { from, Observable, scan, share, map, switchAll, combineLatest, mergeMap, toArray, interval, startWith, exhaustMap, pipe, filter, shareReplay, combineLatestWith, distinctUntilChanged, firstValueFrom, BehaviorSubject, debounceTime, takeUntil, switchMap, withLatestFrom, concatMap } from 'rxjs';
|
1
|
+
import { Dexie } from 'dexie';
|
7
2
|
import anylogger from 'anylogger';
|
3
|
+
import { evmErc20TokenId, TokenBaseSchema, EvmErc20TokenSchema, networkIdFromTokenId, EvmUniswapV2TokenSchema, evmUniswapV2TokenId, getGithubTokenLogoUrl, DotNetworkBalancesConfigSchema, SubAssetsTokenSchema, parseSubAssetTokenId, subAssetTokenId, SubForeignAssetsTokenSchema, parseSubForeignAssetTokenId, subForeignAssetTokenId, parseTokenId, subNativeTokenId, SubPsp22TokenSchema, subPsp22TokenId, SubTokensTokenSchema, parseSubTokensTokenId, subTokensTokenId } from '@talismn/chaindata-provider';
|
8
4
|
import { newTokenRates } from '@talismn/token-rates';
|
9
|
-
import { isBigInt, BigMath, planckToTokens,
|
5
|
+
import { isBigInt, BigMath, planckToTokens, isArrayOf, isTruthy, isEthereumAddress, hasOwnProperty, isAbortError, isNotNil, decodeAnyAddress, blake2Concat, Deferred } from '@talismn/util';
|
10
6
|
import BigNumber from 'bignumber.js';
|
11
|
-
import { u8aToHex, assert, stringCamelCase, u8aConcatStrict,
|
12
|
-
import { xxhashAsU8a
|
7
|
+
import { u8aToHex, assert, stringCamelCase, u8aConcatStrict, arrayChunk, u8aToString, hexToNumber, hexToU8a } from '@polkadot/util';
|
8
|
+
import { xxhashAsU8a } from '@polkadot/util-crypto';
|
13
9
|
import pako from 'pako';
|
10
|
+
import z from 'zod/v4';
|
14
11
|
import { parseAbi, isHex, hexToBigInt } from 'viem';
|
12
|
+
import { fromPairs, toPairs, keys, groupBy as groupBy$1 } from 'lodash';
|
13
|
+
import isEqual from 'lodash/isEqual';
|
15
14
|
import { defineMethod } from '@substrate/txwrapper-core';
|
16
|
-
import { unifyMetadata, decAnyMetadata, getDynamicBuilder, getLookupFn,
|
15
|
+
import { unifyMetadata, decAnyMetadata, getDynamicBuilder, getLookupFn, compactMetadata, encodeMetadata, decodeScale, papiParse, getMetadataVersion, encodeStateKey } from '@talismn/scale';
|
17
16
|
import camelCase from 'lodash/camelCase';
|
17
|
+
import PQueue from 'p-queue';
|
18
|
+
import { fetchBestMetadata, getScaleApi } from '@talismn/sapi';
|
18
19
|
import { Metadata, TypeRegistry } from '@polkadot/types';
|
19
20
|
import groupBy from 'lodash/groupBy';
|
20
21
|
import { mergeUint8, toHex } from '@polkadot-api/utils';
|
21
22
|
import { Binary, AccountId } from 'polkadot-api';
|
22
23
|
import { ChainConnectionError } from '@talismn/chain-connector';
|
23
|
-
import {
|
24
|
+
import { Observable, scan, share, map, switchAll, combineLatest, from, mergeMap, toArray, interval, startWith, exhaustMap, BehaviorSubject, debounceTime, takeUntil, distinctUntilChanged, switchMap, withLatestFrom, concatMap } from 'rxjs';
|
25
|
+
import { u32, Struct, u128 } from 'scale-ts';
|
24
26
|
import upperFirst from 'lodash/upperFirst';
|
25
27
|
import { Abi } from '@polkadot/api-contract';
|
26
|
-
import { compressToEncodedURIComponent } from 'lz-string';
|
27
28
|
|
28
29
|
// TODO: Document default balances module purpose/usage
|
29
30
|
const DefaultBalanceModule = type => ({
|
@@ -58,48 +59,11 @@ const DefaultBalanceModule = type => ({
|
|
58
59
|
// internal
|
59
60
|
//
|
60
61
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
class EvmTokenFetcher {
|
65
|
-
#chaindataProvider;
|
66
|
-
#balanceModules;
|
67
|
-
constructor(chaindataProvider, balanceModules) {
|
68
|
-
this.#chaindataProvider = chaindataProvider;
|
69
|
-
this.#balanceModules = balanceModules;
|
70
|
-
}
|
71
|
-
async update(evmNetworkIds) {
|
72
|
-
await this.updateEvmNetworks(evmNetworkIds);
|
73
|
-
}
|
74
|
-
async updateEvmNetworks(evmNetworkIds) {
|
75
|
-
const evmNetworks = new Map((await this.#chaindataProvider.evmNetworks()).map(evmNetwork => [evmNetwork.id, evmNetwork]));
|
76
|
-
const allEvmTokens = {};
|
77
|
-
const evmNetworkConcurrency = 10;
|
78
|
-
await PromisePool.withConcurrency(evmNetworkConcurrency).for(evmNetworkIds).process(async evmNetworkId => {
|
79
|
-
const evmNetwork = evmNetworks.get(evmNetworkId);
|
80
|
-
if (!evmNetwork) return;
|
81
|
-
for (const mod of this.#balanceModules.filter(m => m.type.startsWith("evm-"))) {
|
82
|
-
const balancesConfig = (evmNetwork.balancesConfig ?? []).find(({
|
83
|
-
moduleType
|
84
|
-
}) => moduleType === mod.type);
|
85
|
-
const moduleConfig = balancesConfig?.moduleConfig ?? {};
|
86
|
-
|
87
|
-
// chainMeta arg only needs the isTestnet property, let's save a db roundtrip for now
|
88
|
-
const isTestnet = evmNetwork.isTestnet ?? false;
|
89
|
-
const tokens = await mod.fetchEvmChainTokens(evmNetworkId, {
|
90
|
-
isTestnet
|
91
|
-
}, moduleConfig);
|
92
|
-
for (const [tokenId, token] of Object.entries(tokens)) allEvmTokens[tokenId] = token;
|
93
|
-
}
|
94
|
-
});
|
95
|
-
await this.#chaindataProvider.updateEvmNetworkTokens(Object.values(allEvmTokens));
|
96
|
-
}
|
97
|
-
}
|
98
|
-
|
99
|
-
var packageJson = {
|
100
|
-
name: "@talismn/balances"};
|
62
|
+
var pkg = {
|
63
|
+
name: "@talismn/balances",
|
64
|
+
version: "0.0.0-pr2075-20250703111149"};
|
101
65
|
|
102
|
-
var log = anylogger(
|
66
|
+
var log = anylogger(pkg.name);
|
103
67
|
|
104
68
|
function excludeFromTransferableAmount(locks) {
|
105
69
|
if (typeof locks === "string") return BigInt(locks);
|
@@ -309,15 +273,13 @@ class Balances {
|
|
309
273
|
return new SumBalancesFormatter(this);
|
310
274
|
}
|
311
275
|
}
|
312
|
-
const isBalanceEvm = balance => "evmNetworkId" in balance;
|
313
276
|
const getBalanceId = balance => {
|
314
277
|
const {
|
315
278
|
source,
|
316
279
|
address,
|
317
280
|
tokenId
|
318
281
|
} = balance;
|
319
|
-
|
320
|
-
return [source, address, locationId, tokenId].filter(isTruthy).join("::");
|
282
|
+
return [source, address, tokenId].join("::");
|
321
283
|
};
|
322
284
|
|
323
285
|
/**
|
@@ -379,23 +341,17 @@ class Balance {
|
|
379
341
|
get address() {
|
380
342
|
return this.#storage.address;
|
381
343
|
}
|
382
|
-
get
|
383
|
-
return
|
384
|
-
}
|
385
|
-
get chain() {
|
386
|
-
return this.#db?.chains && this.chainId && this.#db?.chains[this.chainId] || null;
|
344
|
+
get networkId() {
|
345
|
+
return this.#storage.networkId;
|
387
346
|
}
|
388
|
-
get
|
389
|
-
return
|
390
|
-
}
|
391
|
-
get evmNetwork() {
|
392
|
-
return this.#db?.evmNetworks && this.evmNetworkId && this.#db?.evmNetworks[this.evmNetworkId] || null;
|
347
|
+
get network() {
|
348
|
+
return this.#db?.networks?.[this.networkId] || null;
|
393
349
|
}
|
394
350
|
get tokenId() {
|
395
351
|
return this.#storage.tokenId;
|
396
352
|
}
|
397
353
|
get token() {
|
398
|
-
return this.#db?.tokens
|
354
|
+
return this.#db?.tokens?.[this.tokenId] || null;
|
399
355
|
}
|
400
356
|
get decimals() {
|
401
357
|
return this.token?.decimals || null;
|
@@ -408,9 +364,9 @@ class Balance {
|
|
408
364
|
//
|
409
365
|
// This means that those rates are always available for calculating the uniswapv2 rates,
|
410
366
|
// regardless of whether or not the underlying erc20s are actually in chaindata and enabled.
|
411
|
-
if (this.isSource("evm-uniswapv2") && this.token?.type === "evm-uniswapv2"
|
412
|
-
const tokenId0 = evmErc20TokenId
|
413
|
-
const tokenId1 = evmErc20TokenId
|
367
|
+
if (this.isSource("evm-uniswapv2") && this.token?.type === "evm-uniswapv2") {
|
368
|
+
const tokenId0 = evmErc20TokenId(this.networkId, this.token.tokenAddress0);
|
369
|
+
const tokenId1 = evmErc20TokenId(this.networkId, this.token.tokenAddress1);
|
414
370
|
const decimals = this.token.decimals;
|
415
371
|
const decimals0 = this.token.decimals0;
|
416
372
|
const decimals1 = this.token.decimals1;
|
@@ -489,9 +445,7 @@ class Balance {
|
|
489
445
|
const nomPoolStakedPlancks = this.locks.some(lock => lock.source === "substrate-native-holds" && lock.label === "DelegatedStaking") ? 0n : this.nompools.map(({
|
490
446
|
amount
|
491
447
|
}) => amount.planck).reduce((a, b) => a + b, 0n);
|
492
|
-
return this.#format(this.free.planck + this.reserved.planck + nomPoolStakedPlancks + this.
|
493
|
-
amount
|
494
|
-
}) => amount.planck).reduce((a, b) => a + b, 0n) + this.subtensor.map(({
|
448
|
+
return this.#format(this.free.planck + this.reserved.planck + nomPoolStakedPlancks + this.subtensor.map(({
|
495
449
|
amount
|
496
450
|
}) => amount.planck).reduce((a, b) => a + b, 0n) + includeInTotalExtraAmount(extra));
|
497
451
|
}
|
@@ -527,9 +481,6 @@ class Balance {
|
|
527
481
|
get locks() {
|
528
482
|
return this.getValue("locked");
|
529
483
|
}
|
530
|
-
get crowdloans() {
|
531
|
-
return this.getValue("crowdloan");
|
532
|
-
}
|
533
484
|
get nompools() {
|
534
485
|
return this.getValue("nompool");
|
535
486
|
}
|
@@ -608,7 +559,7 @@ class Balance {
|
|
608
559
|
const nomPoolStakedPlancks = this.locks.some(lock => lock.source === "substrate-native-holds" && lock.label === "DelegatedStaking") ? 0n : this.nompools.map(({
|
609
560
|
amount
|
610
561
|
}) => amount.planck).reduce((a, b) => a + b, 0n);
|
611
|
-
const otherUnavailable = nomPoolStakedPlancks + this.
|
562
|
+
const otherUnavailable = nomPoolStakedPlancks + this.subtensor.reduce((total, each) => total + each.amount.planck, 0n);
|
612
563
|
return this.#format(baseUnavailable + otherUnavailable);
|
613
564
|
}
|
614
565
|
|
@@ -843,10 +794,6 @@ const filterMirrorTokens = (balance, i, balances) => {
|
|
843
794
|
return !mirrorOf || !balances.find(b => b.tokenId === mirrorOf);
|
844
795
|
};
|
845
796
|
|
846
|
-
// TODO: Move this into a common module which can then be imported both here and into EvmErc20Module
|
847
|
-
// We can't import this directly from EvmErc20Module because then we'd have a circular dependency
|
848
|
-
const evmErc20TokenId$1 = (chainId, tokenContractAddress) => `${chainId}-evm-erc20-${tokenContractAddress}`.toLowerCase();
|
849
|
-
|
850
797
|
/**
|
851
798
|
* `BalanceTypes` is an automatically determined sub-selection of `PluginBalanceTypes`.
|
852
799
|
*
|
@@ -877,7 +824,6 @@ const getValueId = amount => {
|
|
877
824
|
const getMetaId = () => {
|
878
825
|
const meta = amount.meta;
|
879
826
|
if (!meta) return "";
|
880
|
-
if (amount.type === "crowdloan") return meta.paraId?.toString() ?? "";
|
881
827
|
if (amount.type === "nompool") return meta.poolId?.toString() ?? "";
|
882
828
|
if (amount.type === "subtensor") {
|
883
829
|
const {
|
@@ -901,10 +847,9 @@ const getValueId = amount => {
|
|
901
847
|
const deriveMiniMetadataId = ({
|
902
848
|
source,
|
903
849
|
chainId,
|
904
|
-
specName,
|
905
850
|
specVersion,
|
906
|
-
|
907
|
-
}) => u8aToHex(xxhashAsU8a(new TextEncoder().encode(`${source}${chainId}${
|
851
|
+
libVersion
|
852
|
+
}) => u8aToHex(xxhashAsU8a(new TextEncoder().encode(`${source}${chainId}${specVersion}${libVersion}`), 64), undefined, false);
|
908
853
|
|
909
854
|
// for DB version 3, Wallet version 1.21.0
|
910
855
|
const upgradeRemoveSymbolFromNativeTokenId = async tx => {
|
@@ -999,230 +944,9 @@ class TalismanBalancesDatabase extends Dexie {
|
|
999
944
|
}
|
1000
945
|
const db = new TalismanBalancesDatabase();
|
1001
946
|
|
1002
|
-
const
|
1003
|
-
|
1004
|
-
|
1005
|
-
* A substrate dapp needs access to a set of types when it wants to communicate with a blockchain node.
|
1006
|
-
*
|
1007
|
-
* These types are used to encode requests & decode responses via the SCALE codec.
|
1008
|
-
* Each chain generally has its own set of types.
|
1009
|
-
*
|
1010
|
-
* Substrate provides a construct to retrieve these types from a blockchain node.
|
1011
|
-
* The chain metadata.
|
1012
|
-
*
|
1013
|
-
* The metadata includes the types required for any communication with the chain,
|
1014
|
-
* including lots of methods which are not relevant to balance fetching.
|
1015
|
-
*
|
1016
|
-
* As such, the metadata can clock in at around 1-2MB per chain, which is a lot of storage
|
1017
|
-
* for browser-based dapps which want to connect to lots of chains.
|
1018
|
-
*
|
1019
|
-
* By utilizing the wonderful [scale-ts](https://github.com/unstoppablejs/unstoppablejs/tree/main/packages/scale-ts#readme) library,
|
1020
|
-
* we can trim the chain metadata down so that it only includes the types we need for balance fetching.
|
1021
|
-
*
|
1022
|
-
* Each balance module has a function to do just that, `BalanceModule::fetchSubstrateChainMeta`.
|
1023
|
-
*
|
1024
|
-
* But, we only want to run this operation when necessary.
|
1025
|
-
*
|
1026
|
-
* The purpose of this class, `MiniMetadataUpdater`, is to maintain a local cache of
|
1027
|
-
* trimmed-down metadatas, which we'll refer to as `MiniMetadatas`.
|
1028
|
-
*/
|
1029
|
-
class MiniMetadataUpdater {
|
1030
|
-
#lastHydratedMiniMetadatasAt = 0;
|
1031
|
-
#lastHydratedCustomChainsAt = 0;
|
1032
|
-
#chainConnectors;
|
1033
|
-
#chaindataProvider;
|
1034
|
-
#balanceModules;
|
1035
|
-
constructor(chainConnectors, chaindataProvider, balanceModules) {
|
1036
|
-
this.#chainConnectors = chainConnectors;
|
1037
|
-
this.#chaindataProvider = chaindataProvider;
|
1038
|
-
this.#balanceModules = balanceModules;
|
1039
|
-
}
|
1040
|
-
|
1041
|
-
/** Subscribe to the metadata for a chain */
|
1042
|
-
subscribe(chainId) {
|
1043
|
-
return from(liveQuery(() => db.miniMetadatas.filter(m => m.chainId === chainId).toArray().then(array => array[0])));
|
1044
|
-
}
|
1045
|
-
async update(chainIds) {
|
1046
|
-
await this.updateSubstrateChains(chainIds);
|
1047
|
-
}
|
1048
|
-
async statuses(chains) {
|
1049
|
-
const ids = await db.miniMetadatas.orderBy("id").primaryKeys();
|
1050
|
-
const wantedIdsByChain = new Map(chains.flatMap(({
|
1051
|
-
id: chainId,
|
1052
|
-
specName,
|
1053
|
-
specVersion,
|
1054
|
-
balancesConfig
|
1055
|
-
}) => {
|
1056
|
-
if (specName === null) return [];
|
1057
|
-
if (specVersion === null) return [];
|
1058
|
-
return [[chainId, this.#balanceModules.filter(m => m.type.startsWith("substrate-")).map(({
|
1059
|
-
type: source
|
1060
|
-
}) => deriveMiniMetadataId({
|
1061
|
-
source,
|
1062
|
-
chainId: chainId,
|
1063
|
-
specName: specName,
|
1064
|
-
specVersion: specVersion,
|
1065
|
-
balancesConfig: JSON.stringify((balancesConfig ?? []).find(({
|
1066
|
-
moduleType
|
1067
|
-
}) => moduleType === source)?.moduleConfig ?? {})
|
1068
|
-
}))]];
|
1069
|
-
}));
|
1070
|
-
const statusesByChain = new Map(Array.from(wantedIdsByChain.entries()).map(([chainId, wantedIds]) => [chainId, wantedIds.every(wantedId => ids.includes(wantedId)) ? "good" : "none"]));
|
1071
|
-
return {
|
1072
|
-
wantedIdsByChain,
|
1073
|
-
statusesByChain
|
1074
|
-
};
|
1075
|
-
}
|
1076
|
-
async hydrateFromChaindata() {
|
1077
|
-
const now = Date.now();
|
1078
|
-
if (now - this.#lastHydratedMiniMetadatasAt < minimumHydrationInterval) return false;
|
1079
|
-
const dbHasMiniMetadatas = (await db.miniMetadatas.count()) > 0;
|
1080
|
-
try {
|
1081
|
-
try {
|
1082
|
-
// TODO: Move `fetchMiniMetadatas` into this package,
|
1083
|
-
// so that we don't have a circular import between `@talismn/balances` and `@talismn/chaindata-provider`.
|
1084
|
-
var miniMetadatas = await fetchMiniMetadatas(); // eslint-disable-line no-var
|
1085
|
-
if (miniMetadatas.length <= 0) throw new Error("Ignoring empty chaindata miniMetadatas response");
|
1086
|
-
} catch (error) {
|
1087
|
-
if (dbHasMiniMetadatas) throw error;
|
1088
|
-
// On first start-up (db is empty), if we fail to fetch miniMetadatas then we should
|
1089
|
-
// initialize the DB with the list of miniMetadatas inside our init/mini-metadatas.json file.
|
1090
|
-
// This data will represent a relatively recent copy of what's in chaindata,
|
1091
|
-
// which will be better for our users than to have nothing at all.
|
1092
|
-
var miniMetadatas = await fetchInitMiniMetadatas(); // eslint-disable-line no-var
|
1093
|
-
}
|
1094
|
-
await db.miniMetadatas.bulkPut(miniMetadatas);
|
1095
|
-
this.#lastHydratedMiniMetadatasAt = now;
|
1096
|
-
return true;
|
1097
|
-
} catch (error) {
|
1098
|
-
log.warn(`Failed to hydrate miniMetadatas from chaindata`, error);
|
1099
|
-
return false;
|
1100
|
-
}
|
1101
|
-
}
|
1102
|
-
async hydrateCustomChains() {
|
1103
|
-
const now = Date.now();
|
1104
|
-
if (now - this.#lastHydratedCustomChainsAt < minimumHydrationInterval) return false;
|
1105
|
-
const chains = await this.#chaindataProvider.chains();
|
1106
|
-
const customChains = chains.filter(chain => "isCustom" in chain && chain.isCustom);
|
1107
|
-
const updatedCustomChains = [];
|
1108
|
-
const concurrency = 4;
|
1109
|
-
(await PromisePool.withConcurrency(concurrency).for(customChains).process(async customChain => {
|
1110
|
-
const send = (method, params) => this.#chainConnectors.substrate?.send(customChain.id, method, params);
|
1111
|
-
const [genesisHash, runtimeVersion, chainName, chainType] = await Promise.all([send("chain_getBlockHash", [0]), send("state_getRuntimeVersion", []), send("system_chain", []), send("system_chainType", [])]);
|
1112
|
-
|
1113
|
-
// deconstruct rpc data
|
1114
|
-
const {
|
1115
|
-
specName,
|
1116
|
-
implName
|
1117
|
-
} = runtimeVersion;
|
1118
|
-
const specVersion = String(runtimeVersion.specVersion);
|
1119
|
-
const changed = customChain.genesisHash !== genesisHash || customChain.chainName !== chainName || !isEqual(customChain.chainType, chainType) || customChain.implName !== implName || customChain.specName !== specName || customChain.specVersion !== specVersion;
|
1120
|
-
if (!changed) return;
|
1121
|
-
customChain.genesisHash = genesisHash;
|
1122
|
-
customChain.chainName = chainName;
|
1123
|
-
customChain.chainType = chainType;
|
1124
|
-
customChain.implName = implName;
|
1125
|
-
customChain.specName = specName;
|
1126
|
-
customChain.specVersion = specVersion;
|
1127
|
-
updatedCustomChains.push(customChain);
|
1128
|
-
})).errors.forEach(error => log.error("Error hydrating custom chains", error));
|
1129
|
-
if (updatedCustomChains.length > 0) {
|
1130
|
-
await this.#chaindataProvider.transaction("rw", ["chains"], async () => {
|
1131
|
-
for (const updatedCustomChain of updatedCustomChains) {
|
1132
|
-
await this.#chaindataProvider.removeCustomChain(updatedCustomChain.id);
|
1133
|
-
await this.#chaindataProvider.addCustomChain(updatedCustomChain);
|
1134
|
-
}
|
1135
|
-
});
|
1136
|
-
}
|
1137
|
-
if (updatedCustomChains.length > 0) this.#lastHydratedCustomChainsAt = now;
|
1138
|
-
return true;
|
1139
|
-
}
|
1140
|
-
async updateSubstrateChains(chainIds) {
|
1141
|
-
const chains = new Map((await this.#chaindataProvider.chains()).map(chain => [chain.id, chain]));
|
1142
|
-
const filteredChains = chainIds.flatMap(chainId => chains.get(chainId) ?? []);
|
1143
|
-
const ids = await db.miniMetadatas.orderBy("id").primaryKeys();
|
1144
|
-
const {
|
1145
|
-
wantedIdsByChain,
|
1146
|
-
statusesByChain
|
1147
|
-
} = await this.statuses(filteredChains);
|
1148
|
-
|
1149
|
-
// clean up store
|
1150
|
-
const wantedIds = Array.from(wantedIdsByChain.values()).flatMap(ids => ids);
|
1151
|
-
const unwantedIds = ids.filter(id => !wantedIds.includes(id));
|
1152
|
-
if (unwantedIds.length > 0) {
|
1153
|
-
const chainIds = Array.from(new Set((await db.miniMetadatas.bulkGet(unwantedIds)).map(m => m?.chainId)));
|
1154
|
-
log.info(`Pruning ${unwantedIds.length} miniMetadatas on chains ${chainIds.join(", ")}`);
|
1155
|
-
await db.miniMetadatas.bulkDelete(unwantedIds);
|
1156
|
-
}
|
1157
|
-
const needUpdates = Array.from(statusesByChain.entries()).filter(([, status]) => status !== "good").map(([chainId]) => chainId);
|
1158
|
-
if (needUpdates.length > 0) log.info(`${needUpdates.length} miniMetadatas need updates (${needUpdates.join(", ")})`);
|
1159
|
-
const availableTokenLogos = await availableTokenLogoFilenames().catch(error => {
|
1160
|
-
log.error("Failed to fetch available token logos", error);
|
1161
|
-
return [];
|
1162
|
-
});
|
1163
|
-
const concurrency = 12;
|
1164
|
-
(await PromisePool.withConcurrency(concurrency).for(needUpdates).process(async chainId => {
|
1165
|
-
log.info(`Updating metadata for chain ${chainId}`);
|
1166
|
-
const chain = chains.get(chainId);
|
1167
|
-
if (!chain) return;
|
1168
|
-
const {
|
1169
|
-
specName,
|
1170
|
-
specVersion
|
1171
|
-
} = chain;
|
1172
|
-
if (specName === null) return;
|
1173
|
-
if (specVersion === null) return;
|
1174
|
-
const fetchMetadata = async () => {
|
1175
|
-
try {
|
1176
|
-
return await fetchBestMetadata((method, params, isCacheable) => {
|
1177
|
-
if (!this.#chainConnectors.substrate) throw new Error("Substrate connector is not available");
|
1178
|
-
return this.#chainConnectors.substrate.send(chainId, method, params, isCacheable);
|
1179
|
-
}, true // allow v14 fallback
|
1180
|
-
);
|
1181
|
-
} catch (err) {
|
1182
|
-
log.warn(`Failed to fetch metadata for chain ${chainId}`);
|
1183
|
-
return undefined;
|
1184
|
-
}
|
1185
|
-
};
|
1186
|
-
const [metadataRpc, systemProperties] = await Promise.all([fetchMetadata(), this.#chainConnectors.substrate?.send(chainId, "system_properties", [])]);
|
1187
|
-
for (const mod of this.#balanceModules.filter(m => m.type.startsWith("substrate-"))) {
|
1188
|
-
const balancesConfig = (chain.balancesConfig ?? []).find(({
|
1189
|
-
moduleType
|
1190
|
-
}) => moduleType === mod.type);
|
1191
|
-
const moduleConfig = balancesConfig?.moduleConfig ?? {};
|
1192
|
-
const chainMeta = await mod.fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc, systemProperties);
|
1193
|
-
const tokens = await mod.fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig);
|
1194
|
-
|
1195
|
-
// update tokens in chaindata
|
1196
|
-
await this.#chaindataProvider.updateChainTokens(chainId, mod.type, Object.values(tokens), availableTokenLogos);
|
1197
|
-
|
1198
|
-
// update miniMetadatas
|
1199
|
-
const {
|
1200
|
-
miniMetadata: data,
|
1201
|
-
metadataVersion: version,
|
1202
|
-
...extra
|
1203
|
-
} = chainMeta ?? {};
|
1204
|
-
await db.miniMetadatas.put({
|
1205
|
-
id: deriveMiniMetadataId({
|
1206
|
-
source: mod.type,
|
1207
|
-
chainId,
|
1208
|
-
specName,
|
1209
|
-
specVersion,
|
1210
|
-
balancesConfig: JSON.stringify(moduleConfig)
|
1211
|
-
}),
|
1212
|
-
source: mod.type,
|
1213
|
-
chainId,
|
1214
|
-
specName,
|
1215
|
-
specVersion,
|
1216
|
-
balancesConfig: JSON.stringify(moduleConfig),
|
1217
|
-
// TODO: Standardise return value from `fetchSubstrateChainMeta`
|
1218
|
-
version,
|
1219
|
-
data,
|
1220
|
-
extra: JSON.stringify(extra)
|
1221
|
-
});
|
1222
|
-
}
|
1223
|
-
})).errors.forEach(error => log.error("Error updating chain metadata", error));
|
1224
|
-
}
|
1225
|
-
}
|
947
|
+
const TokenConfigBaseSchema = TokenBaseSchema.partial().omit({
|
948
|
+
id: true
|
949
|
+
});
|
1226
950
|
|
1227
951
|
const erc20Abi = [{
|
1228
952
|
constant: true,
|
@@ -1454,8 +1178,11 @@ const erc20Abi = [{
|
|
1454
1178
|
|
1455
1179
|
const erc20BalancesAggregatorAbi = parseAbi(["struct AccountToken {address account; address token;}", "function balances(AccountToken[] memory accountTokens) public view returns (uint256[] memory)"]);
|
1456
1180
|
|
1457
|
-
const moduleType$
|
1458
|
-
const
|
1181
|
+
const moduleType$7 = "evm-erc20";
|
1182
|
+
const EvmErc20TokenConfigSchema = z.strictObject({
|
1183
|
+
contractAddress: EvmErc20TokenSchema.shape.contractAddress,
|
1184
|
+
...TokenConfigBaseSchema.shape
|
1185
|
+
});
|
1459
1186
|
const EvmErc20Module = hydrate => {
|
1460
1187
|
const {
|
1461
1188
|
chainConnectors,
|
@@ -1479,38 +1206,36 @@ const EvmErc20Module = hydrate => {
|
|
1479
1206
|
}, {});
|
1480
1207
|
};
|
1481
1208
|
const getModuleTokens = async () => {
|
1482
|
-
return await chaindataProvider.
|
1209
|
+
return await chaindataProvider.getTokensMapById(moduleType$7);
|
1483
1210
|
};
|
1484
1211
|
const getErc20Aggregators = async () => {
|
1485
|
-
const evmNetworks = await chaindataProvider.
|
1486
|
-
return Object.fromEntries(evmNetworks.filter(n => n.
|
1212
|
+
const evmNetworks = await chaindataProvider.getNetworks("ethereum");
|
1213
|
+
return Object.fromEntries(evmNetworks.filter(n => n.contracts?.Erc20Aggregator).map(n => [n.id, n.contracts.Erc20Aggregator]));
|
1487
1214
|
};
|
1488
1215
|
return {
|
1489
|
-
...DefaultBalanceModule(moduleType$
|
1216
|
+
...DefaultBalanceModule(moduleType$7),
|
1490
1217
|
/**
|
1491
1218
|
* This method is currently executed on [a squid](https://github.com/TalismanSociety/chaindata-squid/blob/0ee02818bf5caa7362e3f3664e55ef05ec8df078/src/steps/updateEvmNetworksFromGithub.ts#L280-L284).
|
1492
1219
|
* In a future version of the balance libraries, we may build some kind of async scheduling system which will keep the chainmeta for each chain up to date without relying on a squid.
|
1493
1220
|
*/
|
1494
|
-
async fetchEvmChainMeta(
|
1495
|
-
const isTestnet = (await chaindataProvider.evmNetworkById(chainId))?.isTestnet || false;
|
1221
|
+
async fetchEvmChainMeta(_chainId) {
|
1496
1222
|
return {
|
1497
|
-
|
1223
|
+
miniMetadata: null,
|
1224
|
+
extra: null
|
1498
1225
|
};
|
1499
1226
|
},
|
1500
1227
|
/**
|
1501
1228
|
* This method is currently executed on [a squid](https://github.com/TalismanSociety/chaindata-squid/blob/0ee02818bf5caa7362e3f3664e55ef05ec8df078/src/steps/updateEvmNetworksFromGithub.ts#L338-L343).
|
1502
1229
|
* In a future version of the balance libraries, we may build some kind of async scheduling system which will keep the list of tokens for each chain up to date without relying on a squid.
|
1503
1230
|
*/
|
1504
|
-
async fetchEvmChainTokens(chainId,
|
1505
|
-
const {
|
1506
|
-
isTestnet
|
1507
|
-
} = chainMeta;
|
1231
|
+
async fetchEvmChainTokens(chainId, _chainMeta, moduleConfig, tokens) {
|
1508
1232
|
const chainTokens = {};
|
1509
|
-
for (const tokenConfig of
|
1233
|
+
for (const tokenConfig of tokens ?? []) {
|
1510
1234
|
const {
|
1511
1235
|
contractAddress,
|
1512
1236
|
symbol: contractSymbol,
|
1513
|
-
decimals: contractDecimals
|
1237
|
+
decimals: contractDecimals,
|
1238
|
+
name: contractName
|
1514
1239
|
} = tokenConfig;
|
1515
1240
|
// TODO : in chaindata's build, filter out all tokens that don't have any of these
|
1516
1241
|
if (!contractAddress || !contractSymbol || contractDecimals === undefined) {
|
@@ -1524,22 +1249,25 @@ const EvmErc20Module = hydrate => {
|
|
1524
1249
|
const token = {
|
1525
1250
|
id,
|
1526
1251
|
type: "evm-erc20",
|
1527
|
-
|
1252
|
+
platform: "ethereum",
|
1528
1253
|
isDefault: tokenConfig.isDefault ?? true,
|
1529
1254
|
symbol,
|
1530
1255
|
decimals,
|
1531
|
-
|
1256
|
+
name: contractName ?? symbol,
|
1257
|
+
logo: tokenConfig?.logo,
|
1532
1258
|
contractAddress,
|
1533
|
-
|
1534
|
-
id: chainId
|
1535
|
-
}
|
1259
|
+
networkId: chainId
|
1536
1260
|
};
|
1537
1261
|
if (tokenConfig?.symbol) token.symbol = tokenConfig?.symbol;
|
1538
1262
|
if (tokenConfig?.coingeckoId) token.coingeckoId = tokenConfig?.coingeckoId;
|
1539
|
-
if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName;
|
1540
1263
|
if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf;
|
1541
1264
|
if (tokenConfig?.noDiscovery) token.noDiscovery = tokenConfig?.noDiscovery;
|
1542
|
-
|
1265
|
+
const validation = EvmErc20TokenSchema.safeParse(token);
|
1266
|
+
if (validation.success) {
|
1267
|
+
chainTokens[token.id] = token;
|
1268
|
+
} else {
|
1269
|
+
log.warn("Ignoring invalid token", token.id, validation.error.message, validation.error.issues);
|
1270
|
+
}
|
1543
1271
|
}
|
1544
1272
|
return chainTokens;
|
1545
1273
|
},
|
@@ -1551,18 +1279,18 @@ const EvmErc20Module = hydrate => {
|
|
1551
1279
|
const subscriptionInterval = 6_000; // 6_000ms == 6 seconds
|
1552
1280
|
const initDelay = 1_500; // 1_500ms == 1.5 seconds
|
1553
1281
|
const initialisingBalances = new Set();
|
1554
|
-
const positiveBalanceNetworks = new Set(initialBalances?.map(b => b.
|
1282
|
+
const positiveBalanceNetworks = new Set(initialBalances?.map(b => b.networkId));
|
1555
1283
|
const tokens = await getModuleTokens();
|
1556
1284
|
|
1557
1285
|
// for chains with a zero balance we only call fetchBalances once every 5 subscriptionIntervals
|
1558
1286
|
// if subscriptionInterval is 6 seconds, this means we only poll chains with a zero balance every 30 seconds
|
1559
1287
|
let zeroBalanceSubscriptionIntervalCounter = 0;
|
1560
|
-
const evmNetworks = await chaindataProvider.
|
1288
|
+
const evmNetworks = await chaindataProvider.getNetworksMapById("ethereum");
|
1561
1289
|
const ethAddressesByToken = Object.fromEntries(Object.entries(addressesByToken).map(([tokenId, addresses]) => {
|
1562
1290
|
const ethAddresses = addresses.filter(isEthereumAddress);
|
1563
1291
|
if (ethAddresses.length === 0) return null;
|
1564
1292
|
const token = tokens[tokenId];
|
1565
|
-
const evmNetworkId = token.
|
1293
|
+
const evmNetworkId = token.networkId;
|
1566
1294
|
if (!evmNetworkId) return null;
|
1567
1295
|
return [tokenId, ethAddresses];
|
1568
1296
|
}).filter(x => Boolean(x)));
|
@@ -1688,20 +1416,17 @@ const fetchBalances$3 = async (evmChainConnector, tokenAddressesByNetwork, erc20
|
|
1688
1416
|
results: [],
|
1689
1417
|
errors: []
|
1690
1418
|
};
|
1691
|
-
await Promise.all(Object.entries(tokenAddressesByNetwork).map(async ([
|
1692
|
-
const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(
|
1693
|
-
if (!publicClient) throw new EvmErc20NetworkError(`Could not get rpc provider for evm network ${
|
1694
|
-
const balances = await getEvmTokenBalances(publicClient, networkParams, result.errors, erc20Aggregators[
|
1419
|
+
await Promise.all(Object.entries(tokenAddressesByNetwork).map(async ([networkId, networkParams]) => {
|
1420
|
+
const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(networkId);
|
1421
|
+
if (!publicClient) throw new EvmErc20NetworkError(`Could not get rpc provider for evm network ${networkId}`, networkId);
|
1422
|
+
const balances = await getEvmTokenBalances(publicClient, networkParams, result.errors, erc20Aggregators[networkId]);
|
1695
1423
|
|
1696
1424
|
// consider only non null balances in the results
|
1697
1425
|
result.results.push(...balances.filter(isTruthy).map((free, i) => ({
|
1698
1426
|
source: "evm-erc20",
|
1699
1427
|
status: "live",
|
1700
1428
|
address: networkParams[i].address,
|
1701
|
-
|
1702
|
-
evmChainId: evmNetworkId
|
1703
|
-
},
|
1704
|
-
evmNetworkId,
|
1429
|
+
networkId,
|
1705
1430
|
tokenId: networkParams[i].token.id,
|
1706
1431
|
value: free
|
1707
1432
|
})));
|
@@ -1772,7 +1497,7 @@ function groupAddressesByTokenByEvmNetwork$1(addressesByToken, tokens) {
|
|
1772
1497
|
log.error(`Token ${tokenId} not found`);
|
1773
1498
|
return byChain;
|
1774
1499
|
}
|
1775
|
-
const chainId = token.
|
1500
|
+
const chainId = token.networkId;
|
1776
1501
|
if (!chainId) {
|
1777
1502
|
log.error(`Token ${tokenId} has no evm network`);
|
1778
1503
|
return byChain;
|
@@ -1785,67 +1510,38 @@ function groupAddressesByTokenByEvmNetwork$1(addressesByToken, tokens) {
|
|
1785
1510
|
|
1786
1511
|
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)"]);
|
1787
1512
|
|
1788
|
-
const moduleType$
|
1789
|
-
const
|
1790
|
-
const getEvmNetworkIdFromTokenId = tokenId => {
|
1791
|
-
const evmNetworkId = tokenId.split("-")[0];
|
1792
|
-
if (!evmNetworkId) throw new Error(`Can't detect chainId for token ${tokenId}`);
|
1793
|
-
return evmNetworkId;
|
1794
|
-
};
|
1513
|
+
const moduleType$6 = "evm-native";
|
1514
|
+
const EvmNativeTokenConfigSchema = TokenConfigBaseSchema;
|
1795
1515
|
const EvmNativeModule = hydrate => {
|
1796
1516
|
const {
|
1797
1517
|
chainConnectors,
|
1798
1518
|
chaindataProvider
|
1799
1519
|
} = hydrate;
|
1800
1520
|
const getModuleTokens = async () => {
|
1801
|
-
return await chaindataProvider.
|
1521
|
+
return await chaindataProvider.getTokensMapById(moduleType$6);
|
1802
1522
|
};
|
1803
1523
|
return {
|
1804
|
-
...DefaultBalanceModule(moduleType$
|
1524
|
+
...DefaultBalanceModule(moduleType$6),
|
1805
1525
|
get tokens() {
|
1806
|
-
return chaindataProvider.
|
1526
|
+
return chaindataProvider.getTokensMapById(moduleType$6);
|
1807
1527
|
},
|
1808
1528
|
/**
|
1809
1529
|
* This method is currently executed on [a squid](https://github.com/TalismanSociety/chaindata-squid/blob/0ee02818bf5caa7362e3f3664e55ef05ec8df078/src/steps/updateEvmNetworksFromGithub.ts#L280-L284).
|
1810
1530
|
* In a future version of the balance libraries, we may build some kind of async scheduling system which will keep the chainmeta for each chain up to date without relying on a squid.
|
1811
1531
|
*/
|
1812
|
-
async fetchEvmChainMeta(
|
1813
|
-
const isTestnet = (await chaindataProvider.evmNetworkById(chainId))?.isTestnet || false;
|
1532
|
+
async fetchEvmChainMeta(_chainId) {
|
1814
1533
|
return {
|
1815
|
-
|
1534
|
+
miniMetadata: null,
|
1535
|
+
extra: null
|
1816
1536
|
};
|
1817
1537
|
},
|
1818
1538
|
/**
|
1819
1539
|
* This method is currently executed on [a squid](https://github.com/TalismanSociety/chaindata-squid/blob/0ee02818bf5caa7362e3f3664e55ef05ec8df078/src/steps/updateEvmNetworksFromGithub.ts#L338-L343).
|
1820
1540
|
* In a future version of the balance libraries, we may build some kind of async scheduling system which will keep the list of tokens for each chain up to date without relying on a squid.
|
1821
1541
|
*/
|
1822
|
-
async fetchEvmChainTokens(
|
1823
|
-
|
1824
|
-
|
1825
|
-
} = chainMeta;
|
1826
|
-
const symbol = moduleConfig?.symbol ?? "ETH";
|
1827
|
-
const decimals = typeof moduleConfig?.decimals === "number" ? moduleConfig.decimals : 18;
|
1828
|
-
const id = evmNativeTokenId(chainId);
|
1829
|
-
const nativeToken = {
|
1830
|
-
id,
|
1831
|
-
type: "evm-native",
|
1832
|
-
isTestnet,
|
1833
|
-
isDefault: true,
|
1834
|
-
symbol,
|
1835
|
-
decimals,
|
1836
|
-
logo: moduleConfig?.logo || githubTokenLogoUrl(id),
|
1837
|
-
evmNetwork: {
|
1838
|
-
id: chainId
|
1839
|
-
}
|
1840
|
-
};
|
1841
|
-
if (moduleConfig?.symbol) nativeToken.symbol = moduleConfig?.symbol;
|
1842
|
-
if (moduleConfig?.coingeckoId) nativeToken.coingeckoId = moduleConfig?.coingeckoId;
|
1843
|
-
if (moduleConfig?.dcentName) nativeToken.dcentName = moduleConfig?.dcentName;
|
1844
|
-
if (moduleConfig?.mirrorOf) nativeToken.mirrorOf = moduleConfig?.mirrorOf;
|
1845
|
-
if (moduleConfig?.noDiscovery) nativeToken.noDiscovery = moduleConfig?.noDiscovery;
|
1846
|
-
return {
|
1847
|
-
[nativeToken.id]: nativeToken
|
1848
|
-
};
|
1542
|
+
async fetchEvmChainTokens() {
|
1543
|
+
// token is currently generated in chaindata from the EthNetworkConfig["nativeCurrency"] field
|
1544
|
+
return {};
|
1849
1545
|
},
|
1850
1546
|
async subscribeBalances({
|
1851
1547
|
addressesByToken,
|
@@ -1856,11 +1552,11 @@ const EvmNativeModule = hydrate => {
|
|
1856
1552
|
const initDelay = 500; // 500ms == 0.5 seconds
|
1857
1553
|
|
1858
1554
|
const tokens = await getModuleTokens();
|
1859
|
-
const ethAddressesByToken =
|
1555
|
+
const ethAddressesByToken = fromPairs(toPairs(addressesByToken).map(([tokenId, addresses]) => {
|
1860
1556
|
const ethAddresses = addresses.filter(isEthereumAddress);
|
1861
1557
|
if (ethAddresses.length === 0) return null;
|
1862
1558
|
const token = tokens[tokenId];
|
1863
|
-
const evmNetworkId = token.
|
1559
|
+
const evmNetworkId = token.networkId;
|
1864
1560
|
if (!evmNetworkId) return null;
|
1865
1561
|
return [tokenId, ethAddresses];
|
1866
1562
|
}).filter(x => Boolean(x)));
|
@@ -1870,16 +1566,16 @@ const EvmNativeModule = hydrate => {
|
|
1870
1566
|
let zeroBalanceSubscriptionIntervalCounter = 0;
|
1871
1567
|
|
1872
1568
|
// setup initialising balances for all active evm networks
|
1873
|
-
const activeEvmNetworkIds =
|
1569
|
+
const activeEvmNetworkIds = keys(ethAddressesByToken).map(networkIdFromTokenId);
|
1874
1570
|
const initialisingBalances = new Set(activeEvmNetworkIds);
|
1875
|
-
const positiveBalanceNetworks = new Set(initialBalances?.map(b => b.
|
1571
|
+
const positiveBalanceNetworks = new Set(initialBalances?.map(b => b.networkId));
|
1876
1572
|
const poll = async () => {
|
1877
1573
|
if (!subscriptionActive) return;
|
1878
1574
|
zeroBalanceSubscriptionIntervalCounter = (zeroBalanceSubscriptionIntervalCounter + 1) % 5;
|
1879
1575
|
try {
|
1880
1576
|
// fetch balance for each network sequentially to prevent creating a big queue of http requests (browser can only handle 2 at a time)
|
1881
1577
|
for (const [tokenId, addresses] of Object.entries(ethAddressesByToken)) {
|
1882
|
-
const evmNetworkId =
|
1578
|
+
const evmNetworkId = networkIdFromTokenId(tokenId);
|
1883
1579
|
|
1884
1580
|
// a zero balance network is one that has initialised and does not have a positive balance
|
1885
1581
|
const isZeroBalanceNetwork = !initialisingBalances.has(evmNetworkId) && !positiveBalanceNetworks.has(evmNetworkId);
|
@@ -1899,10 +1595,10 @@ const EvmNativeModule = hydrate => {
|
|
1899
1595
|
log.error(balance.message, balance.networkId);
|
1900
1596
|
initialisingBalances.delete(balance.networkId);
|
1901
1597
|
} else {
|
1902
|
-
if (balance.
|
1903
|
-
initialisingBalances.delete(balance.
|
1598
|
+
if (balance.networkId) {
|
1599
|
+
initialisingBalances.delete(balance.networkId);
|
1904
1600
|
if (BigInt(balance.value) > 0n) {
|
1905
|
-
positiveBalanceNetworks.add(balance.
|
1601
|
+
positiveBalanceNetworks.add(balance.networkId);
|
1906
1602
|
}
|
1907
1603
|
resultBalances.push(balance);
|
1908
1604
|
}
|
@@ -1952,23 +1648,20 @@ const fetchBalances$2 = async (evmChainConnector, addressesByToken, tokens) => {
|
|
1952
1648
|
if (!evmChainConnector) throw new Error(`This module requires an evm chain connector`);
|
1953
1649
|
return Promise.all(Object.entries(addressesByToken).map(async ([tokenId, addresses]) => {
|
1954
1650
|
const token = tokens[tokenId];
|
1955
|
-
const
|
1956
|
-
if (!
|
1957
|
-
const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(
|
1958
|
-
if (!publicClient) throw new Error(`Could not get rpc provider for evm network ${
|
1651
|
+
const networkId = token.networkId;
|
1652
|
+
if (!networkId) throw new Error(`Token ${token.id} has no evm network`);
|
1653
|
+
const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(networkId);
|
1654
|
+
if (!publicClient) throw new Error(`Could not get rpc provider for evm network ${networkId}`);
|
1959
1655
|
|
1960
1656
|
// fetch all balances
|
1961
1657
|
const freeBalances = await getFreeBalances(publicClient, addresses);
|
1962
1658
|
const balanceResults = addresses.map((address, i) => {
|
1963
|
-
if (freeBalances[i] === "error") return new EvmNativeBalanceError("Could not fetch balance ",
|
1659
|
+
if (freeBalances[i] === "error") return new EvmNativeBalanceError("Could not fetch balance ", networkId);
|
1964
1660
|
return {
|
1965
1661
|
source: "evm-native",
|
1966
1662
|
status: "live",
|
1967
1663
|
address: address,
|
1968
|
-
|
1969
|
-
evmChainId: evmNetworkId
|
1970
|
-
},
|
1971
|
-
evmNetworkId,
|
1664
|
+
networkId,
|
1972
1665
|
tokenId,
|
1973
1666
|
value: freeBalances[i].toString()
|
1974
1667
|
};
|
@@ -2021,7 +1714,7 @@ async function getFreeBalances(publicClient, addresses) {
|
|
2021
1714
|
});
|
2022
1715
|
} catch (err) {
|
2023
1716
|
const errorMessage = hasOwnProperty(err, "shortMessage") ? err.shortMessage : hasOwnProperty(err, "message") ? err.message : err;
|
2024
|
-
log.warn(`Failed to get balance from chain ${publicClient.chain
|
1717
|
+
log.warn(`Failed to get balance from chain ${publicClient.chain.id} for ${ethAddresses.length} addresses: ${errorMessage}`);
|
2025
1718
|
return ethAddresses.map(() => "error");
|
2026
1719
|
}
|
2027
1720
|
}
|
@@ -2589,8 +2282,20 @@ const uniswapV2PairAbi = [{
|
|
2589
2282
|
type: "function"
|
2590
2283
|
}];
|
2591
2284
|
|
2592
|
-
const moduleType$
|
2593
|
-
const
|
2285
|
+
const moduleType$5 = "evm-uniswapv2";
|
2286
|
+
const EvmUniswapV2TokenConfigSchema = z.strictObject({
|
2287
|
+
contractAddress: EvmUniswapV2TokenSchema.shape.contractAddress,
|
2288
|
+
...TokenConfigBaseSchema.shape,
|
2289
|
+
// on chaindata side these are fetched by a dedicated task
|
2290
|
+
symbol0: EvmUniswapV2TokenSchema.shape.symbol0.optional(),
|
2291
|
+
symbol1: EvmUniswapV2TokenSchema.shape.symbol1.optional(),
|
2292
|
+
decimals0: EvmUniswapV2TokenSchema.shape.decimals0.optional(),
|
2293
|
+
decimals1: EvmUniswapV2TokenSchema.shape.decimals1.optional(),
|
2294
|
+
tokenAddress0: EvmUniswapV2TokenSchema.shape.tokenAddress0.optional(),
|
2295
|
+
tokenAddress1: EvmUniswapV2TokenSchema.shape.tokenAddress1.optional(),
|
2296
|
+
coingeckoId0: EvmUniswapV2TokenSchema.shape.coingeckoId0.optional(),
|
2297
|
+
coingeckoId1: EvmUniswapV2TokenSchema.shape.coingeckoId1.optional()
|
2298
|
+
});
|
2594
2299
|
const EvmUniswapV2Module = hydrate => {
|
2595
2300
|
const {
|
2596
2301
|
chainConnectors,
|
@@ -2599,19 +2304,16 @@ const EvmUniswapV2Module = hydrate => {
|
|
2599
2304
|
const chainConnector = chainConnectors.evm;
|
2600
2305
|
assert(chainConnector, "This module requires an evm chain connector");
|
2601
2306
|
return {
|
2602
|
-
...DefaultBalanceModule(moduleType$
|
2603
|
-
async fetchEvmChainMeta(
|
2604
|
-
const isTestnet = (await chaindataProvider.evmNetworkById(chainId))?.isTestnet || false;
|
2307
|
+
...DefaultBalanceModule(moduleType$5),
|
2308
|
+
async fetchEvmChainMeta(_chainId) {
|
2605
2309
|
return {
|
2606
|
-
|
2310
|
+
miniMetadata: null,
|
2311
|
+
extra: null
|
2607
2312
|
};
|
2608
2313
|
},
|
2609
|
-
async fetchEvmChainTokens(chainId,
|
2610
|
-
const {
|
2611
|
-
isTestnet
|
2612
|
-
} = chainMeta;
|
2314
|
+
async fetchEvmChainTokens(chainId, _chainMeta, moduleConfig, pools) {
|
2613
2315
|
const tokens = {};
|
2614
|
-
for (const tokenConfig of
|
2316
|
+
for (const tokenConfig of pools ?? []) {
|
2615
2317
|
const {
|
2616
2318
|
contractAddress,
|
2617
2319
|
decimals,
|
@@ -2622,7 +2324,8 @@ const EvmUniswapV2Module = hydrate => {
|
|
2622
2324
|
tokenAddress0,
|
2623
2325
|
tokenAddress1,
|
2624
2326
|
coingeckoId0,
|
2625
|
-
coingeckoId1
|
2327
|
+
coingeckoId1,
|
2328
|
+
name
|
2626
2329
|
} = tokenConfig;
|
2627
2330
|
if (!contractAddress || decimals === undefined || symbol0 === undefined || decimals0 === undefined || symbol1 === undefined || decimals1 === undefined || tokenAddress0 === undefined || tokenAddress1 === undefined) {
|
2628
2331
|
log.warn("ignoring token on chain %s", chainId, tokenConfig);
|
@@ -2632,11 +2335,12 @@ const EvmUniswapV2Module = hydrate => {
|
|
2632
2335
|
const token = {
|
2633
2336
|
id,
|
2634
2337
|
type: "evm-uniswapv2",
|
2635
|
-
|
2338
|
+
platform: "ethereum",
|
2636
2339
|
isDefault: tokenConfig.isDefault ?? false,
|
2637
2340
|
symbol: `${symbol0 ?? "UNKNOWN"}/${symbol1 ?? "UNKNOWN"}`,
|
2341
|
+
name: name ?? `${symbol0 ?? "UNKNOWN"}/${symbol1 ?? "UNKNOWN"}`,
|
2638
2342
|
decimals,
|
2639
|
-
logo: tokenConfig?.logo ||
|
2343
|
+
logo: tokenConfig?.logo || getGithubTokenLogoUrl("uniswap"),
|
2640
2344
|
symbol0,
|
2641
2345
|
decimals0,
|
2642
2346
|
symbol1,
|
@@ -2646,13 +2350,10 @@ const EvmUniswapV2Module = hydrate => {
|
|
2646
2350
|
tokenAddress1,
|
2647
2351
|
coingeckoId0,
|
2648
2352
|
coingeckoId1,
|
2649
|
-
|
2650
|
-
id: chainId
|
2651
|
-
}
|
2353
|
+
networkId: chainId
|
2652
2354
|
};
|
2653
2355
|
if (tokenConfig?.symbol) token.symbol = tokenConfig?.symbol;
|
2654
2356
|
if (tokenConfig?.coingeckoId) token.coingeckoId = tokenConfig?.coingeckoId;
|
2655
|
-
if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName;
|
2656
2357
|
if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf;
|
2657
2358
|
if (tokenConfig?.noDiscovery) token.noDiscovery = tokenConfig?.noDiscovery;
|
2658
2359
|
tokens[token.id] = token;
|
@@ -2668,9 +2369,9 @@ const EvmUniswapV2Module = hydrate => {
|
|
2668
2369
|
const initDelay = 1_500; // 1_500ms == 1.5 seconds
|
2669
2370
|
|
2670
2371
|
const initialBalancesByNetwork = initialBalances?.reduce((result, b) => {
|
2671
|
-
if (!b.
|
2672
|
-
if (!result[b.
|
2673
|
-
result[b.
|
2372
|
+
if (!b.networkId) return result;
|
2373
|
+
if (!result[b.networkId]) result[b.networkId] = {};
|
2374
|
+
result[b.networkId][getBalanceId(b)] = b;
|
2674
2375
|
return result;
|
2675
2376
|
}, {});
|
2676
2377
|
const cache = new Map(Object.entries(initialBalancesByNetwork ?? {}));
|
@@ -2678,8 +2379,8 @@ const EvmUniswapV2Module = hydrate => {
|
|
2678
2379
|
// for chains with a zero balance we only call fetchBalances once every 5 subscriptionIntervals
|
2679
2380
|
// if subscriptionInterval is 6 seconds, this means we only poll chains with a zero balance every 30 seconds
|
2680
2381
|
let zeroBalanceSubscriptionIntervalCounter = 0;
|
2681
|
-
const evmNetworks = await chaindataProvider.
|
2682
|
-
const tokens = await chaindataProvider.
|
2382
|
+
const evmNetworks = await chaindataProvider.getNetworksMapById("ethereum");
|
2383
|
+
const tokens = await chaindataProvider.getTokensMapById();
|
2683
2384
|
const poll = async () => {
|
2684
2385
|
if (!subscriptionActive) return;
|
2685
2386
|
zeroBalanceSubscriptionIntervalCounter = (zeroBalanceSubscriptionIntervalCounter + 1) % 5;
|
@@ -2722,20 +2423,20 @@ const EvmUniswapV2Module = hydrate => {
|
|
2722
2423
|
},
|
2723
2424
|
async fetchBalances(addressesByToken) {
|
2724
2425
|
if (!chainConnectors.evm) throw new Error(`This module requires an evm chain connector`);
|
2725
|
-
const evmNetworks = await chaindataProvider.
|
2726
|
-
const tokens = await chaindataProvider.
|
2426
|
+
const evmNetworks = await chaindataProvider.getNetworksMapById("ethereum");
|
2427
|
+
const tokens = await chaindataProvider.getTokensMapById();
|
2727
2428
|
return fetchBalances$1(chainConnectors.evm, evmNetworks, tokens, addressesByToken);
|
2728
2429
|
}
|
2729
2430
|
};
|
2730
2431
|
};
|
2731
2432
|
const fetchBalances$1 = async (evmChainConnector, evmNetworks, tokens, addressesByToken) => {
|
2732
2433
|
const addressesByTokenGroupedByEvmNetwork = groupAddressesByTokenByEvmNetwork(addressesByToken, tokens);
|
2733
|
-
const balances = (await Promise.allSettled(Object.entries(addressesByTokenGroupedByEvmNetwork).map(async ([
|
2434
|
+
const balances = (await Promise.allSettled(Object.entries(addressesByTokenGroupedByEvmNetwork).map(async ([networkId, addressesByToken]) => {
|
2734
2435
|
if (!evmChainConnector) throw new Error(`This module requires an evm chain connector`);
|
2735
|
-
const evmNetwork = evmNetworks[
|
2736
|
-
if (!evmNetwork) throw new Error(`Evm network ${
|
2737
|
-
const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(
|
2738
|
-
if (!publicClient) throw new Error(`Could not get rpc provider for evm network ${
|
2436
|
+
const evmNetwork = evmNetworks[networkId];
|
2437
|
+
if (!evmNetwork) throw new Error(`Evm network ${networkId} not found`);
|
2438
|
+
const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(networkId);
|
2439
|
+
if (!publicClient) throw new Error(`Could not get rpc provider for evm network ${networkId}`);
|
2739
2440
|
const tokensAndAddresses = Object.entries(addressesByToken).reduce((tokensAndAddresses, [tokenId, addresses]) => {
|
2740
2441
|
const token = tokens[tokenId];
|
2741
2442
|
if (!token) {
|
@@ -2757,10 +2458,7 @@ const fetchBalances$1 = async (evmChainConnector, evmNetworks, tokens, addresses
|
|
2757
2458
|
source: "evm-uniswapv2",
|
2758
2459
|
status: "live",
|
2759
2460
|
address: address,
|
2760
|
-
|
2761
|
-
evmChainId: evmNetwork.id
|
2762
|
-
},
|
2763
|
-
evmNetworkId,
|
2461
|
+
networkId,
|
2764
2462
|
tokenId: token.id,
|
2765
2463
|
values: await getPoolBalance(publicClient, token.contractAddress, address)
|
2766
2464
|
}));
|
@@ -2796,7 +2494,7 @@ function groupAddressesByTokenByEvmNetwork(addressesByToken, tokens) {
|
|
2796
2494
|
log.error(`Token ${tokenId} not found`);
|
2797
2495
|
return byChain;
|
2798
2496
|
}
|
2799
|
-
const chainId = token.
|
2497
|
+
const chainId = token.networkId;
|
2800
2498
|
if (!chainId) {
|
2801
2499
|
log.error(`Token ${tokenId} has no evm network`);
|
2802
2500
|
return byChain;
|
@@ -2871,6 +2569,170 @@ async function getPoolBalance(publicClient, contractAddress, accountAddress) {
|
|
2871
2569
|
}
|
2872
2570
|
}
|
2873
2571
|
|
2572
|
+
const libVersion = pkg.version;
|
2573
|
+
|
2574
|
+
// cache the promise so it can be shared across multiple calls
|
2575
|
+
const CACHE_GET_SPEC_VERSION = new Map();
|
2576
|
+
const fetchSpecVersion = async (chainConnector, networkId) => {
|
2577
|
+
const {
|
2578
|
+
specVersion
|
2579
|
+
} = await chainConnector.send(networkId, "state_getRuntimeVersion", [], true);
|
2580
|
+
return specVersion;
|
2581
|
+
};
|
2582
|
+
|
2583
|
+
/**
|
2584
|
+
* fetches the spec version of a network. current request is cached in case of multiple calls (all balance modules will kick in at once)
|
2585
|
+
*/
|
2586
|
+
const getSpecVersion = async (chainConnector, networkId) => {
|
2587
|
+
if (CACHE_GET_SPEC_VERSION.has(networkId)) return CACHE_GET_SPEC_VERSION.get(networkId);
|
2588
|
+
const pResult = fetchSpecVersion(chainConnector, networkId);
|
2589
|
+
CACHE_GET_SPEC_VERSION.set(networkId, pResult);
|
2590
|
+
try {
|
2591
|
+
return await pResult;
|
2592
|
+
} catch (cause) {
|
2593
|
+
throw new Error(`Failed to fetch specVersion for network ${networkId}`, {
|
2594
|
+
cause
|
2595
|
+
});
|
2596
|
+
} finally {
|
2597
|
+
CACHE_GET_SPEC_VERSION.delete(networkId);
|
2598
|
+
}
|
2599
|
+
};
|
2600
|
+
|
2601
|
+
// share requests as all modules will call this at once
|
2602
|
+
const CACHE$1 = new Map();
|
2603
|
+
const getMetadataRpc = async (chainConnector, networkId) => {
|
2604
|
+
if (CACHE$1.has(networkId)) return CACHE$1.get(networkId);
|
2605
|
+
const pResult = fetchBestMetadata((method, params, isCacheable) => chainConnector.send(networkId, method, params, isCacheable, {
|
2606
|
+
expectErrors: true
|
2607
|
+
}), true // allow fallback to 14 as modules dont use any v15 or v16 specifics yet
|
2608
|
+
);
|
2609
|
+
CACHE$1.set(networkId, pResult);
|
2610
|
+
try {
|
2611
|
+
return await pResult;
|
2612
|
+
} catch (cause) {
|
2613
|
+
if (isAbortError(cause)) throw cause;
|
2614
|
+
throw new Error(`Failed to fetch metadataRpc for network ${networkId}`, {
|
2615
|
+
cause
|
2616
|
+
});
|
2617
|
+
} finally {
|
2618
|
+
CACHE$1.delete(networkId);
|
2619
|
+
}
|
2620
|
+
};
|
2621
|
+
|
2622
|
+
// share requests as all modules will call this at once
|
2623
|
+
const CACHE = new Map();
|
2624
|
+
|
2625
|
+
// ensures we dont fetch miniMetadatas on more than 4 chains at once
|
2626
|
+
const POOL = new PQueue({
|
2627
|
+
concurrency: 4
|
2628
|
+
});
|
2629
|
+
const getMiniMetadatas = async (chainConnector, chaindataProvider, networkId, specVersion, signal) => {
|
2630
|
+
if (CACHE.has(networkId)) return CACHE.get(networkId);
|
2631
|
+
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
|
2632
|
+
);
|
2633
|
+
const pResult = POOL.add(() => fetchMiniMetadatas(chainConnector, chaindataProvider, networkId, specVersion), {
|
2634
|
+
signal
|
2635
|
+
});
|
2636
|
+
CACHE.set(networkId, pResult);
|
2637
|
+
try {
|
2638
|
+
return await pResult;
|
2639
|
+
} catch (cause) {
|
2640
|
+
if (isAbortError(cause)) throw cause;
|
2641
|
+
throw new Error(`Failed to fetch metadataRpc for network ${networkId}`, {
|
2642
|
+
cause
|
2643
|
+
});
|
2644
|
+
} finally {
|
2645
|
+
CACHE.delete(networkId);
|
2646
|
+
}
|
2647
|
+
};
|
2648
|
+
const DotBalanceModuleTypeSchema = z.keyof(DotNetworkBalancesConfigSchema);
|
2649
|
+
const fetchMiniMetadatas = async (chainConnector, chaindataProvider, chainId, specVersion, signal) => {
|
2650
|
+
const start = performance.now();
|
2651
|
+
log.info("[miniMetadata] fetching minimetadatas for %s", chainId);
|
2652
|
+
try {
|
2653
|
+
const metadataRpc = await getMetadataRpc(chainConnector, chainId);
|
2654
|
+
signal?.throwIfAborted();
|
2655
|
+
const chainConnectors = {
|
2656
|
+
substrate: chainConnector,
|
2657
|
+
evm: {} // wont be used but workarounds error for module creation
|
2658
|
+
};
|
2659
|
+
const modules = defaultBalanceModules.map(mod => mod({
|
2660
|
+
chainConnectors,
|
2661
|
+
chaindataProvider
|
2662
|
+
})).filter(mod => DotBalanceModuleTypeSchema.safeParse(mod.type).success);
|
2663
|
+
return Promise.all(modules.map(async mod => {
|
2664
|
+
const source = mod.type;
|
2665
|
+
const chain = await chaindataProvider.getNetworkById(chainId, "polkadot");
|
2666
|
+
const balancesConfig = chain?.balancesConfig?.[mod.type];
|
2667
|
+
const chainMeta = await mod.fetchSubstrateChainMeta(chainId, balancesConfig,
|
2668
|
+
// TODO better typing
|
2669
|
+
metadataRpc);
|
2670
|
+
return {
|
2671
|
+
id: deriveMiniMetadataId({
|
2672
|
+
source,
|
2673
|
+
chainId,
|
2674
|
+
specVersion,
|
2675
|
+
libVersion
|
2676
|
+
}),
|
2677
|
+
source,
|
2678
|
+
chainId,
|
2679
|
+
specVersion,
|
2680
|
+
libVersion,
|
2681
|
+
data: chainMeta?.miniMetadata ?? null,
|
2682
|
+
extra: chainMeta?.extra ?? null
|
2683
|
+
};
|
2684
|
+
}));
|
2685
|
+
} finally {
|
2686
|
+
log.debug("[miniMetadata] updated miniMetadatas for %s in %sms", chainId, (performance.now() - start).toFixed(2));
|
2687
|
+
}
|
2688
|
+
};
|
2689
|
+
|
2690
|
+
const getUpdatedMiniMetadatas = async (chainConnector, chaindataProvider, chainId, specVersion, signal) => {
|
2691
|
+
const miniMetadatas = await getMiniMetadatas(chainConnector, chaindataProvider, chainId, specVersion, signal);
|
2692
|
+
signal?.throwIfAborted();
|
2693
|
+
await db.transaction("readwrite", "miniMetadatas", async tx => {
|
2694
|
+
await tx.miniMetadatas.where({
|
2695
|
+
chainId
|
2696
|
+
}).delete();
|
2697
|
+
await tx.miniMetadatas.bulkPut(miniMetadatas);
|
2698
|
+
});
|
2699
|
+
return miniMetadatas;
|
2700
|
+
};
|
2701
|
+
|
2702
|
+
const getMiniMetadata = async (chaindataProvider, chainConnector, chainId, source, signal) => {
|
2703
|
+
const specVersion = await getSpecVersion(chainConnector, chainId);
|
2704
|
+
signal?.throwIfAborted();
|
2705
|
+
const miniMetadataId = deriveMiniMetadataId({
|
2706
|
+
source,
|
2707
|
+
chainId,
|
2708
|
+
specVersion,
|
2709
|
+
libVersion
|
2710
|
+
});
|
2711
|
+
|
2712
|
+
// lookup local ones
|
2713
|
+
const [dbMiniMetadata, ghMiniMetadata] = await Promise.all([db.miniMetadatas.get(miniMetadataId), chaindataProvider.miniMetadataById(miniMetadataId)]);
|
2714
|
+
signal?.throwIfAborted();
|
2715
|
+
const miniMetadata = dbMiniMetadata ?? ghMiniMetadata;
|
2716
|
+
if (miniMetadata) return miniMetadata;
|
2717
|
+
|
2718
|
+
// update from live chain metadata and persist locally
|
2719
|
+
const miniMetadatas = await getUpdatedMiniMetadatas(chainConnector, chaindataProvider, chainId, specVersion, signal);
|
2720
|
+
signal?.throwIfAborted();
|
2721
|
+
const found = miniMetadatas.find(m => m.id === miniMetadataId);
|
2722
|
+
if (!found) {
|
2723
|
+
log.warn("MiniMetadata not found in updated miniMetadatas", {
|
2724
|
+
source,
|
2725
|
+
chainId,
|
2726
|
+
specVersion,
|
2727
|
+
libVersion,
|
2728
|
+
miniMetadataId,
|
2729
|
+
miniMetadatas
|
2730
|
+
});
|
2731
|
+
throw new Error(`MiniMetadata not found for ${source} on ${chainId}`);
|
2732
|
+
}
|
2733
|
+
return found;
|
2734
|
+
};
|
2735
|
+
|
2874
2736
|
/**
|
2875
2737
|
* Wraps a BalanceModule's fetch/subscribe methods with a single `balances` method.
|
2876
2738
|
* This `balances` method will subscribe if a callback parameter is provided, or otherwise fetch.
|
@@ -2886,44 +2748,16 @@ async function balances(balanceModule, addressesByToken, callback) {
|
|
2886
2748
|
return await balanceModule.fetchBalances(addressesByToken);
|
2887
2749
|
}
|
2888
2750
|
|
2889
|
-
|
2890
|
-
* Given a `moduleType` and a `chain` from a chaindataProvider, this function will find the chainMeta
|
2891
|
-
* associated with the given balanceModule for the given chain.
|
2892
|
-
*/
|
2893
|
-
const findChainMeta = (miniMetadatas, moduleType, chain) => {
|
2894
|
-
if (!chain) return [undefined, undefined];
|
2895
|
-
if (!chain.specName) return [undefined, undefined];
|
2896
|
-
if (!chain.specVersion) return [undefined, undefined];
|
2897
|
-
|
2898
|
-
// TODO: This is spaghetti to import this here, it should be injected into each balance module or something.
|
2899
|
-
const metadataId = deriveMiniMetadataId({
|
2900
|
-
source: moduleType,
|
2901
|
-
chainId: chain.id,
|
2902
|
-
specName: chain.specName,
|
2903
|
-
specVersion: chain.specVersion,
|
2904
|
-
balancesConfig: JSON.stringify(chain.balancesConfig?.find(config => config.moduleType === moduleType)?.moduleConfig ?? {})
|
2905
|
-
});
|
2906
|
-
|
2907
|
-
// TODO: Fix this (needs to fetch miniMetadata without being async)
|
2908
|
-
const miniMetadata = miniMetadatas.get(metadataId);
|
2909
|
-
const chainMeta = miniMetadata ? {
|
2910
|
-
miniMetadata: miniMetadata.data,
|
2911
|
-
metadataVersion: miniMetadata.version,
|
2912
|
-
...JSON.parse(miniMetadata.extra)
|
2913
|
-
} : undefined;
|
2914
|
-
return [chainMeta, miniMetadata];
|
2915
|
-
};
|
2916
|
-
|
2751
|
+
// TODO remove this one in favor of the network specific one below
|
2917
2752
|
const buildStorageCoders = ({
|
2918
2753
|
chainIds,
|
2919
2754
|
chains,
|
2920
2755
|
miniMetadatas,
|
2921
|
-
moduleType,
|
2922
2756
|
coders
|
2923
2757
|
}) => new Map([...chainIds].flatMap(chainId => {
|
2924
2758
|
const chain = chains[chainId];
|
2925
2759
|
if (!chain) return [];
|
2926
|
-
const
|
2760
|
+
const miniMetadata = miniMetadatas.get(chainId); // findMiniMetadata<TBalanceModule>(miniMetadatas, moduleType, chain)
|
2927
2761
|
if (!miniMetadata) return [];
|
2928
2762
|
if (!miniMetadata.data) return [];
|
2929
2763
|
const metadata = unifyMetadata(decAnyMetadata(miniMetadata.data));
|
@@ -2946,6 +2780,28 @@ const buildStorageCoders = ({
|
|
2946
2780
|
return [];
|
2947
2781
|
}
|
2948
2782
|
}));
|
2783
|
+
const buildNetworkStorageCoders = (chainId, miniMetadata, coders) => {
|
2784
|
+
if (!miniMetadata.data) return null;
|
2785
|
+
const metadata = unifyMetadata(decAnyMetadata(miniMetadata.data));
|
2786
|
+
try {
|
2787
|
+
const scaleBuilder = getDynamicBuilder(getLookupFn(metadata));
|
2788
|
+
const builtCoders = Object.fromEntries(Object.entries(coders).flatMap(([key, moduleMethodOrFn]) => {
|
2789
|
+
const [module, method] = typeof moduleMethodOrFn === "function" ? moduleMethodOrFn({
|
2790
|
+
chainId
|
2791
|
+
}) : moduleMethodOrFn;
|
2792
|
+
try {
|
2793
|
+
return [[key, scaleBuilder.buildStorage(module, method)]];
|
2794
|
+
} catch (cause) {
|
2795
|
+
log.trace(`Failed to build SCALE coder for chain ${chainId} (${module}::${method})`, cause);
|
2796
|
+
return [];
|
2797
|
+
}
|
2798
|
+
}));
|
2799
|
+
return builtCoders;
|
2800
|
+
} catch (cause) {
|
2801
|
+
log.error(`Failed to build SCALE coders for chain ${chainId} (${JSON.stringify(coders)})`, cause);
|
2802
|
+
}
|
2803
|
+
return null;
|
2804
|
+
};
|
2949
2805
|
|
2950
2806
|
/**
|
2951
2807
|
* Decodes & unwraps outputs and errors of a given result, contract, and method.
|
@@ -3027,7 +2883,7 @@ const detectTransferMethod = metadataRpc => {
|
|
3027
2883
|
return hasDeprecatedTransferCall ? "transfer" : "transfer_allow_death";
|
3028
2884
|
};
|
3029
2885
|
|
3030
|
-
const getUniqueChainIds = (addressesByToken, tokens) => [...new Set(Object.keys(addressesByToken).map(tokenId => tokens[tokenId]?.
|
2886
|
+
const getUniqueChainIds = (addressesByToken, tokens) => [...new Set(Object.keys(addressesByToken).map(tokenId => tokens[tokenId]?.networkId).flatMap(chainId => chainId ? [chainId] : []))];
|
3031
2887
|
|
3032
2888
|
const makeContractCaller = ({
|
3033
2889
|
chainConnector,
|
@@ -3136,8 +2992,15 @@ const decompress = data => {
|
|
3136
2992
|
return JSON.parse(decompressed);
|
3137
2993
|
};
|
3138
2994
|
|
3139
|
-
const moduleType$
|
3140
|
-
const
|
2995
|
+
const moduleType$4 = "substrate-assets";
|
2996
|
+
const SubAssetsTokenConfigSchema = z.strictObject({
|
2997
|
+
assetId: SubAssetsTokenSchema.shape.assetId,
|
2998
|
+
...TokenConfigBaseSchema.shape
|
2999
|
+
});
|
3000
|
+
const UNSUPPORTED_CHAIN_META$2 = {
|
3001
|
+
miniMetadata: null,
|
3002
|
+
extra: null
|
3003
|
+
};
|
3141
3004
|
const SubAssetsModule = hydrate => {
|
3142
3005
|
const {
|
3143
3006
|
chainConnectors,
|
@@ -3146,16 +3009,10 @@ const SubAssetsModule = hydrate => {
|
|
3146
3009
|
const chainConnector = chainConnectors.substrate;
|
3147
3010
|
assert(chainConnector, "This module requires a substrate chain connector");
|
3148
3011
|
return {
|
3149
|
-
...DefaultBalanceModule(moduleType$
|
3012
|
+
...DefaultBalanceModule(moduleType$4),
|
3013
|
+
// TODO make synchronous at the module definition level ?
|
3150
3014
|
async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) {
|
3151
|
-
|
3152
|
-
if (metadataRpc === undefined) return {
|
3153
|
-
isTestnet
|
3154
|
-
};
|
3155
|
-
if ((moduleConfig?.tokens ?? []).length < 1) return {
|
3156
|
-
isTestnet
|
3157
|
-
};
|
3158
|
-
const metadataVersion = getMetadataVersion(metadataRpc);
|
3015
|
+
if (!metadataRpc) return UNSUPPORTED_CHAIN_META$2;
|
3159
3016
|
const metadata = decAnyMetadata(metadataRpc);
|
3160
3017
|
compactMetadata(metadata, [{
|
3161
3018
|
pallet: "Assets",
|
@@ -3163,82 +3020,98 @@ const SubAssetsModule = hydrate => {
|
|
3163
3020
|
}]);
|
3164
3021
|
const miniMetadata = encodeMetadata(metadata);
|
3165
3022
|
return {
|
3166
|
-
isTestnet,
|
3167
3023
|
miniMetadata,
|
3168
|
-
|
3024
|
+
extra: null
|
3169
3025
|
};
|
3170
3026
|
},
|
3171
|
-
async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) {
|
3172
|
-
if (
|
3027
|
+
async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig, tokens) {
|
3028
|
+
if (!tokens?.length) return {};
|
3173
3029
|
const {
|
3174
|
-
|
3175
|
-
miniMetadata,
|
3176
|
-
metadataVersion
|
3030
|
+
miniMetadata
|
3177
3031
|
} = chainMeta;
|
3178
|
-
if (miniMetadata
|
3179
|
-
if (metadataVersion < 14) return {};
|
3032
|
+
if (!miniMetadata) return {};
|
3180
3033
|
const metadata = unifyMetadata(decAnyMetadata(miniMetadata));
|
3181
3034
|
const scaleBuilder = getDynamicBuilder(getLookupFn(metadata));
|
3182
3035
|
const assetCoder = scaleBuilder.buildStorage("Assets", "Asset");
|
3183
3036
|
const metadataCoder = scaleBuilder.buildStorage("Assets", "Metadata");
|
3184
|
-
const
|
3185
|
-
for (const tokenConfig of
|
3037
|
+
const tokenList = {};
|
3038
|
+
for (const tokenConfig of tokens ?? []) {
|
3186
3039
|
try {
|
3187
|
-
const assetId =
|
3040
|
+
const assetId = String(tokenConfig.assetId);
|
3188
3041
|
const assetStateKey = tryEncode(assetCoder, BigInt(assetId)) ?? tryEncode(assetCoder, assetId);
|
3189
3042
|
const metadataStateKey = tryEncode(metadataCoder, BigInt(assetId)) ?? tryEncode(metadataCoder, assetId);
|
3190
3043
|
if (assetStateKey === null || metadataStateKey === null) throw new Error(`Failed to encode stateKey for asset ${assetId} on chain ${chainId}`);
|
3191
3044
|
const [assetsAsset, assetsMetadata] = await Promise.all([chainConnector.send(chainId, "state_getStorage", [assetStateKey]).then(result => assetCoder.value.dec(result) ?? null), chainConnector.send(chainId, "state_getStorage", [metadataStateKey]).then(result => metadataCoder.value.dec(result) ?? null)]);
|
3192
3045
|
const existentialDeposit = assetsAsset?.min_balance?.toString?.() ?? "0";
|
3193
3046
|
const symbol = assetsMetadata?.symbol?.asText?.() ?? "Unit";
|
3047
|
+
const name = assetsMetadata?.name?.asText?.() ?? symbol;
|
3194
3048
|
const decimals = assetsMetadata?.decimals ?? 0;
|
3195
3049
|
const isFrozen = assetsMetadata?.is_frozen ?? false;
|
3196
|
-
const id = subAssetTokenId(chainId, assetId
|
3050
|
+
const id = subAssetTokenId(chainId, assetId);
|
3197
3051
|
const token = {
|
3198
3052
|
id,
|
3199
3053
|
type: "substrate-assets",
|
3200
|
-
|
3054
|
+
platform: "polkadot",
|
3201
3055
|
isDefault: tokenConfig?.isDefault ?? true,
|
3202
|
-
symbol,
|
3056
|
+
symbol: tokenConfig?.symbol ?? symbol,
|
3057
|
+
name: tokenConfig?.name ?? name,
|
3203
3058
|
decimals,
|
3204
|
-
logo: tokenConfig?.logo
|
3059
|
+
logo: tokenConfig?.logo,
|
3205
3060
|
existentialDeposit,
|
3206
3061
|
assetId,
|
3207
3062
|
isFrozen,
|
3208
|
-
|
3209
|
-
id: chainId
|
3210
|
-
}
|
3063
|
+
networkId: chainId
|
3211
3064
|
};
|
3212
3065
|
if (tokenConfig?.symbol) {
|
3213
3066
|
token.symbol = tokenConfig?.symbol;
|
3214
|
-
token.id = subAssetTokenId(chainId, assetId
|
3067
|
+
token.id = subAssetTokenId(chainId, assetId);
|
3215
3068
|
}
|
3216
3069
|
if (tokenConfig?.coingeckoId) token.coingeckoId = tokenConfig?.coingeckoId;
|
3217
|
-
if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName;
|
3218
3070
|
if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf;
|
3219
|
-
|
3071
|
+
tokenList[token.id] = token;
|
3220
3072
|
} catch (error) {
|
3221
3073
|
log.error(`Failed to build substrate-assets token ${tokenConfig.assetId} (${tokenConfig.symbol}) on ${chainId}`, error);
|
3222
3074
|
continue;
|
3223
3075
|
}
|
3224
3076
|
}
|
3225
|
-
return
|
3077
|
+
return tokenList;
|
3226
3078
|
},
|
3227
3079
|
// TODO: Don't create empty subscriptions
|
3228
3080
|
async subscribeBalances({
|
3229
3081
|
addressesByToken
|
3230
3082
|
}, callback) {
|
3231
|
-
const
|
3232
|
-
|
3233
|
-
if (
|
3234
|
-
|
3235
|
-
|
3236
|
-
});
|
3237
|
-
|
3083
|
+
const byNetwork = keys(addressesByToken).reduce((acc, tokenId) => {
|
3084
|
+
const networkId = parseSubAssetTokenId(tokenId).networkId;
|
3085
|
+
if (!acc[networkId]) acc[networkId] = {};
|
3086
|
+
acc[networkId][tokenId] = addressesByToken[tokenId];
|
3087
|
+
return acc;
|
3088
|
+
}, {});
|
3089
|
+
const controller = new AbortController();
|
3090
|
+
const pUnsubs = Promise.all(toPairs(byNetwork).map(async ([networkId, addressesByToken]) => {
|
3091
|
+
try {
|
3092
|
+
const queries = await buildNetworkQueries$2(networkId, chainConnector, chaindataProvider, addressesByToken, controller.signal);
|
3093
|
+
if (controller.signal.aborted) return () => {};
|
3094
|
+
const stateHelper = new RpcStateQueryHelper(chainConnector, queries);
|
3095
|
+
return await stateHelper.subscribe((error, result) => {
|
3096
|
+
if (error) return callback(error);
|
3097
|
+
const balances = result?.filter(b => b !== null) ?? [];
|
3098
|
+
if (balances.length > 0) callback(null, new Balances(balances));
|
3099
|
+
});
|
3100
|
+
} catch (err) {
|
3101
|
+
if (!isAbortError(err)) log.error(`Failed to subscribe balances for network ${networkId}`, err);
|
3102
|
+
return () => {};
|
3103
|
+
}
|
3104
|
+
}));
|
3105
|
+
return () => {
|
3106
|
+
pUnsubs.then(unsubs => {
|
3107
|
+
unsubs.forEach(unsubscribe => unsubscribe());
|
3108
|
+
});
|
3109
|
+
controller.abort();
|
3110
|
+
};
|
3238
3111
|
},
|
3239
3112
|
async fetchBalances(addressesByToken) {
|
3240
3113
|
assert(chainConnectors.substrate, "This module requires a substrate chain connector");
|
3241
|
-
const queries = await buildQueries$
|
3114
|
+
const queries = await buildQueries$3(chainConnector, chaindataProvider, addressesByToken);
|
3242
3115
|
const result = await new RpcStateQueryHelper(chainConnectors.substrate, queries).fetch();
|
3243
3116
|
const balances = result?.filter(b => b !== null) ?? [];
|
3244
3117
|
return new Balances(balances);
|
@@ -3259,11 +3132,10 @@ const SubAssetsModule = hydrate => {
|
|
3259
3132
|
transferMethod,
|
3260
3133
|
userExtensions
|
3261
3134
|
}) {
|
3262
|
-
const token = await chaindataProvider.
|
3135
|
+
const token = await chaindataProvider.getTokenById(tokenId, "substrate-assets");
|
3263
3136
|
assert(token, `Token ${tokenId} not found in store`);
|
3264
|
-
|
3265
|
-
const
|
3266
|
-
const chain = await chaindataProvider.chainById(chainId);
|
3137
|
+
const chainId = token.networkId;
|
3138
|
+
const chain = await chaindataProvider.getNetworkById(chainId, "polkadot");
|
3267
3139
|
assert(chain?.genesisHash, `Chain ${chainId} not found in store`);
|
3268
3140
|
const {
|
3269
3141
|
genesisHash
|
@@ -3308,23 +3180,16 @@ const SubAssetsModule = hydrate => {
|
|
3308
3180
|
}
|
3309
3181
|
};
|
3310
3182
|
};
|
3311
|
-
async function
|
3312
|
-
const
|
3313
|
-
const
|
3314
|
-
const
|
3315
|
-
|
3316
|
-
const
|
3317
|
-
|
3318
|
-
chainIds: uniqueChainIds,
|
3319
|
-
chains,
|
3320
|
-
miniMetadatas,
|
3321
|
-
moduleType: "substrate-assets",
|
3322
|
-
coders: {
|
3323
|
-
storage: ["Assets", "Account"]
|
3324
|
-
}
|
3183
|
+
async function buildNetworkQueries$2(networkId, chainConnector, chaindataProvider, addressesByToken, signal) {
|
3184
|
+
const miniMetadata = await getMiniMetadata(chaindataProvider, chainConnector, networkId, moduleType$4, signal);
|
3185
|
+
const chain = await chaindataProvider.getNetworkById(networkId, "polkadot");
|
3186
|
+
const tokensById = await chaindataProvider.getTokensMapById();
|
3187
|
+
signal?.throwIfAborted();
|
3188
|
+
const networkStorageCoders = buildNetworkStorageCoders(networkId, miniMetadata, {
|
3189
|
+
storage: ["Assets", "Account"]
|
3325
3190
|
});
|
3326
3191
|
return Object.entries(addressesByToken).flatMap(([tokenId, addresses]) => {
|
3327
|
-
const token =
|
3192
|
+
const token = tokensById[tokenId];
|
3328
3193
|
if (!token) {
|
3329
3194
|
log.warn(`Token ${tokenId} not found`);
|
3330
3195
|
return [];
|
@@ -3333,27 +3198,22 @@ async function buildQueries$4(chaindataProvider, addressesByToken) {
|
|
3333
3198
|
log.debug(`This module doesn't handle tokens of type ${token.type}`);
|
3334
3199
|
return [];
|
3335
3200
|
}
|
3336
|
-
|
3337
|
-
if (!chainId) {
|
3338
|
-
log.warn(`Token ${tokenId} has no chain`);
|
3339
|
-
return [];
|
3340
|
-
}
|
3341
|
-
const chain = chains[chainId];
|
3201
|
+
//
|
3342
3202
|
if (!chain) {
|
3343
|
-
log.warn(`Chain ${
|
3203
|
+
log.warn(`Chain ${networkId} for token ${tokenId} not found`);
|
3344
3204
|
return [];
|
3345
3205
|
}
|
3346
3206
|
return addresses.flatMap(address => {
|
3347
|
-
const scaleCoder =
|
3348
|
-
const stateKey = tryEncode(scaleCoder,
|
3207
|
+
const scaleCoder = networkStorageCoders?.storage;
|
3208
|
+
const stateKey = tryEncode(scaleCoder, Number(token.assetId), address) ?? tryEncode(scaleCoder, BigInt(token.assetId), address);
|
3349
3209
|
if (!stateKey) {
|
3350
|
-
log.warn(`Invalid assetId / address in ${
|
3210
|
+
log.warn(`Invalid assetId / address in ${networkId} storage query ${token.assetId} / ${address}`);
|
3351
3211
|
return [];
|
3352
3212
|
}
|
3353
3213
|
const decodeResult = change => {
|
3354
3214
|
/** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
|
3355
3215
|
|
3356
|
-
const decoded = decodeScale(scaleCoder, change, `Failed to decode substrate-assets balance on chain ${
|
3216
|
+
const decoded = decodeScale(scaleCoder, change, `Failed to decode substrate-assets balance on chain ${networkId}`) ?? {
|
3357
3217
|
balance: 0n,
|
3358
3218
|
status: {
|
3359
3219
|
type: "Liquid"
|
@@ -3387,22 +3247,30 @@ async function buildQueries$4(chaindataProvider, addressesByToken) {
|
|
3387
3247
|
source: "substrate-assets",
|
3388
3248
|
status: "live",
|
3389
3249
|
address,
|
3390
|
-
|
3391
|
-
subChainId: chainId
|
3392
|
-
},
|
3393
|
-
chainId,
|
3250
|
+
networkId,
|
3394
3251
|
tokenId: token.id,
|
3395
3252
|
values: balanceValues
|
3396
3253
|
};
|
3397
3254
|
};
|
3398
3255
|
return {
|
3399
|
-
chainId,
|
3256
|
+
chainId: networkId,
|
3400
3257
|
stateKey,
|
3401
3258
|
decodeResult
|
3402
3259
|
};
|
3403
3260
|
});
|
3404
3261
|
});
|
3405
3262
|
}
|
3263
|
+
async function buildQueries$3(chainConnector, chaindataProvider, addressesByToken, signal) {
|
3264
|
+
const byNetwork = keys(addressesByToken).reduce((acc, tokenId) => {
|
3265
|
+
const networkId = parseSubAssetTokenId(tokenId).networkId;
|
3266
|
+
if (!acc[networkId]) acc[networkId] = {};
|
3267
|
+
acc[networkId][tokenId] = addressesByToken[tokenId];
|
3268
|
+
return acc;
|
3269
|
+
}, {});
|
3270
|
+
return (await Promise.all(toPairs(byNetwork).map(([networkId, addressesByToken]) => {
|
3271
|
+
return buildNetworkQueries$2(networkId, chainConnector, chaindataProvider, addressesByToken, signal);
|
3272
|
+
}))).flat();
|
3273
|
+
}
|
3406
3274
|
// NOTE: Different chains need different formats for assetId when encoding the stateKey
|
3407
3275
|
// E.g. Polkadot Asset Hub needs it to be a string, Astar needs it to be a bigint
|
3408
3276
|
//
|
@@ -3415,9 +3283,16 @@ const tryEncode = (scaleCoder, ...args) => {
|
|
3415
3283
|
}
|
3416
3284
|
};
|
3417
3285
|
|
3418
|
-
const moduleType$
|
3419
|
-
const
|
3420
|
-
|
3286
|
+
const moduleType$3 = "substrate-foreignassets";
|
3287
|
+
const UNSUPPORTED_CHAIN_META$1 = {
|
3288
|
+
miniMetadata: null,
|
3289
|
+
extra: null
|
3290
|
+
};
|
3291
|
+
const SubForeignAssetsTokenConfigSchema = z.strictObject({
|
3292
|
+
onChainId: SubForeignAssetsTokenSchema.shape.onChainId,
|
3293
|
+
...TokenConfigBaseSchema.shape
|
3294
|
+
});
|
3295
|
+
const SubForeignAssetsModule = hydrate => {
|
3421
3296
|
const {
|
3422
3297
|
chainConnectors,
|
3423
3298
|
chaindataProvider
|
@@ -3425,318 +3300,41 @@ const SubEquilibriumModule = hydrate => {
|
|
3425
3300
|
const chainConnector = chainConnectors.substrate;
|
3426
3301
|
assert(chainConnector, "This module requires a substrate chain connector");
|
3427
3302
|
return {
|
3428
|
-
...DefaultBalanceModule(moduleType$
|
3303
|
+
...DefaultBalanceModule(moduleType$3),
|
3429
3304
|
async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) {
|
3430
|
-
|
3431
|
-
if (metadataRpc === undefined) return {
|
3432
|
-
isTestnet
|
3433
|
-
};
|
3434
|
-
if (moduleConfig?.disable !== false) return {
|
3435
|
-
isTestnet
|
3436
|
-
}; // default to disabled
|
3437
|
-
|
3305
|
+
if (metadataRpc === undefined) return UNSUPPORTED_CHAIN_META$1;
|
3438
3306
|
const metadataVersion = getMetadataVersion(metadataRpc);
|
3307
|
+
if (metadataVersion < 14) return UNSUPPORTED_CHAIN_META$1;
|
3439
3308
|
const metadata = decAnyMetadata(metadataRpc);
|
3440
3309
|
compactMetadata(metadata, [{
|
3441
|
-
pallet: "
|
3442
|
-
items: ["
|
3443
|
-
}, {
|
3444
|
-
pallet: "System",
|
3445
|
-
items: ["Account"]
|
3310
|
+
pallet: "ForeignAssets",
|
3311
|
+
items: ["Account", "Asset", "Metadata"]
|
3446
3312
|
}]);
|
3447
3313
|
const miniMetadata = encodeMetadata(metadata);
|
3448
3314
|
return {
|
3449
|
-
isTestnet,
|
3450
3315
|
miniMetadata,
|
3451
|
-
|
3316
|
+
extra: null
|
3452
3317
|
};
|
3453
3318
|
},
|
3454
|
-
async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) {
|
3455
|
-
|
3456
|
-
if (moduleConfig?.disable !== false) return {};
|
3319
|
+
async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig, tokens) {
|
3320
|
+
if (!tokens?.length) return {};
|
3457
3321
|
const {
|
3458
|
-
|
3459
|
-
miniMetadata,
|
3460
|
-
metadataVersion
|
3322
|
+
miniMetadata
|
3461
3323
|
} = chainMeta;
|
3462
|
-
if (miniMetadata
|
3463
|
-
|
3464
|
-
|
3465
|
-
|
3466
|
-
|
3467
|
-
|
3468
|
-
|
3469
|
-
|
3470
|
-
|
3471
|
-
|
3472
|
-
|
3473
|
-
|
3474
|
-
|
3475
|
-
|
3476
|
-
const assetId = asset.id.toString(10);
|
3477
|
-
const symbol = tokenSymbolFromU64Id(asset.id);
|
3478
|
-
const id = subEquilibriumTokenId(chainId, symbol);
|
3479
|
-
const decimals = DEFAULT_DECIMALS$1;
|
3480
|
-
const tokenConfig = (moduleConfig?.tokens ?? []).find(token => token.assetId === assetId);
|
3481
|
-
const token = {
|
3482
|
-
id,
|
3483
|
-
type: "substrate-equilibrium",
|
3484
|
-
isTestnet,
|
3485
|
-
isDefault: tokenConfig?.isDefault ?? true,
|
3486
|
-
symbol,
|
3487
|
-
decimals,
|
3488
|
-
logo: tokenConfig?.logo || githubTokenLogoUrl(id),
|
3489
|
-
// TODO: Fetch the ED
|
3490
|
-
existentialDeposit: "0",
|
3491
|
-
assetId,
|
3492
|
-
chain: {
|
3493
|
-
id: chainId
|
3494
|
-
}
|
3495
|
-
};
|
3496
|
-
if (tokenConfig?.symbol) {
|
3497
|
-
token.symbol = tokenConfig?.symbol;
|
3498
|
-
token.id = subEquilibriumTokenId(chainId, token.symbol);
|
3499
|
-
}
|
3500
|
-
if (tokenConfig?.coingeckoId) token.coingeckoId = tokenConfig?.coingeckoId;
|
3501
|
-
if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName;
|
3502
|
-
if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf;
|
3503
|
-
return [[token.id, token]];
|
3504
|
-
});
|
3505
|
-
return Object.fromEntries(tokens);
|
3506
|
-
} catch (error) {
|
3507
|
-
log.error(`Failed to build substrate-equilibrium tokens on ${chainId}`, error?.message ?? error);
|
3508
|
-
return {};
|
3509
|
-
}
|
3510
|
-
},
|
3511
|
-
// TODO: Don't create empty subscriptions
|
3512
|
-
async subscribeBalances({
|
3513
|
-
addressesByToken
|
3514
|
-
}, callback) {
|
3515
|
-
const queries = await buildQueries$3(chaindataProvider, addressesByToken);
|
3516
|
-
const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe((error, result) => {
|
3517
|
-
if (error) return callback(error);
|
3518
|
-
const balances = result?.flatMap(balances => balances) ?? [];
|
3519
|
-
if (balances.length > 0) callback(null, new Balances(balances));
|
3520
|
-
});
|
3521
|
-
return unsubscribe;
|
3522
|
-
},
|
3523
|
-
async fetchBalances(addressesByToken) {
|
3524
|
-
assert(chainConnectors.substrate, "This module requires a substrate chain connector");
|
3525
|
-
const queries = await buildQueries$3(chaindataProvider, addressesByToken);
|
3526
|
-
const result = await new RpcStateQueryHelper(chainConnectors.substrate, queries).fetch();
|
3527
|
-
const balances = result?.flatMap(balances => balances) ?? [];
|
3528
|
-
return new Balances(balances);
|
3529
|
-
},
|
3530
|
-
async transferToken({
|
3531
|
-
tokenId,
|
3532
|
-
from,
|
3533
|
-
to,
|
3534
|
-
amount,
|
3535
|
-
registry,
|
3536
|
-
metadataRpc,
|
3537
|
-
blockHash,
|
3538
|
-
blockNumber,
|
3539
|
-
nonce,
|
3540
|
-
specVersion,
|
3541
|
-
transactionVersion,
|
3542
|
-
tip,
|
3543
|
-
transferMethod,
|
3544
|
-
userExtensions
|
3545
|
-
}) {
|
3546
|
-
const token = await chaindataProvider.tokenById(tokenId);
|
3547
|
-
assert(token, `Token ${tokenId} not found in store`);
|
3548
|
-
if (token.type !== "substrate-equilibrium") throw new Error(`This module doesn't handle tokens of type ${token.type}`);
|
3549
|
-
const chainId = token.chain.id;
|
3550
|
-
const chain = await chaindataProvider.chainById(chainId);
|
3551
|
-
assert(chain?.genesisHash, `Chain ${chainId} not found in store`);
|
3552
|
-
const {
|
3553
|
-
genesisHash
|
3554
|
-
} = chain;
|
3555
|
-
const {
|
3556
|
-
assetId
|
3557
|
-
} = token;
|
3558
|
-
const pallet = "EqBalances";
|
3559
|
-
const method = transferMethod === "transfer_all" ?
|
3560
|
-
// the eqBalances pallet has no transfer_all method
|
3561
|
-
"transfer" : transferMethod === "transfer_keep_alive" ?
|
3562
|
-
// the eqBalances pallet has no transfer_keep_alive method
|
3563
|
-
"transfer" : "transfer";
|
3564
|
-
const args = {
|
3565
|
-
asset: assetId,
|
3566
|
-
to,
|
3567
|
-
value: amount
|
3568
|
-
};
|
3569
|
-
const unsigned = defineMethod({
|
3570
|
-
method: {
|
3571
|
-
pallet: camelCase(pallet),
|
3572
|
-
name: camelCase(method),
|
3573
|
-
args
|
3574
|
-
},
|
3575
|
-
address: from,
|
3576
|
-
blockHash,
|
3577
|
-
blockNumber,
|
3578
|
-
eraPeriod: 64,
|
3579
|
-
genesisHash,
|
3580
|
-
metadataRpc,
|
3581
|
-
nonce,
|
3582
|
-
specVersion,
|
3583
|
-
tip: tip ? Number(tip) : 0,
|
3584
|
-
transactionVersion
|
3585
|
-
}, {
|
3586
|
-
metadataRpc,
|
3587
|
-
registry,
|
3588
|
-
userExtensions
|
3589
|
-
});
|
3590
|
-
return {
|
3591
|
-
type: "substrate",
|
3592
|
-
callData: unsigned.method
|
3593
|
-
};
|
3594
|
-
}
|
3595
|
-
};
|
3596
|
-
};
|
3597
|
-
async function buildQueries$3(chaindataProvider, addressesByToken) {
|
3598
|
-
const allChains = await chaindataProvider.chainsById();
|
3599
|
-
const tokens = await chaindataProvider.tokensById();
|
3600
|
-
const miniMetadatas = new Map((await db.miniMetadatas.toArray()).map(miniMetadata => [miniMetadata.id, miniMetadata]));
|
3601
|
-
const uniqueChainIds = getUniqueChainIds(addressesByToken, tokens);
|
3602
|
-
const chains = Object.fromEntries(uniqueChainIds.map(chainId => [chainId, allChains[chainId]]));
|
3603
|
-
const chainStorageCoders = buildStorageCoders({
|
3604
|
-
chainIds: uniqueChainIds,
|
3605
|
-
chains,
|
3606
|
-
miniMetadatas,
|
3607
|
-
moduleType: "substrate-equilibrium",
|
3608
|
-
coders: {
|
3609
|
-
storage: ["System", "Account"]
|
3610
|
-
}
|
3611
|
-
});
|
3612
|
-
|
3613
|
-
// equilibrium returns all chain tokens for each address in the one query
|
3614
|
-
// so, we only need to make one query per address, rather than one query per token per address
|
3615
|
-
const addressesByChain = new Map();
|
3616
|
-
const tokensByAddress = new Map();
|
3617
|
-
Object.entries(addressesByToken).map(([tokenId, addresses]) => {
|
3618
|
-
const token = tokens[tokenId];
|
3619
|
-
if (!token) return log.warn(`Token ${tokenId} not found`);
|
3620
|
-
if (token.type !== "substrate-equilibrium") return log.debug(`This module doesn't handle tokens of type ${token.type}`);
|
3621
|
-
const chainId = token?.chain?.id;
|
3622
|
-
if (!chainId) return log.warn(`Token ${tokenId} has no chain`);
|
3623
|
-
const byChain = addressesByChain.get(chainId) ?? new Set();
|
3624
|
-
addresses.forEach(address => {
|
3625
|
-
byChain?.add(address);
|
3626
|
-
tokensByAddress.set(address, (tokensByAddress.get(address) ?? new Set()).add(token));
|
3627
|
-
});
|
3628
|
-
addressesByChain.set(chainId, byChain);
|
3629
|
-
});
|
3630
|
-
return Array.from(addressesByChain).flatMap(([chainId, addresses]) => {
|
3631
|
-
const chain = chains[chainId];
|
3632
|
-
if (!chain) {
|
3633
|
-
log.warn(`Chain ${chainId} not found`);
|
3634
|
-
return [];
|
3635
|
-
}
|
3636
|
-
return Array.from(addresses).flatMap(address => {
|
3637
|
-
const scaleCoder = chainStorageCoders.get(chainId)?.storage;
|
3638
|
-
const stateKey = encodeStateKey(scaleCoder, `Invalid address in ${chainId} storage query ${address}`, address);
|
3639
|
-
if (!stateKey) return [];
|
3640
|
-
const decodeResult = change => {
|
3641
|
-
/** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
|
3642
|
-
|
3643
|
-
const decoded = decodeScale(scaleCoder, change, `Failed to decode eqBalances on chain ${chainId}`);
|
3644
|
-
const tokenBalances = Object.fromEntries((decoded?.data?.value?.balance ?? []).map(balance => ({
|
3645
|
-
id: (balance?.[0] ?? 0n)?.toString?.(),
|
3646
|
-
free: balance?.[1]?.type === "Positive" ? (balance?.[1]?.value ?? 0n).toString() : balance?.[1]?.type === "Negative" ? ((balance?.[1]?.value ?? 0n) * -1n).toString() : "0"
|
3647
|
-
})).map(({
|
3648
|
-
id,
|
3649
|
-
free
|
3650
|
-
}) => [id, free]).filter(([id, free]) => id !== undefined && free !== undefined));
|
3651
|
-
const result = Array.from(tokensByAddress.get(address) ?? []).filter(t => t.chain.id === chainId).map(token => {
|
3652
|
-
const value = tokenBalances[token.assetId];
|
3653
|
-
return {
|
3654
|
-
source: "substrate-equilibrium",
|
3655
|
-
status: "live",
|
3656
|
-
address,
|
3657
|
-
multiChainId: {
|
3658
|
-
subChainId: chainId
|
3659
|
-
},
|
3660
|
-
chainId,
|
3661
|
-
tokenId: token.id,
|
3662
|
-
value
|
3663
|
-
};
|
3664
|
-
}).filter(b => b !== undefined);
|
3665
|
-
return result;
|
3666
|
-
};
|
3667
|
-
return {
|
3668
|
-
chainId,
|
3669
|
-
stateKey,
|
3670
|
-
decodeResult
|
3671
|
-
};
|
3672
|
-
});
|
3673
|
-
});
|
3674
|
-
}
|
3675
|
-
const DEFAULT_DECIMALS$1 = 9;
|
3676
|
-
const tokenSymbolFromU64Id = u64 => {
|
3677
|
-
const bytes = [];
|
3678
|
-
let num = typeof u64 === "number" ? BigInt(u64) : isBigInt(u64) ? u64 : u64.toBigInt();
|
3679
|
-
do {
|
3680
|
-
bytes.unshift(Number(num % 256n));
|
3681
|
-
num = num / 256n;
|
3682
|
-
} while (num > 0);
|
3683
|
-
return new TextDecoder("utf-8").decode(new Uint8Array(bytes)).toUpperCase();
|
3684
|
-
};
|
3685
|
-
|
3686
|
-
const moduleType$3 = "substrate-foreignassets";
|
3687
|
-
const subForeignAssetTokenId = (chainId, tokenSymbol) => `${chainId}-substrate-foreignassets-${tokenSymbol}`.toLowerCase().replace(/ /g, "-");
|
3688
|
-
const SubForeignAssetsModule = hydrate => {
|
3689
|
-
const {
|
3690
|
-
chainConnectors,
|
3691
|
-
chaindataProvider
|
3692
|
-
} = hydrate;
|
3693
|
-
const chainConnector = chainConnectors.substrate;
|
3694
|
-
assert(chainConnector, "This module requires a substrate chain connector");
|
3695
|
-
return {
|
3696
|
-
...DefaultBalanceModule(moduleType$3),
|
3697
|
-
async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) {
|
3698
|
-
const isTestnet = (await chaindataProvider.chainById(chainId))?.isTestnet || false;
|
3699
|
-
if (metadataRpc === undefined) return {
|
3700
|
-
isTestnet
|
3701
|
-
};
|
3702
|
-
if ((moduleConfig?.tokens ?? []).length < 1) return {
|
3703
|
-
isTestnet
|
3704
|
-
};
|
3705
|
-
const metadataVersion = getMetadataVersion(metadataRpc);
|
3706
|
-
const metadata = decAnyMetadata(metadataRpc);
|
3707
|
-
compactMetadata(metadata, [{
|
3708
|
-
pallet: "ForeignAssets",
|
3709
|
-
items: ["Account", "Asset", "Metadata"]
|
3710
|
-
}]);
|
3711
|
-
const miniMetadata = encodeMetadata(metadata);
|
3712
|
-
return {
|
3713
|
-
isTestnet,
|
3714
|
-
miniMetadata,
|
3715
|
-
metadataVersion
|
3716
|
-
};
|
3717
|
-
},
|
3718
|
-
async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) {
|
3719
|
-
if ((moduleConfig?.tokens ?? []).length < 1) return {};
|
3720
|
-
const {
|
3721
|
-
isTestnet,
|
3722
|
-
miniMetadata,
|
3723
|
-
metadataVersion
|
3724
|
-
} = chainMeta;
|
3725
|
-
if (miniMetadata === undefined || metadataVersion === undefined) return {};
|
3726
|
-
if (metadataVersion < 14) return {};
|
3727
|
-
const metadata = decAnyMetadata(miniMetadata);
|
3728
|
-
const unifiedMetadata = unifyMetadata(metadata);
|
3729
|
-
const scaleBuilder = getDynamicBuilder(getLookupFn(unifiedMetadata));
|
3730
|
-
const assetCoder = scaleBuilder.buildStorage("ForeignAssets", "Asset");
|
3731
|
-
const metadataCoder = scaleBuilder.buildStorage("ForeignAssets", "Metadata");
|
3732
|
-
const tokens = {};
|
3733
|
-
for (const tokenConfig of moduleConfig?.tokens ?? []) {
|
3734
|
-
try {
|
3735
|
-
const onChainId = (() => {
|
3736
|
-
try {
|
3737
|
-
return papiParse(tokenConfig.onChainId);
|
3738
|
-
} catch (error) {
|
3739
|
-
return tokenConfig.onChainId;
|
3324
|
+
if (!miniMetadata) return {};
|
3325
|
+
const metadata = decAnyMetadata(miniMetadata);
|
3326
|
+
const unifiedMetadata = unifyMetadata(metadata);
|
3327
|
+
const scaleBuilder = getDynamicBuilder(getLookupFn(unifiedMetadata));
|
3328
|
+
const assetCoder = scaleBuilder.buildStorage("ForeignAssets", "Asset");
|
3329
|
+
const metadataCoder = scaleBuilder.buildStorage("ForeignAssets", "Metadata");
|
3330
|
+
const tokenList = {};
|
3331
|
+
for (const tokenConfig of tokens ?? []) {
|
3332
|
+
try {
|
3333
|
+
const onChainId = (() => {
|
3334
|
+
try {
|
3335
|
+
return papiParse(tokenConfig.onChainId);
|
3336
|
+
} catch (error) {
|
3337
|
+
return tokenConfig.onChainId;
|
3740
3338
|
}
|
3741
3339
|
})();
|
3742
3340
|
if (onChainId === undefined) continue;
|
@@ -3745,54 +3343,70 @@ const SubForeignAssetsModule = hydrate => {
|
|
3745
3343
|
const [assetsAsset, assetsMetadata] = await Promise.all([chainConnector.send(chainId, "state_getStorage", [assetStateKey]).then(result => assetCoder.value.dec(result) ?? null), chainConnector.send(chainId, "state_getStorage", [metadataStateKey]).then(result => metadataCoder.value.dec(result) ?? null)]);
|
3746
3344
|
const existentialDeposit = assetsAsset?.min_balance?.toString?.() ?? "0";
|
3747
3345
|
const symbol = assetsMetadata?.symbol?.asText?.() ?? "Unit";
|
3346
|
+
const name = assetsMetadata?.name?.asText?.() ?? symbol;
|
3748
3347
|
const decimals = assetsMetadata?.decimals ?? 0;
|
3749
3348
|
const isFrozen = assetsMetadata?.is_frozen ?? false;
|
3750
|
-
const id = subForeignAssetTokenId(chainId,
|
3349
|
+
const id = subForeignAssetTokenId(chainId, tokenConfig.onChainId);
|
3751
3350
|
const token = {
|
3752
3351
|
id,
|
3753
3352
|
type: "substrate-foreignassets",
|
3754
|
-
|
3353
|
+
platform: "polkadot",
|
3755
3354
|
isDefault: tokenConfig?.isDefault ?? true,
|
3756
3355
|
symbol,
|
3757
3356
|
decimals,
|
3758
|
-
|
3357
|
+
name: tokenConfig?.name ?? name,
|
3358
|
+
logo: tokenConfig?.logo,
|
3759
3359
|
existentialDeposit,
|
3760
3360
|
onChainId: tokenConfig.onChainId,
|
3761
3361
|
isFrozen,
|
3762
|
-
|
3763
|
-
id: chainId
|
3764
|
-
}
|
3362
|
+
networkId: chainId
|
3765
3363
|
};
|
3766
|
-
if (tokenConfig?.symbol) {
|
3767
|
-
token.symbol = tokenConfig?.symbol;
|
3768
|
-
token.id = subForeignAssetTokenId(chainId, token.symbol);
|
3769
|
-
}
|
3770
3364
|
if (tokenConfig?.coingeckoId) token.coingeckoId = tokenConfig?.coingeckoId;
|
3771
|
-
if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName;
|
3772
3365
|
if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf;
|
3773
|
-
|
3366
|
+
tokenList[token.id] = token;
|
3774
3367
|
} catch (error) {
|
3775
3368
|
log.error(`Failed to build substrate-foreignassets token ${tokenConfig.onChainId} (${tokenConfig.symbol}) on ${chainId}`, error?.message ?? error);
|
3776
3369
|
continue;
|
3777
3370
|
}
|
3778
3371
|
}
|
3779
|
-
return
|
3372
|
+
return tokenList;
|
3780
3373
|
},
|
3781
3374
|
// TODO: Don't create empty subscriptions
|
3782
3375
|
async subscribeBalances({
|
3783
3376
|
addressesByToken
|
3784
3377
|
}, callback) {
|
3785
|
-
const
|
3786
|
-
|
3787
|
-
if (
|
3788
|
-
|
3789
|
-
|
3790
|
-
});
|
3791
|
-
|
3378
|
+
const byNetwork = keys(addressesByToken).reduce((acc, tokenId) => {
|
3379
|
+
const networkId = parseSubForeignAssetTokenId(tokenId).networkId;
|
3380
|
+
if (!acc[networkId]) acc[networkId] = {};
|
3381
|
+
acc[networkId][tokenId] = addressesByToken[tokenId];
|
3382
|
+
return acc;
|
3383
|
+
}, {});
|
3384
|
+
const controller = new AbortController();
|
3385
|
+
const pUnsubs = Promise.all(toPairs(byNetwork).map(async ([networkId, addressesByToken]) => {
|
3386
|
+
try {
|
3387
|
+
const queries = await buildNetworkQueries$1(networkId, chainConnector, chaindataProvider, addressesByToken, controller.signal);
|
3388
|
+
if (controller.signal.aborted) return () => {};
|
3389
|
+
const stateHelper = new RpcStateQueryHelper(chainConnector, queries);
|
3390
|
+
return await stateHelper.subscribe((error, result) => {
|
3391
|
+
if (error) return callback(error);
|
3392
|
+
const balances = result?.filter(b => b !== null) ?? [];
|
3393
|
+
if (balances.length > 0) callback(null, new Balances(balances));
|
3394
|
+
});
|
3395
|
+
} catch (err) {
|
3396
|
+
if (!isAbortError(err)) log.error(`Failed to subscribe ${moduleType$3} balances for network ${networkId}`, err);
|
3397
|
+
return () => {};
|
3398
|
+
}
|
3399
|
+
}));
|
3400
|
+
return () => {
|
3401
|
+
pUnsubs.then(unsubs => {
|
3402
|
+
unsubs.forEach(unsubscribe => unsubscribe());
|
3403
|
+
});
|
3404
|
+
controller.abort();
|
3405
|
+
};
|
3792
3406
|
},
|
3793
3407
|
async fetchBalances(addressesByToken) {
|
3794
3408
|
assert(chainConnectors.substrate, "This module requires a substrate chain connector");
|
3795
|
-
const queries = await buildQueries$2(chaindataProvider, addressesByToken);
|
3409
|
+
const queries = await buildQueries$2(chainConnector, chaindataProvider, addressesByToken);
|
3796
3410
|
const result = await new RpcStateQueryHelper(chainConnectors.substrate, queries).fetch();
|
3797
3411
|
const balances = result?.filter(b => b !== null) ?? [];
|
3798
3412
|
return new Balances(balances);
|
@@ -3804,11 +3418,10 @@ const SubForeignAssetsModule = hydrate => {
|
|
3804
3418
|
transferMethod,
|
3805
3419
|
metadataRpc
|
3806
3420
|
}) {
|
3807
|
-
const token = await chaindataProvider.
|
3421
|
+
const token = await chaindataProvider.getTokenById(tokenId, "substrate-foreignassets");
|
3808
3422
|
assert(token, `Token ${tokenId} not found in store`);
|
3809
|
-
|
3810
|
-
const
|
3811
|
-
const chain = await chaindataProvider.chainById(chainId);
|
3423
|
+
const chainId = token.networkId;
|
3424
|
+
const chain = await chaindataProvider.getNetworkById(chainId, "polkadot");
|
3812
3425
|
assert(chain?.genesisHash, `Chain ${chainId} not found in store`);
|
3813
3426
|
const onChainId = (() => {
|
3814
3427
|
try {
|
@@ -3848,23 +3461,16 @@ const SubForeignAssetsModule = hydrate => {
|
|
3848
3461
|
}
|
3849
3462
|
};
|
3850
3463
|
};
|
3851
|
-
async function
|
3852
|
-
const
|
3853
|
-
const
|
3854
|
-
const
|
3855
|
-
|
3856
|
-
const
|
3857
|
-
|
3858
|
-
chainIds: uniqueChainIds,
|
3859
|
-
chains,
|
3860
|
-
miniMetadatas,
|
3861
|
-
moduleType: "substrate-foreignassets",
|
3862
|
-
coders: {
|
3863
|
-
storage: ["ForeignAssets", "Account"]
|
3864
|
-
}
|
3464
|
+
async function buildNetworkQueries$1(networkId, chainConnector, chaindataProvider, addressesByToken, signal) {
|
3465
|
+
const miniMetadata = await getMiniMetadata(chaindataProvider, chainConnector, networkId, moduleType$3, signal);
|
3466
|
+
const chain = await chaindataProvider.getNetworkById(networkId, "polkadot");
|
3467
|
+
const tokensById = await chaindataProvider.getTokensMapById();
|
3468
|
+
signal?.throwIfAborted();
|
3469
|
+
const networkStorageCoders = buildNetworkStorageCoders(networkId, miniMetadata, {
|
3470
|
+
storage: ["ForeignAssets", "Account"]
|
3865
3471
|
});
|
3866
3472
|
return Object.entries(addressesByToken).flatMap(([tokenId, addresses]) => {
|
3867
|
-
const token =
|
3473
|
+
const token = tokensById[tokenId];
|
3868
3474
|
if (!token) {
|
3869
3475
|
log.warn(`Token ${tokenId} not found`);
|
3870
3476
|
return [];
|
@@ -3873,18 +3479,12 @@ async function buildQueries$2(chaindataProvider, addressesByToken) {
|
|
3873
3479
|
log.debug(`This module doesn't handle tokens of type ${token.type}`);
|
3874
3480
|
return [];
|
3875
3481
|
}
|
3876
|
-
const chainId = token.chain?.id;
|
3877
|
-
if (!chainId) {
|
3878
|
-
log.warn(`Token ${tokenId} has no chain`);
|
3879
|
-
return [];
|
3880
|
-
}
|
3881
|
-
const chain = chains[chainId];
|
3882
3482
|
if (!chain) {
|
3883
|
-
log.warn(`Chain ${
|
3483
|
+
log.warn(`Chain ${networkId} for token ${tokenId} not found`);
|
3884
3484
|
return [];
|
3885
3485
|
}
|
3886
3486
|
return addresses.flatMap(address => {
|
3887
|
-
const scaleCoder =
|
3487
|
+
const scaleCoder = networkStorageCoders?.storage;
|
3888
3488
|
const onChainId = (() => {
|
3889
3489
|
try {
|
3890
3490
|
return papiParse(token.onChainId);
|
@@ -3892,12 +3492,12 @@ async function buildQueries$2(chaindataProvider, addressesByToken) {
|
|
3892
3492
|
return token.onChainId;
|
3893
3493
|
}
|
3894
3494
|
})();
|
3895
|
-
const stateKey = encodeStateKey(scaleCoder, `Invalid address / token onChainId in ${
|
3495
|
+
const stateKey = encodeStateKey(scaleCoder, `Invalid address / token onChainId in ${networkId} storage query ${address} / ${token.onChainId}`, onChainId, address);
|
3896
3496
|
if (!stateKey) return [];
|
3897
3497
|
const decodeResult = change => {
|
3898
3498
|
/** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
|
3899
3499
|
|
3900
|
-
const decoded = decodeScale(scaleCoder, change, `Failed to decode substrate-foreignassets balance on chain ${
|
3500
|
+
const decoded = decodeScale(scaleCoder, change, `Failed to decode substrate-foreignassets balance on chain ${networkId}`) ?? {
|
3901
3501
|
balance: 0n,
|
3902
3502
|
status: {
|
3903
3503
|
type: "Liquid"
|
@@ -3931,29 +3531,54 @@ async function buildQueries$2(chaindataProvider, addressesByToken) {
|
|
3931
3531
|
source: "substrate-foreignassets",
|
3932
3532
|
status: "live",
|
3933
3533
|
address,
|
3934
|
-
|
3935
|
-
subChainId: chainId
|
3936
|
-
},
|
3937
|
-
chainId,
|
3534
|
+
networkId,
|
3938
3535
|
tokenId: token.id,
|
3939
3536
|
values: balanceValues
|
3940
3537
|
};
|
3941
3538
|
};
|
3942
3539
|
return {
|
3943
|
-
chainId,
|
3540
|
+
chainId: networkId,
|
3944
3541
|
stateKey,
|
3945
3542
|
decodeResult
|
3946
3543
|
};
|
3947
3544
|
});
|
3948
3545
|
});
|
3949
3546
|
}
|
3547
|
+
async function buildQueries$2(chainConnector, chaindataProvider, addressesByToken, signal) {
|
3548
|
+
const byNetwork = keys(addressesByToken).reduce((acc, tokenId) => {
|
3549
|
+
const networkId = parseSubForeignAssetTokenId(tokenId).networkId;
|
3550
|
+
if (!acc[networkId]) acc[networkId] = {};
|
3551
|
+
acc[networkId][tokenId] = addressesByToken[tokenId];
|
3552
|
+
return acc;
|
3553
|
+
}, {});
|
3554
|
+
return (await Promise.all(toPairs(byNetwork).map(([networkId, addressesByToken]) => {
|
3555
|
+
return buildNetworkQueries$1(networkId, chainConnector, chaindataProvider, addressesByToken, signal);
|
3556
|
+
}))).flat();
|
3557
|
+
}
|
3558
|
+
|
3559
|
+
const getAddresssesByTokenByNetwork = addressesByToken => {
|
3560
|
+
const addressesByTokenByNetwork = toPairs(addressesByToken).reduce((acc, [tokenId, addresses]) => {
|
3561
|
+
const networkId = parseTokenId(tokenId).networkId;
|
3562
|
+
if (!acc[networkId]) acc[networkId] = {};
|
3563
|
+
acc[networkId][tokenId] = addresses;
|
3564
|
+
return acc;
|
3565
|
+
}, {});
|
3566
|
+
return addressesByTokenByNetwork;
|
3567
|
+
};
|
3950
3568
|
|
3951
3569
|
async function subscribeBase(queries, chainConnector, callback) {
|
3952
|
-
|
3953
|
-
|
3954
|
-
|
3955
|
-
|
3956
|
-
|
3570
|
+
try {
|
3571
|
+
const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe((error, result) => {
|
3572
|
+
if (error) callback(error);
|
3573
|
+
if (result && result.length > 0) callback(null, result);
|
3574
|
+
});
|
3575
|
+
return unsubscribe;
|
3576
|
+
} catch (err) {
|
3577
|
+
if (!isAbortError(err)) log.error("Error subscribing to base queries", {
|
3578
|
+
err
|
3579
|
+
});
|
3580
|
+
return () => {};
|
3581
|
+
}
|
3957
3582
|
}
|
3958
3583
|
|
3959
3584
|
/**
|
@@ -3969,262 +3594,6 @@ const asObservable = handler => (...args) => new Observable(subscriber => {
|
|
3969
3594
|
return unsubscribe;
|
3970
3595
|
});
|
3971
3596
|
|
3972
|
-
/**
|
3973
|
-
* Crowdloan contributions are stored in the `childstate` key returned by this function.
|
3974
|
-
*/
|
3975
|
-
const crowdloanFundContributionsChildKey = fundIndex => u8aToHex(u8aConcat(":child_storage:default:", blake2AsU8a(u8aConcat("crowdloan", u32.enc(fundIndex)))));
|
3976
|
-
|
3977
|
-
async function subscribeCrowdloans(chaindataProvider, chainConnector, addressesByToken, callback) {
|
3978
|
-
const allChains = await chaindataProvider.chainsById();
|
3979
|
-
const tokens = await chaindataProvider.tokensById();
|
3980
|
-
const miniMetadatas = new Map((await db.miniMetadatas.toArray()).map(miniMetadata => [miniMetadata.id, miniMetadata]));
|
3981
|
-
const crowdloanTokenIds = Object.entries(tokens).filter(([, token]) => {
|
3982
|
-
// ignore non-native tokens
|
3983
|
-
if (token.type !== "substrate-native") return;
|
3984
|
-
// ignore tokens on chains with no crowdloans pallet
|
3985
|
-
const [chainMeta] = findChainMeta(miniMetadatas, "substrate-native", allChains[token.chain.id]);
|
3986
|
-
return typeof chainMeta?.crowdloanPalletId === "string";
|
3987
|
-
}).map(([tokenId]) => tokenId);
|
3988
|
-
|
3989
|
-
// crowdloan contributions can only be done by the native token on chains with the crowdloan pallet
|
3990
|
-
const addressesByCrowdloanToken = Object.fromEntries(Object.entries(addressesByToken)
|
3991
|
-
// remove ethereum addresses
|
3992
|
-
.map(([tokenId, addresses]) => [tokenId, addresses.filter(address => !isEthereumAddress(address))])
|
3993
|
-
// remove tokens which aren't crowdloan tokens
|
3994
|
-
.filter(([tokenId]) => crowdloanTokenIds.includes(tokenId)));
|
3995
|
-
const uniqueChainIds = getUniqueChainIds(addressesByCrowdloanToken, tokens);
|
3996
|
-
const chains = Object.fromEntries(Object.entries(allChains).filter(([chainId]) => uniqueChainIds.includes(chainId)));
|
3997
|
-
const chainStorageCoders = buildStorageCoders({
|
3998
|
-
chainIds: uniqueChainIds,
|
3999
|
-
chains,
|
4000
|
-
miniMetadatas,
|
4001
|
-
moduleType: "substrate-native",
|
4002
|
-
coders: {
|
4003
|
-
parachains: ["Paras", "Parachains"],
|
4004
|
-
funds: ["Crowdloan", "Funds"]
|
4005
|
-
}
|
4006
|
-
});
|
4007
|
-
const tokenSubscriptions = [];
|
4008
|
-
for (const [tokenId, addresses] of Object.entries(addressesByCrowdloanToken)) {
|
4009
|
-
const token = tokens[tokenId];
|
4010
|
-
if (!token) {
|
4011
|
-
log.warn(`Token ${tokenId} not found`);
|
4012
|
-
continue;
|
4013
|
-
}
|
4014
|
-
if (token.type !== "substrate-native") {
|
4015
|
-
log.debug(`This module doesn't handle tokens of type ${token.type}`);
|
4016
|
-
continue;
|
4017
|
-
}
|
4018
|
-
const chainId = token.chain?.id;
|
4019
|
-
if (!chainId) {
|
4020
|
-
log.warn(`Token ${tokenId} has no chain`);
|
4021
|
-
continue;
|
4022
|
-
}
|
4023
|
-
const chain = chains[chainId];
|
4024
|
-
if (!chain) {
|
4025
|
-
log.warn(`Chain ${chainId} for token ${tokenId} not found`);
|
4026
|
-
continue;
|
4027
|
-
}
|
4028
|
-
const subscribeParaIds = callback => {
|
4029
|
-
const scaleCoder = chainStorageCoders.get(chainId)?.parachains;
|
4030
|
-
const queries = [0].flatMap(() => {
|
4031
|
-
const stateKey = encodeStateKey(scaleCoder);
|
4032
|
-
if (!stateKey) return [];
|
4033
|
-
const decodeResult = change => {
|
4034
|
-
/** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
|
4035
|
-
|
4036
|
-
const decoded = decodeScale(scaleCoder, change, `Failed to decode parachains on chain ${chainId}`);
|
4037
|
-
const paraIds = decoded ?? [];
|
4038
|
-
return paraIds;
|
4039
|
-
};
|
4040
|
-
return {
|
4041
|
-
chainId,
|
4042
|
-
stateKey,
|
4043
|
-
decodeResult
|
4044
|
-
};
|
4045
|
-
});
|
4046
|
-
const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
|
4047
|
-
return () => subscription.then(unsubscribe => unsubscribe());
|
4048
|
-
};
|
4049
|
-
const subscribeParaFundIndexes = (paraIds, callback) => {
|
4050
|
-
const scaleCoder = chainStorageCoders.get(chainId)?.funds;
|
4051
|
-
const queries = paraIds.flatMap(paraId => {
|
4052
|
-
const stateKey = encodeStateKey(scaleCoder, `Invalid paraId in ${chainId} funds query ${paraId}`, paraId);
|
4053
|
-
if (!stateKey) return [];
|
4054
|
-
const decodeResult = change => {
|
4055
|
-
/** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
|
4056
|
-
|
4057
|
-
const decoded = decodeScale(scaleCoder, change, `Failed to decode paras on chain ${chainId}`);
|
4058
|
-
const firstPeriod = decoded?.first_period?.toString?.() ?? "";
|
4059
|
-
const lastPeriod = decoded?.last_period?.toString?.() ?? "";
|
4060
|
-
const fundPeriod = `${firstPeriod}-${lastPeriod}`;
|
4061
|
-
const fundIndex = decoded?.fund_index ?? decoded?.trie_index;
|
4062
|
-
return {
|
4063
|
-
paraId,
|
4064
|
-
fundPeriod,
|
4065
|
-
fundIndex
|
4066
|
-
};
|
4067
|
-
};
|
4068
|
-
return {
|
4069
|
-
chainId,
|
4070
|
-
stateKey,
|
4071
|
-
decodeResult
|
4072
|
-
};
|
4073
|
-
});
|
4074
|
-
const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
|
4075
|
-
return () => subscription.then(unsubscribe => unsubscribe());
|
4076
|
-
};
|
4077
|
-
const subscribeFundContributions = (funds, addresses, callback) => {
|
4078
|
-
// TODO: Watch system_events in order to subscribe to changes, then redo the contributions query when changes are detected:
|
4079
|
-
// https://github.com/polkadot-js/api/blob/8fe02a14345b57e6abb8f7f2c2b624cf70c51b23/packages/api-derive/src/crowdloan/ownContributions.ts#L32-L47
|
4080
|
-
//
|
4081
|
-
// For now we just re-fetch all contributions on a timer and then only send them to the subscription callback when they have changed
|
4082
|
-
|
4083
|
-
const queries = funds.map(({
|
4084
|
-
paraId,
|
4085
|
-
fundIndex
|
4086
|
-
}) => ({
|
4087
|
-
paraId,
|
4088
|
-
fundIndex,
|
4089
|
-
addresses,
|
4090
|
-
childKey: crowdloanFundContributionsChildKey(fundIndex),
|
4091
|
-
storageKeys: addresses.map(address => u8aToHex(decodeAnyAddress(address)))
|
4092
|
-
}));
|
4093
|
-
|
4094
|
-
// track whether our caller is still subscribed
|
4095
|
-
let subscriptionActive = true;
|
4096
|
-
let previousContributions = null;
|
4097
|
-
const fetchContributions = async () => {
|
4098
|
-
try {
|
4099
|
-
const results = await Promise.all(queries.map(async ({
|
4100
|
-
paraId,
|
4101
|
-
fundIndex,
|
4102
|
-
addresses,
|
4103
|
-
childKey,
|
4104
|
-
storageKeys
|
4105
|
-
}) => ({
|
4106
|
-
paraId,
|
4107
|
-
fundIndex,
|
4108
|
-
addresses,
|
4109
|
-
result: await chainConnector.send(chainId, "childstate_getStorageEntries", [childKey, storageKeys])
|
4110
|
-
})));
|
4111
|
-
const contributions = results.flatMap(queryResult => {
|
4112
|
-
const {
|
4113
|
-
paraId,
|
4114
|
-
fundIndex,
|
4115
|
-
addresses,
|
4116
|
-
result
|
4117
|
-
} = queryResult;
|
4118
|
-
return (Array.isArray(result) ? result : []).flatMap((encoded, index) => {
|
4119
|
-
const amount = (() => {
|
4120
|
-
try {
|
4121
|
-
return typeof encoded === "string" ? u128.dec(encoded) ?? 0n : 0n;
|
4122
|
-
} catch {
|
4123
|
-
return 0n;
|
4124
|
-
}
|
4125
|
-
})().toString();
|
4126
|
-
return {
|
4127
|
-
paraId,
|
4128
|
-
fundIndex,
|
4129
|
-
address: addresses[index],
|
4130
|
-
amount
|
4131
|
-
};
|
4132
|
-
});
|
4133
|
-
});
|
4134
|
-
|
4135
|
-
// ignore these results if our caller has tried to close this subscription
|
4136
|
-
if (!subscriptionActive) return;
|
4137
|
-
|
4138
|
-
// ignore these results if they're the same as what we previously fetched
|
4139
|
-
if (isEqual(previousContributions, contributions)) return;
|
4140
|
-
previousContributions = contributions;
|
4141
|
-
callback(null, contributions);
|
4142
|
-
} catch (error) {
|
4143
|
-
callback(error);
|
4144
|
-
}
|
4145
|
-
};
|
4146
|
-
|
4147
|
-
// set up polling for contributions
|
4148
|
-
const crowdloanContributionsPollInterval = 60_000; // 60_000ms === 1 minute
|
4149
|
-
const pollContributions = async () => {
|
4150
|
-
if (!subscriptionActive) return;
|
4151
|
-
try {
|
4152
|
-
await fetchContributions();
|
4153
|
-
} catch (error) {
|
4154
|
-
// log any errors, but don't cancel the poll for contributions when one fetch fails
|
4155
|
-
log.error(error);
|
4156
|
-
}
|
4157
|
-
if (!subscriptionActive) return;
|
4158
|
-
setTimeout(pollContributions, crowdloanContributionsPollInterval);
|
4159
|
-
};
|
4160
|
-
|
4161
|
-
// start polling
|
4162
|
-
pollContributions();
|
4163
|
-
return () => {
|
4164
|
-
// stop polling
|
4165
|
-
subscriptionActive = false;
|
4166
|
-
};
|
4167
|
-
};
|
4168
|
-
const paraIds$ = asObservable(subscribeParaIds)().pipe(scan((_, next) => Array.from(new Set(next.flatMap(paraIds => paraIds))), []), share());
|
4169
|
-
const fundIndexesByParaId$ = paraIds$.pipe(map(paraIds => asObservable(subscribeParaFundIndexes)(paraIds)), switchAll(), scan((state, next) => {
|
4170
|
-
for (const fund of next) {
|
4171
|
-
const {
|
4172
|
-
paraId,
|
4173
|
-
fundIndex
|
4174
|
-
} = fund;
|
4175
|
-
if (typeof fundIndex === "number") {
|
4176
|
-
state.set(paraId, (state.get(paraId) ?? new Set()).add(fundIndex));
|
4177
|
-
}
|
4178
|
-
}
|
4179
|
-
return state;
|
4180
|
-
}, new Map()));
|
4181
|
-
const contributionsByAddress$ = fundIndexesByParaId$.pipe(map(fundIndexesByParaId => Array.from(fundIndexesByParaId).flatMap(([paraId, fundIndexes]) => Array.from(fundIndexes).map(fundIndex => ({
|
4182
|
-
paraId,
|
4183
|
-
fundIndex
|
4184
|
-
})))), map(funds => asObservable(subscribeFundContributions)(funds, addresses)), switchAll(), scan((state, next) => {
|
4185
|
-
for (const contribution of next) {
|
4186
|
-
const {
|
4187
|
-
address
|
4188
|
-
} = contribution;
|
4189
|
-
state.set(address, (state.get(address) ?? new Set()).add(contribution));
|
4190
|
-
}
|
4191
|
-
return state;
|
4192
|
-
}, new Map()));
|
4193
|
-
const subscription = contributionsByAddress$.subscribe({
|
4194
|
-
next: contributionsByAddress => {
|
4195
|
-
const balances = Array.from(contributionsByAddress).map(([address, contributions]) => {
|
4196
|
-
return {
|
4197
|
-
source: "substrate-native",
|
4198
|
-
status: "live",
|
4199
|
-
address,
|
4200
|
-
multiChainId: {
|
4201
|
-
subChainId: chainId
|
4202
|
-
},
|
4203
|
-
chainId,
|
4204
|
-
tokenId,
|
4205
|
-
values: Array.from(contributions).map(({
|
4206
|
-
amount,
|
4207
|
-
paraId
|
4208
|
-
}) => ({
|
4209
|
-
type: "crowdloan",
|
4210
|
-
label: "crowdloan",
|
4211
|
-
source: "crowdloan",
|
4212
|
-
amount: amount,
|
4213
|
-
meta: {
|
4214
|
-
paraId
|
4215
|
-
}
|
4216
|
-
}))
|
4217
|
-
};
|
4218
|
-
});
|
4219
|
-
if (balances.length > 0) callback(null, balances);
|
4220
|
-
},
|
4221
|
-
error: error => callback(error)
|
4222
|
-
});
|
4223
|
-
tokenSubscriptions.push(() => subscription.unsubscribe());
|
4224
|
-
}
|
4225
|
-
return () => tokenSubscriptions.forEach(unsub => unsub());
|
4226
|
-
}
|
4227
|
-
|
4228
3597
|
/**
|
4229
3598
|
* Each nominationPool in the nominationPools pallet has access to some accountIds which have no
|
4230
3599
|
* associated private key. Instead, they are derived from this function.
|
@@ -4244,280 +3613,293 @@ const nompoolAccountId = (palletId, poolId, index) => {
|
|
4244
3613
|
/** The stash account for the nomination pool */
|
4245
3614
|
const nompoolStashAccountId = (palletId, poolId) => nompoolAccountId(palletId, poolId, 0);
|
4246
3615
|
|
4247
|
-
|
4248
|
-
|
4249
|
-
|
4250
|
-
|
4251
|
-
|
4252
|
-
|
4253
|
-
|
4254
|
-
|
4255
|
-
const
|
4256
|
-
|
4257
|
-
|
4258
|
-
|
4259
|
-
// staking can only be done by the native token on chains with the staking pallet
|
4260
|
-
const addressesByNomPoolToken = Object.fromEntries(Object.entries(addressesByToken)
|
4261
|
-
// remove ethereum addresses
|
4262
|
-
.map(([tokenId, addresses]) => [tokenId, addresses.filter(address => !isEthereumAddress(address))])
|
4263
|
-
// remove tokens which aren't nom pool tokens
|
4264
|
-
.filter(([tokenId]) => nomPoolTokenIds.includes(tokenId)));
|
4265
|
-
const uniqueChainIds = getUniqueChainIds(addressesByNomPoolToken, tokens);
|
4266
|
-
const chains = Object.fromEntries(Object.entries(allChains).filter(([chainId]) => uniqueChainIds.includes(chainId)));
|
4267
|
-
const chainStorageCoders = buildStorageCoders({
|
4268
|
-
chainIds: uniqueChainIds,
|
4269
|
-
chains,
|
4270
|
-
miniMetadatas,
|
4271
|
-
moduleType: "substrate-native",
|
4272
|
-
coders: {
|
4273
|
-
poolMembers: ["NominationPools", "PoolMembers"],
|
4274
|
-
bondedPools: ["NominationPools", "BondedPools"],
|
4275
|
-
ledger: ["Staking", "Ledger"],
|
4276
|
-
metadata: ["NominationPools", "Metadata"]
|
4277
|
-
}
|
4278
|
-
});
|
4279
|
-
const resultUnsubscribes = [];
|
4280
|
-
for (const [tokenId, addresses] of Object.entries(addressesByNomPoolToken)) {
|
4281
|
-
const token = tokens[tokenId];
|
4282
|
-
if (!token) {
|
4283
|
-
log.warn(`Token ${tokenId} not found`);
|
4284
|
-
continue;
|
4285
|
-
}
|
4286
|
-
if (token.type !== "substrate-native") {
|
4287
|
-
log.debug(`This module doesn't handle tokens of type ${token.type}`);
|
4288
|
-
continue;
|
4289
|
-
}
|
4290
|
-
const chainId = token.chain?.id;
|
4291
|
-
if (!chainId) {
|
4292
|
-
log.warn(`Token ${tokenId} has no chain`);
|
4293
|
-
continue;
|
4294
|
-
}
|
4295
|
-
const chain = chains[chainId];
|
4296
|
-
if (!chain) {
|
4297
|
-
log.warn(`Chain ${chainId} for token ${tokenId} not found`);
|
4298
|
-
continue;
|
3616
|
+
// TODO make this method chain-specific
|
3617
|
+
async function subscribeNompoolStaking(chaindataProvider, chainConnector, addressesByToken, callback, signal) {
|
3618
|
+
try {
|
3619
|
+
const allChains = await chaindataProvider.getNetworksMapById("polkadot");
|
3620
|
+
const tokens = await chaindataProvider.getTokensMapById();
|
3621
|
+
|
3622
|
+
// there should be only one network here when subscribing to balances, we've split it up by network at the top level
|
3623
|
+
const networkIds = keys(addressesByToken).map(tokenId => parseTokenId(tokenId).networkId);
|
3624
|
+
const miniMetadatas = new Map();
|
3625
|
+
for (const networkId of networkIds) {
|
3626
|
+
const miniMetadata = await getMiniMetadata(chaindataProvider, chainConnector, networkId, "substrate-native");
|
3627
|
+
miniMetadatas.set(networkId, miniMetadata);
|
4299
3628
|
}
|
4300
|
-
|
4301
|
-
const {
|
4302
|
-
|
4303
|
-
|
4304
|
-
|
4305
|
-
|
4306
|
-
const
|
4307
|
-
|
4308
|
-
|
4309
|
-
|
4310
|
-
|
4311
|
-
|
4312
|
-
|
4313
|
-
|
4314
|
-
|
4315
|
-
|
4316
|
-
|
4317
|
-
|
4318
|
-
|
4319
|
-
|
4320
|
-
|
3629
|
+
signal?.throwIfAborted();
|
3630
|
+
const nomPoolTokenIds = Object.entries(tokens).filter(([, token]) => {
|
3631
|
+
// ignore non-native tokens
|
3632
|
+
if (token.type !== "substrate-native") return false;
|
3633
|
+
|
3634
|
+
// ignore tokens on chains with no nompools pallet
|
3635
|
+
const miniMetadata = miniMetadatas.get(token.networkId);
|
3636
|
+
return typeof miniMetadata?.extra?.nominationPoolsPalletId === "string";
|
3637
|
+
}).map(([tokenId]) => tokenId);
|
3638
|
+
|
3639
|
+
// staking can only be done by the native token on chains with the staking pallet
|
3640
|
+
const addressesByNomPoolToken = Object.fromEntries(Object.entries(addressesByToken)
|
3641
|
+
// remove ethereum addresses
|
3642
|
+
.map(([tokenId, addresses]) => [tokenId, addresses.filter(address => !isEthereumAddress(address))])
|
3643
|
+
// remove tokens which aren't nom pool tokens
|
3644
|
+
.filter(([tokenId]) => nomPoolTokenIds.includes(tokenId)));
|
3645
|
+
const uniqueChainIds = getUniqueChainIds(addressesByNomPoolToken, tokens);
|
3646
|
+
const chains = Object.fromEntries(Object.entries(allChains).filter(([chainId]) => uniqueChainIds.includes(chainId)));
|
3647
|
+
const chainStorageCoders = buildStorageCoders({
|
3648
|
+
chainIds: uniqueChainIds,
|
3649
|
+
chains,
|
3650
|
+
miniMetadatas,
|
3651
|
+
coders: {
|
3652
|
+
poolMembers: ["NominationPools", "PoolMembers"],
|
3653
|
+
bondedPools: ["NominationPools", "BondedPools"],
|
3654
|
+
ledger: ["Staking", "Ledger"],
|
3655
|
+
metadata: ["NominationPools", "Metadata"]
|
3656
|
+
}
|
3657
|
+
});
|
3658
|
+
const resultUnsubscribes = [];
|
3659
|
+
for (const [tokenId, addresses] of Object.entries(addressesByNomPoolToken)) {
|
3660
|
+
const token = tokens[tokenId];
|
3661
|
+
if (!token) {
|
3662
|
+
log.warn(`Token ${tokenId} not found`);
|
3663
|
+
continue;
|
3664
|
+
}
|
3665
|
+
if (token.type !== "substrate-native") {
|
3666
|
+
log.debug(`This module doesn't handle tokens of type ${token.type}`);
|
3667
|
+
continue;
|
3668
|
+
}
|
3669
|
+
const chainId = token.networkId;
|
3670
|
+
if (!chainId) {
|
3671
|
+
log.warn(`Token ${tokenId} has no chain`);
|
3672
|
+
continue;
|
3673
|
+
}
|
3674
|
+
const chain = chains[chainId];
|
3675
|
+
if (!chain) {
|
3676
|
+
log.warn(`Chain ${chainId} for token ${tokenId} not found`);
|
3677
|
+
continue;
|
3678
|
+
}
|
3679
|
+
const miniMetadata = miniMetadatas.get(chainId);
|
3680
|
+
const {
|
3681
|
+
nominationPoolsPalletId
|
3682
|
+
} = miniMetadata?.extra ?? {};
|
3683
|
+
const subscribePoolMembers = (addresses, callback) => {
|
3684
|
+
const scaleCoder = chainStorageCoders.get(chainId)?.poolMembers;
|
3685
|
+
const queries = addresses.flatMap(address => {
|
3686
|
+
const stateKey = encodeStateKey(scaleCoder, `Invalid address in ${chainId} poolMembers query ${address}`, address);
|
3687
|
+
if (!stateKey) return [];
|
3688
|
+
const decodeResult = change => {
|
3689
|
+
/** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
|
3690
|
+
|
3691
|
+
const decoded = decodeScale(scaleCoder, change, `Failed to decode poolMembers on chain ${chainId}`);
|
3692
|
+
const poolId = decoded?.pool_id?.toString?.();
|
3693
|
+
const points = decoded?.points?.toString?.();
|
3694
|
+
const unbondingEras = Array.from(decoded?.unbonding_eras ?? []).flatMap(entry => {
|
3695
|
+
if (entry === undefined) return [];
|
3696
|
+
const [key, value] = Array.from(entry);
|
3697
|
+
const era = key?.toString?.();
|
3698
|
+
const amount = value?.toString?.();
|
3699
|
+
if (typeof era !== "string" || typeof amount !== "string") return [];
|
3700
|
+
return {
|
3701
|
+
era,
|
3702
|
+
amount
|
3703
|
+
};
|
3704
|
+
});
|
4321
3705
|
return {
|
4322
|
-
|
4323
|
-
|
3706
|
+
tokenId,
|
3707
|
+
address,
|
3708
|
+
poolId,
|
3709
|
+
points,
|
3710
|
+
unbondingEras
|
4324
3711
|
};
|
4325
|
-
}
|
3712
|
+
};
|
4326
3713
|
return {
|
4327
|
-
|
4328
|
-
|
3714
|
+
chainId,
|
3715
|
+
stateKey,
|
3716
|
+
decodeResult
|
3717
|
+
};
|
3718
|
+
});
|
3719
|
+
const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
|
3720
|
+
return () => subscription.then(unsubscribe => unsubscribe());
|
3721
|
+
};
|
3722
|
+
const subscribePoolPoints = (poolIds, callback) => {
|
3723
|
+
if (poolIds.length === 0) callback(null, []);
|
3724
|
+
const scaleCoder = chainStorageCoders.get(chainId)?.bondedPools;
|
3725
|
+
const queries = poolIds.flatMap(poolId => {
|
3726
|
+
const stateKey = encodeStateKey(scaleCoder, `Invalid poolId in ${chainId} bondedPools query ${poolId}`, poolId);
|
3727
|
+
if (!stateKey) return [];
|
3728
|
+
const decodeResult = change => {
|
3729
|
+
/** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
|
3730
|
+
|
3731
|
+
const decoded = decodeScale(scaleCoder, change, `Failed to decode bondedPools on chain ${chainId}`);
|
3732
|
+
const points = decoded?.points?.toString?.();
|
3733
|
+
return {
|
3734
|
+
poolId,
|
3735
|
+
points
|
3736
|
+
};
|
3737
|
+
};
|
3738
|
+
return {
|
3739
|
+
chainId,
|
3740
|
+
stateKey,
|
3741
|
+
decodeResult
|
3742
|
+
};
|
3743
|
+
});
|
3744
|
+
const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
|
3745
|
+
return () => subscription.then(unsubscribe => unsubscribe());
|
3746
|
+
};
|
3747
|
+
const subscribePoolStake = (poolIds, callback) => {
|
3748
|
+
if (poolIds.length === 0) callback(null, []);
|
3749
|
+
const scaleCoder = chainStorageCoders.get(chainId)?.ledger;
|
3750
|
+
const queries = poolIds.flatMap(poolId => {
|
3751
|
+
if (!nominationPoolsPalletId) return [];
|
3752
|
+
const stashAddress = nompoolStashAccountId(nominationPoolsPalletId, poolId);
|
3753
|
+
const stateKey = encodeStateKey(scaleCoder, `Invalid address in ${chainId} ledger query ${stashAddress}`, stashAddress);
|
3754
|
+
if (!stateKey) return [];
|
3755
|
+
const decodeResult = change => {
|
3756
|
+
/** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
|
3757
|
+
|
3758
|
+
const decoded = decodeScale(scaleCoder, change, `Failed to decode ledger on chain ${chainId}`);
|
3759
|
+
const activeStake = decoded?.active?.toString?.();
|
3760
|
+
return {
|
3761
|
+
poolId,
|
3762
|
+
activeStake
|
3763
|
+
};
|
3764
|
+
};
|
3765
|
+
return {
|
3766
|
+
chainId,
|
3767
|
+
stateKey,
|
3768
|
+
decodeResult
|
3769
|
+
};
|
3770
|
+
});
|
3771
|
+
const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
|
3772
|
+
return () => subscription.then(unsubscribe => unsubscribe());
|
3773
|
+
};
|
3774
|
+
const subscribePoolMetadata = (poolIds, callback) => {
|
3775
|
+
if (poolIds.length === 0) callback(null, []);
|
3776
|
+
const scaleCoder = chainStorageCoders.get(chainId)?.metadata;
|
3777
|
+
const queries = poolIds.flatMap(poolId => {
|
3778
|
+
if (!nominationPoolsPalletId) return [];
|
3779
|
+
const stateKey = encodeStateKey(scaleCoder, `Invalid poolId in ${chainId} metadata query ${poolId}`, poolId);
|
3780
|
+
if (!stateKey) return [];
|
3781
|
+
const decodeResult = change => {
|
3782
|
+
/** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
|
3783
|
+
|
3784
|
+
const decoded = decodeScale(scaleCoder, change, `Failed to decode metadata on chain ${chainId}`);
|
3785
|
+
const metadata = decoded?.asText?.();
|
3786
|
+
return {
|
3787
|
+
poolId,
|
3788
|
+
metadata
|
3789
|
+
};
|
3790
|
+
};
|
3791
|
+
return {
|
3792
|
+
chainId,
|
3793
|
+
stateKey,
|
3794
|
+
decodeResult
|
3795
|
+
};
|
3796
|
+
});
|
3797
|
+
const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
|
3798
|
+
return () => subscription.then(unsubscribe => unsubscribe());
|
3799
|
+
};
|
3800
|
+
const poolMembersByAddress$ = asObservable(subscribePoolMembers)(addresses).pipe(scan((state, next) => {
|
3801
|
+
for (const poolMembers of next) {
|
3802
|
+
const {
|
4329
3803
|
poolId,
|
4330
3804
|
points,
|
4331
3805
|
unbondingEras
|
4332
|
-
};
|
4333
|
-
|
4334
|
-
|
4335
|
-
|
4336
|
-
|
4337
|
-
|
4338
|
-
}
|
4339
|
-
|
4340
|
-
|
4341
|
-
|
4342
|
-
|
4343
|
-
|
4344
|
-
|
4345
|
-
const
|
4346
|
-
|
4347
|
-
|
4348
|
-
if (!stateKey) return [];
|
4349
|
-
const decodeResult = change => {
|
4350
|
-
/** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
|
4351
|
-
|
4352
|
-
const decoded = decodeScale(scaleCoder, change, `Failed to decode bondedPools on chain ${chainId}`);
|
4353
|
-
const points = decoded?.points?.toString?.();
|
4354
|
-
return {
|
3806
|
+
} = poolMembers;
|
3807
|
+
if (typeof poolId === "string" && typeof points === "string") state.set(poolMembers.address, {
|
3808
|
+
poolId,
|
3809
|
+
points,
|
3810
|
+
unbondingEras
|
3811
|
+
});else state.set(poolMembers.address, null);
|
3812
|
+
}
|
3813
|
+
return state;
|
3814
|
+
}, new Map()), share());
|
3815
|
+
const poolIdByAddress$ = poolMembersByAddress$.pipe(map(pm => new Map(Array.from(pm).map(([address, pm]) => [address, pm?.poolId ?? null]))));
|
3816
|
+
const pointsByAddress$ = poolMembersByAddress$.pipe(map(pm => new Map(Array.from(pm).map(([address, pm]) => [address, pm?.points ?? null]))));
|
3817
|
+
const unbondingErasByAddress$ = poolMembersByAddress$.pipe(map(pm => new Map(Array.from(pm).map(([address, pm]) => [address, pm?.unbondingEras ?? null]))));
|
3818
|
+
const poolIds$ = poolIdByAddress$.pipe(map(byAddress => [...new Set(Array.from(byAddress.values()).flatMap(poolId => poolId ?? []))]));
|
3819
|
+
const pointsByPool$ = poolIds$.pipe(map(poolIds => asObservable(subscribePoolPoints)(poolIds)), switchAll(), scan((state, next) => {
|
3820
|
+
for (const poolPoints of next) {
|
3821
|
+
const {
|
4355
3822
|
poolId,
|
4356
3823
|
points
|
4357
|
-
};
|
4358
|
-
|
4359
|
-
|
4360
|
-
|
4361
|
-
|
4362
|
-
|
4363
|
-
|
4364
|
-
|
4365
|
-
const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
|
4366
|
-
return () => subscription.then(unsubscribe => unsubscribe());
|
4367
|
-
};
|
4368
|
-
const subscribePoolStake = (poolIds, callback) => {
|
4369
|
-
if (poolIds.length === 0) callback(null, []);
|
4370
|
-
const scaleCoder = chainStorageCoders.get(chainId)?.ledger;
|
4371
|
-
const queries = poolIds.flatMap(poolId => {
|
4372
|
-
if (!nominationPoolsPalletId) return [];
|
4373
|
-
const stashAddress = nompoolStashAccountId(nominationPoolsPalletId, poolId);
|
4374
|
-
const stateKey = encodeStateKey(scaleCoder, `Invalid address in ${chainId} ledger query ${stashAddress}`, stashAddress);
|
4375
|
-
if (!stateKey) return [];
|
4376
|
-
const decodeResult = change => {
|
4377
|
-
/** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
|
4378
|
-
|
4379
|
-
const decoded = decodeScale(scaleCoder, change, `Failed to decode ledger on chain ${chainId}`);
|
4380
|
-
const activeStake = decoded?.active?.toString?.();
|
4381
|
-
return {
|
3824
|
+
} = poolPoints;
|
3825
|
+
if (typeof points === "string") state.set(poolId, points);else state.delete(poolId);
|
3826
|
+
}
|
3827
|
+
return state;
|
3828
|
+
}, new Map()));
|
3829
|
+
const stakeByPool$ = poolIds$.pipe(map(poolIds => asObservable(subscribePoolStake)(poolIds)), switchAll(), scan((state, next) => {
|
3830
|
+
for (const poolStake of next) {
|
3831
|
+
const {
|
4382
3832
|
poolId,
|
4383
3833
|
activeStake
|
4384
|
-
};
|
4385
|
-
|
4386
|
-
|
4387
|
-
|
4388
|
-
|
4389
|
-
|
4390
|
-
|
4391
|
-
|
4392
|
-
const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
|
4393
|
-
return () => subscription.then(unsubscribe => unsubscribe());
|
4394
|
-
};
|
4395
|
-
const subscribePoolMetadata = (poolIds, callback) => {
|
4396
|
-
if (poolIds.length === 0) callback(null, []);
|
4397
|
-
const scaleCoder = chainStorageCoders.get(chainId)?.metadata;
|
4398
|
-
const queries = poolIds.flatMap(poolId => {
|
4399
|
-
if (!nominationPoolsPalletId) return [];
|
4400
|
-
const stateKey = encodeStateKey(scaleCoder, `Invalid poolId in ${chainId} metadata query ${poolId}`, poolId);
|
4401
|
-
if (!stateKey) return [];
|
4402
|
-
const decodeResult = change => {
|
4403
|
-
/** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
|
4404
|
-
|
4405
|
-
const decoded = decodeScale(scaleCoder, change, `Failed to decode metadata on chain ${chainId}`);
|
4406
|
-
const metadata = decoded?.asText?.();
|
4407
|
-
return {
|
3834
|
+
} = poolStake;
|
3835
|
+
if (typeof activeStake === "string") state.set(poolId, activeStake);else state.delete(poolId);
|
3836
|
+
}
|
3837
|
+
return state;
|
3838
|
+
}, new Map()));
|
3839
|
+
const metadataByPool$ = poolIds$.pipe(map(poolIds => asObservable(subscribePoolMetadata)(poolIds)), switchAll(), scan((state, next) => {
|
3840
|
+
for (const poolMetadata of next) {
|
3841
|
+
const {
|
4408
3842
|
poolId,
|
4409
3843
|
metadata
|
4410
|
-
};
|
4411
|
-
|
4412
|
-
|
4413
|
-
|
4414
|
-
|
4415
|
-
|
4416
|
-
|
4417
|
-
|
4418
|
-
|
4419
|
-
|
4420
|
-
|
4421
|
-
|
4422
|
-
|
4423
|
-
|
4424
|
-
|
4425
|
-
|
4426
|
-
|
4427
|
-
|
4428
|
-
|
4429
|
-
|
4430
|
-
|
4431
|
-
|
4432
|
-
|
4433
|
-
|
4434
|
-
|
4435
|
-
}, new Map()), share());
|
4436
|
-
const poolIdByAddress$ = poolMembersByAddress$.pipe(map(pm => new Map(Array.from(pm).map(([address, pm]) => [address, pm?.poolId ?? null]))));
|
4437
|
-
const pointsByAddress$ = poolMembersByAddress$.pipe(map(pm => new Map(Array.from(pm).map(([address, pm]) => [address, pm?.points ?? null]))));
|
4438
|
-
const unbondingErasByAddress$ = poolMembersByAddress$.pipe(map(pm => new Map(Array.from(pm).map(([address, pm]) => [address, pm?.unbondingEras ?? null]))));
|
4439
|
-
const poolIds$ = poolIdByAddress$.pipe(map(byAddress => [...new Set(Array.from(byAddress.values()).flatMap(poolId => poolId ?? []))]));
|
4440
|
-
const pointsByPool$ = poolIds$.pipe(map(poolIds => asObservable(subscribePoolPoints)(poolIds)), switchAll(), scan((state, next) => {
|
4441
|
-
for (const poolPoints of next) {
|
4442
|
-
const {
|
4443
|
-
poolId,
|
4444
|
-
points
|
4445
|
-
} = poolPoints;
|
4446
|
-
if (typeof points === "string") state.set(poolId, points);else state.delete(poolId);
|
4447
|
-
}
|
4448
|
-
return state;
|
4449
|
-
}, new Map()));
|
4450
|
-
const stakeByPool$ = poolIds$.pipe(map(poolIds => asObservable(subscribePoolStake)(poolIds)), switchAll(), scan((state, next) => {
|
4451
|
-
for (const poolStake of next) {
|
4452
|
-
const {
|
4453
|
-
poolId,
|
4454
|
-
activeStake
|
4455
|
-
} = poolStake;
|
4456
|
-
if (typeof activeStake === "string") state.set(poolId, activeStake);else state.delete(poolId);
|
4457
|
-
}
|
4458
|
-
return state;
|
4459
|
-
}, new Map()));
|
4460
|
-
const metadataByPool$ = poolIds$.pipe(map(poolIds => asObservable(subscribePoolMetadata)(poolIds)), switchAll(), scan((state, next) => {
|
4461
|
-
for (const poolMetadata of next) {
|
4462
|
-
const {
|
4463
|
-
poolId,
|
4464
|
-
metadata
|
4465
|
-
} = poolMetadata;
|
4466
|
-
if (typeof metadata === "string") state.set(poolId, metadata);else state.delete(poolId);
|
4467
|
-
}
|
4468
|
-
return state;
|
4469
|
-
}, new Map()));
|
4470
|
-
const subscription = combineLatest([poolIdByAddress$, pointsByAddress$, unbondingErasByAddress$, pointsByPool$, stakeByPool$, metadataByPool$]).subscribe({
|
4471
|
-
next: ([poolIdByAddress, pointsByAddress, unbondingErasByAddress, pointsByPool, stakeByPool, metadataByPool]) => {
|
4472
|
-
const balances = Array.from(poolIdByAddress).map(([address, poolId]) => {
|
4473
|
-
const parsedPoolId = poolId === null ? undefined : parseInt(poolId);
|
4474
|
-
const points = pointsByAddress.get(address) ?? "0";
|
4475
|
-
const poolPoints = pointsByPool.get(poolId ?? "") ?? "0";
|
4476
|
-
const poolStake = stakeByPool.get(poolId ?? "") ?? "0";
|
4477
|
-
const poolMetadata = poolId ? metadataByPool.get(poolId) ?? `Pool ${poolId}` : undefined;
|
4478
|
-
const amount = points === "0" || poolPoints === "0" || poolStake === "0" ? 0n : BigInt(poolStake) * BigInt(points) / BigInt(poolPoints);
|
4479
|
-
const unbondingAmount = (unbondingErasByAddress.get(address) ?? []).reduce((total, {
|
4480
|
-
amount
|
4481
|
-
}) => total + BigInt(amount ?? "0"), 0n);
|
4482
|
-
return {
|
4483
|
-
source: "substrate-native",
|
4484
|
-
status: "live",
|
4485
|
-
address,
|
4486
|
-
multiChainId: {
|
4487
|
-
subChainId: chainId
|
4488
|
-
},
|
4489
|
-
chainId,
|
4490
|
-
tokenId,
|
4491
|
-
values: [{
|
4492
|
-
source: "nompools-staking",
|
4493
|
-
type: "nompool",
|
4494
|
-
label: "nompools-staking",
|
4495
|
-
amount: amount.toString(),
|
4496
|
-
meta: {
|
3844
|
+
} = poolMetadata;
|
3845
|
+
if (typeof metadata === "string") state.set(poolId, metadata);else state.delete(poolId);
|
3846
|
+
}
|
3847
|
+
return state;
|
3848
|
+
}, new Map()));
|
3849
|
+
const subscription = combineLatest([poolIdByAddress$, pointsByAddress$, unbondingErasByAddress$, pointsByPool$, stakeByPool$, metadataByPool$]).subscribe({
|
3850
|
+
next: ([poolIdByAddress, pointsByAddress, unbondingErasByAddress, pointsByPool, stakeByPool, metadataByPool]) => {
|
3851
|
+
const balances = Array.from(poolIdByAddress).map(([address, poolId]) => {
|
3852
|
+
const parsedPoolId = poolId === null ? undefined : parseInt(poolId);
|
3853
|
+
const points = pointsByAddress.get(address) ?? "0";
|
3854
|
+
const poolPoints = pointsByPool.get(poolId ?? "") ?? "0";
|
3855
|
+
const poolStake = stakeByPool.get(poolId ?? "") ?? "0";
|
3856
|
+
const poolMetadata = poolId ? metadataByPool.get(poolId) ?? `Pool ${poolId}` : undefined;
|
3857
|
+
const amount = points === "0" || poolPoints === "0" || poolStake === "0" ? 0n : BigInt(poolStake) * BigInt(points) / BigInt(poolPoints);
|
3858
|
+
const unbondingAmount = (unbondingErasByAddress.get(address) ?? []).reduce((total, {
|
3859
|
+
amount
|
3860
|
+
}) => total + BigInt(amount ?? "0"), 0n);
|
3861
|
+
return {
|
3862
|
+
source: "substrate-native",
|
3863
|
+
status: "live",
|
3864
|
+
address,
|
3865
|
+
networkId: chainId,
|
3866
|
+
tokenId,
|
3867
|
+
values: [{
|
3868
|
+
source: "nompools-staking",
|
4497
3869
|
type: "nompool",
|
4498
|
-
|
4499
|
-
|
4500
|
-
|
4501
|
-
|
4502
|
-
|
4503
|
-
|
4504
|
-
|
4505
|
-
|
4506
|
-
|
4507
|
-
|
4508
|
-
|
4509
|
-
|
4510
|
-
|
4511
|
-
|
4512
|
-
|
4513
|
-
|
4514
|
-
|
4515
|
-
|
4516
|
-
|
3870
|
+
label: "nompools-staking",
|
3871
|
+
amount: amount.toString(),
|
3872
|
+
meta: {
|
3873
|
+
type: "nompool",
|
3874
|
+
poolId: parsedPoolId,
|
3875
|
+
description: poolMetadata
|
3876
|
+
}
|
3877
|
+
}, {
|
3878
|
+
source: "nompools-staking",
|
3879
|
+
type: "nompool",
|
3880
|
+
label: "nompools-unbonding",
|
3881
|
+
amount: unbondingAmount.toString(),
|
3882
|
+
meta: {
|
3883
|
+
poolId: parsedPoolId,
|
3884
|
+
description: poolMetadata,
|
3885
|
+
unbonding: true
|
3886
|
+
}
|
3887
|
+
}]
|
3888
|
+
};
|
3889
|
+
}).filter(isNotNil);
|
3890
|
+
if (balances.length > 0) callback(null, balances);
|
3891
|
+
},
|
3892
|
+
error: error => callback(error)
|
3893
|
+
});
|
3894
|
+
resultUnsubscribes.push(() => subscription.unsubscribe());
|
3895
|
+
}
|
3896
|
+
return () => resultUnsubscribes.forEach(unsub => unsub());
|
3897
|
+
} catch (err) {
|
3898
|
+
if (!isAbortError(err)) log.error("Error subscribing to nom pool staking", {
|
3899
|
+
err
|
4517
3900
|
});
|
4518
|
-
|
3901
|
+
return () => {};
|
4519
3902
|
}
|
4520
|
-
return () => resultUnsubscribes.forEach(unsub => unsub());
|
4521
3903
|
}
|
4522
3904
|
|
4523
3905
|
const SUBTENSOR_ROOT_NETUID = 0;
|
@@ -4554,230 +3936,241 @@ const calculateTaoFromDynamicInfo = ({
|
|
4554
3936
|
dynamicInfo
|
4555
3937
|
});
|
4556
3938
|
return calculateTaoAmountFromAlpha({
|
4557
|
-
alphaPrice,
|
4558
|
-
alphaStaked
|
4559
|
-
});
|
4560
|
-
};
|
4561
|
-
|
4562
|
-
|
4563
|
-
|
4564
|
-
|
4565
|
-
|
4566
|
-
|
4567
|
-
|
4568
|
-
|
4569
|
-
|
4570
|
-
const
|
4571
|
-
|
4572
|
-
|
4573
|
-
|
4574
|
-
// staking can only be done by the native token on chains with the subtensor pallet
|
4575
|
-
const addressesBySubtensorToken = Object.fromEntries(Object.entries(addressesByToken)
|
4576
|
-
// remove ethereum addresses
|
4577
|
-
.map(([tokenId, addresses]) => [tokenId, addresses.filter(address => !isEthereumAddress(address))])
|
4578
|
-
// remove tokens which aren't subtensor staking tokens
|
4579
|
-
.filter(([tokenId]) => subtensorTokenIds.includes(tokenId)));
|
4580
|
-
const uniqueChainIds = getUniqueChainIds(addressesBySubtensorToken, tokens);
|
4581
|
-
const chains = Object.fromEntries(Object.entries(allChains).filter(([chainId]) => uniqueChainIds.includes(chainId)));
|
4582
|
-
const abortController = new AbortController();
|
4583
|
-
for (const [tokenId, addresses] of Object.entries(addressesBySubtensorToken)) {
|
4584
|
-
const token = tokens[tokenId];
|
4585
|
-
if (!token) {
|
4586
|
-
log.warn(`Token ${tokenId} not found`);
|
4587
|
-
continue;
|
4588
|
-
}
|
4589
|
-
if (token.type !== "substrate-native") {
|
4590
|
-
log.debug(`This module doesn't handle tokens of type ${token.type}`);
|
4591
|
-
continue;
|
4592
|
-
}
|
4593
|
-
const chainId = token.chain?.id;
|
4594
|
-
if (!chainId) {
|
4595
|
-
log.warn(`Token ${tokenId} has no chain`);
|
4596
|
-
continue;
|
4597
|
-
}
|
4598
|
-
const chain = chains[chainId];
|
4599
|
-
if (!chain) {
|
4600
|
-
log.warn(`Chain ${chainId} for token ${tokenId} not found`);
|
4601
|
-
continue;
|
4602
|
-
}
|
4603
|
-
const [chainMeta] = findChainMeta(miniMetadatas, "substrate-native", chain);
|
4604
|
-
if (!chainMeta?.miniMetadata) {
|
4605
|
-
log.warn(`MiniMetadata for chain ${chainId} not found`);
|
4606
|
-
continue;
|
3939
|
+
alphaPrice,
|
3940
|
+
alphaStaked
|
3941
|
+
});
|
3942
|
+
};
|
3943
|
+
|
3944
|
+
// TODO make this method chain-specific
|
3945
|
+
async function subscribeSubtensorStaking(chaindataProvider, chainConnector, addressesByToken, callback, signal) {
|
3946
|
+
try {
|
3947
|
+
const allChains = await chaindataProvider.getNetworksMapById("polkadot");
|
3948
|
+
const tokens = await chaindataProvider.getTokensMapById();
|
3949
|
+
|
3950
|
+
// there should be only one network here when subscribing to balances, we've split it up by network at the top level
|
3951
|
+
const networkIds = keys(addressesByToken).map(tokenId => parseTokenId(tokenId).networkId);
|
3952
|
+
const miniMetadatas = new Map();
|
3953
|
+
for (const networkId of networkIds) {
|
3954
|
+
const miniMetadata = await getMiniMetadata(chaindataProvider, chainConnector, networkId, "substrate-native", signal);
|
3955
|
+
miniMetadatas.set(networkId, miniMetadata);
|
4607
3956
|
}
|
4608
|
-
|
4609
|
-
|
4610
|
-
|
4611
|
-
|
4612
|
-
|
4613
|
-
)
|
4614
|
-
|
4615
|
-
|
4616
|
-
|
4617
|
-
|
4618
|
-
|
4619
|
-
|
4620
|
-
|
4621
|
-
|
4622
|
-
|
4623
|
-
|
4624
|
-
|
4625
|
-
|
4626
|
-
|
4627
|
-
|
4628
|
-
|
4629
|
-
|
4630
|
-
|
4631
|
-
|
4632
|
-
|
4633
|
-
|
4634
|
-
|
4635
|
-
|
4636
|
-
|
4637
|
-
|
3957
|
+
signal?.throwIfAborted();
|
3958
|
+
const subtensorTokenIds = Object.entries(tokens).filter(([, token]) => {
|
3959
|
+
// ignore non-native tokens
|
3960
|
+
if (token.type !== "substrate-native") return false;
|
3961
|
+
// ignore tokens on chains with no subtensor pallet
|
3962
|
+
const miniMetadata = miniMetadatas.get(token.networkId);
|
3963
|
+
return miniMetadata?.extra?.hasSubtensorPallet === true;
|
3964
|
+
}).map(([tokenId]) => tokenId);
|
3965
|
+
|
3966
|
+
// staking can only be done by the native token on chains with the subtensor pallet
|
3967
|
+
const addressesBySubtensorToken = Object.fromEntries(Object.entries(addressesByToken)
|
3968
|
+
// remove ethereum addresses
|
3969
|
+
.map(([tokenId, addresses]) => [tokenId, addresses.filter(address => !isEthereumAddress(address))])
|
3970
|
+
// remove tokens which aren't subtensor staking tokens
|
3971
|
+
.filter(([tokenId]) => subtensorTokenIds.includes(tokenId)));
|
3972
|
+
const uniqueChainIds = getUniqueChainIds(addressesBySubtensorToken, tokens);
|
3973
|
+
const chains = Object.fromEntries(Object.entries(allChains).filter(([chainId]) => uniqueChainIds.includes(chainId)));
|
3974
|
+
const abortController = new AbortController();
|
3975
|
+
for (const [tokenId, addresses] of Object.entries(addressesBySubtensorToken)) {
|
3976
|
+
const token = tokens[tokenId];
|
3977
|
+
if (!token) {
|
3978
|
+
log.warn(`Token ${tokenId} not found`);
|
3979
|
+
continue;
|
3980
|
+
}
|
3981
|
+
if (token.type !== "substrate-native") {
|
3982
|
+
log.debug(`This module doesn't handle tokens of type ${token.type}`);
|
3983
|
+
continue;
|
3984
|
+
}
|
3985
|
+
const chainId = token.networkId;
|
3986
|
+
const chain = chains[chainId];
|
3987
|
+
if (!chain) {
|
3988
|
+
log.warn(`Chain ${chainId} for token ${tokenId} not found`);
|
3989
|
+
continue;
|
3990
|
+
}
|
3991
|
+
const miniMetadata = miniMetadatas.get(token.networkId);
|
3992
|
+
if (!miniMetadata?.data) {
|
3993
|
+
log.warn(`MiniMetadata for chain ${chainId} not found`);
|
3994
|
+
continue;
|
3995
|
+
}
|
3996
|
+
const scaleApi = getScaleApi({
|
3997
|
+
chainId,
|
3998
|
+
send: (...args) => chainConnector.send(chainId, ...args, {
|
3999
|
+
expectErrors: true
|
4000
|
+
} // don't pollute the wallet logs when this request fails
|
4001
|
+
)
|
4002
|
+
}, miniMetadata.data, token, chain.hasCheckMetadataHash, chain.signedExtensions, chain.registryTypes);
|
4003
|
+
|
4004
|
+
// sets the number of addresses to query in parallel (per chain, since each chain runs in parallel to the others)
|
4005
|
+
const concurrency = 4;
|
4006
|
+
// In-memory cache for successful dynamic info results
|
4007
|
+
const dynamicInfoCache = new Map();
|
4008
|
+
const fetchDynamicInfoForNetuids = async uniqueNetuids => {
|
4009
|
+
const MAX_RETRIES = 3;
|
4010
|
+
const RETRY_DELAY_MS = 500;
|
4011
|
+
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
4012
|
+
const fetchInfo = async netuid => {
|
4013
|
+
if (netuid === 0) return null;
|
4014
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
4015
|
+
try {
|
4016
|
+
const params = [netuid];
|
4017
|
+
const result = await scaleApi.getRuntimeCallValue("SubnetInfoRuntimeApi", "get_dynamic_info", params);
|
4018
|
+
dynamicInfoCache.set(netuid, result); // Cache successful response
|
4019
|
+
return result;
|
4020
|
+
} catch (error) {
|
4021
|
+
log.trace(`Attempt ${attempt} failed for netuid ${netuid}:`, error);
|
4022
|
+
if (attempt < MAX_RETRIES) {
|
4023
|
+
const backoffTime = RETRY_DELAY_MS * 2 ** (attempt - 1);
|
4024
|
+
log.trace(`Retrying in ${backoffTime}ms...`);
|
4025
|
+
await delay(backoffTime);
|
4026
|
+
}
|
4638
4027
|
}
|
4639
4028
|
}
|
4640
|
-
|
4641
|
-
|
4642
|
-
|
4643
|
-
|
4644
|
-
|
4645
|
-
|
4029
|
+
if (dynamicInfoCache.has(netuid)) {
|
4030
|
+
return dynamicInfoCache.get(netuid); // Use cached value on failure
|
4031
|
+
}
|
4032
|
+
log.trace(`Failed to fetch dynamic info for netuid ${netuid} after ${MAX_RETRIES} attempts.`);
|
4033
|
+
return null;
|
4034
|
+
};
|
4035
|
+
return Promise.all(uniqueNetuids.map(fetchInfo));
|
4646
4036
|
};
|
4647
|
-
|
4648
|
-
|
4649
|
-
|
4650
|
-
|
4651
|
-
|
4652
|
-
|
4653
|
-
|
4654
|
-
|
4655
|
-
|
4656
|
-
|
4657
|
-
|
4658
|
-
|
4659
|
-
const stakes = result?.map(({
|
4660
|
-
coldkey,
|
4661
|
-
hotkey,
|
4662
|
-
netuid,
|
4663
|
-
stake
|
4664
|
-
}) => {
|
4665
|
-
return {
|
4666
|
-
address: coldkey,
|
4037
|
+
const subtensorQueries = from(addresses).pipe(
|
4038
|
+
// mergeMap lets us run N concurrent queries, where N is the value of `concurrency`
|
4039
|
+
mergeMap(async address => {
|
4040
|
+
const queryMethods = [async () => {
|
4041
|
+
if (chain.isTestnet) return [];
|
4042
|
+
const params = [address];
|
4043
|
+
const result = await scaleApi.getRuntimeCallValue("StakeInfoRuntimeApi", "get_stake_info_for_coldkey", params);
|
4044
|
+
if (!Array.isArray(result)) return [];
|
4045
|
+
const uniqueNetuids = Array.from(new Set(result.map(item => Number(item.netuid)).filter(netuid => netuid !== SUBTENSOR_ROOT_NETUID)));
|
4046
|
+
await fetchDynamicInfoForNetuids(uniqueNetuids);
|
4047
|
+
const stakes = result?.map(({
|
4048
|
+
coldkey,
|
4667
4049
|
hotkey,
|
4668
|
-
netuid
|
4669
|
-
stake
|
4670
|
-
|
4671
|
-
|
4672
|
-
|
4673
|
-
|
4674
|
-
|
4675
|
-
|
4676
|
-
|
4677
|
-
|
4678
|
-
|
4679
|
-
|
4680
|
-
|
4681
|
-
return
|
4682
|
-
}
|
4683
|
-
|
4684
|
-
|
4050
|
+
netuid,
|
4051
|
+
stake
|
4052
|
+
}) => {
|
4053
|
+
return {
|
4054
|
+
address: coldkey,
|
4055
|
+
hotkey,
|
4056
|
+
netuid: Number(netuid),
|
4057
|
+
stake: BigInt(stake),
|
4058
|
+
dynamicInfo: dynamicInfoCache.get(Number(netuid))
|
4059
|
+
};
|
4060
|
+
}).filter(({
|
4061
|
+
stake
|
4062
|
+
}) => stake >= SUBTENSOR_MIN_STAKE_AMOUNT_PLANK);
|
4063
|
+
return stakes;
|
4064
|
+
}];
|
4065
|
+
const errors = [];
|
4066
|
+
for (const queryMethod of queryMethods) {
|
4067
|
+
try {
|
4068
|
+
// try each query method
|
4069
|
+
return await queryMethod();
|
4070
|
+
} catch (cause) {
|
4071
|
+
// if it fails, keep track of the error and try the next one
|
4072
|
+
errors.push(cause);
|
4073
|
+
}
|
4685
4074
|
}
|
4686
|
-
}
|
4687
4075
|
|
4688
|
-
|
4689
|
-
|
4690
|
-
|
4691
|
-
|
4692
|
-
|
4693
|
-
|
4694
|
-
|
4695
|
-
|
4696
|
-
|
4697
|
-
|
4698
|
-
address,
|
4699
|
-
hotkey,
|
4700
|
-
stake,
|
4701
|
-
netuid,
|
4702
|
-
dynamicInfo
|
4703
|
-
}) => {
|
4704
|
-
const {
|
4705
|
-
token_symbol,
|
4706
|
-
subnet_name,
|
4707
|
-
subnet_identity
|
4708
|
-
} = dynamicInfo ?? {};
|
4709
|
-
const tokenSymbol = new TextDecoder().decode(Uint8Array.from(token_symbol ?? []));
|
4710
|
-
const subnetName = new TextDecoder().decode(Uint8Array.from(subnet_name ?? []));
|
4711
|
-
|
4712
|
-
/** Map from Record<string, Binary> to Record<string, string> */
|
4713
|
-
const binaryToText = input => Object.entries(input).reduce((acc, [key, value]) => {
|
4714
|
-
acc[key] = value.asText();
|
4715
|
-
return acc;
|
4716
|
-
}, {});
|
4717
|
-
const subnetIdentity = subnet_identity ? binaryToText(subnet_identity) : undefined;
|
4718
|
-
|
4719
|
-
// Add 1n balance if failed to fetch dynamic info, so the position is not ignored by Balance lib and is displayed in the UI.
|
4720
|
-
const alphaStakedInTao = dynamicInfo ? calculateTaoFromDynamicInfo({
|
4721
|
-
dynamicInfo,
|
4722
|
-
alphaStaked: stake
|
4723
|
-
}) : 1n;
|
4724
|
-
const alphaToTaoRate = calculateTaoFromDynamicInfo({
|
4725
|
-
dynamicInfo: dynamicInfo ?? null,
|
4726
|
-
alphaStaked: ONE_ALPHA_TOKEN
|
4727
|
-
}).toString();
|
4728
|
-
const stakeByNetuid = Number(netuid) === SUBTENSOR_ROOT_NETUID ? stake : alphaStakedInTao;
|
4729
|
-
return {
|
4730
|
-
source: "substrate-native",
|
4731
|
-
status: "live",
|
4076
|
+
// if we get to here, that means that all query methods failed
|
4077
|
+
// let's throw the errors back to the native balance module
|
4078
|
+
throw new Error([`Failed to fetch ${tokenId} subtensor staked balance for ${address}:`, ...errors.map(error => String(error))].join("\n\t"));
|
4079
|
+
}, concurrency),
|
4080
|
+
// instead of emitting each balance as it's fetched, toArray waits for them all to fetch and then it collects them into an array
|
4081
|
+
toArray(),
|
4082
|
+
// this mergeMap flattens our Array<Array<Stakes>> into just an Array<Stakes>
|
4083
|
+
mergeMap(stakes => stakes),
|
4084
|
+
// convert our Array<Stakes> into Array<Balances>, which we can then return to the native balance module
|
4085
|
+
map(stakes => stakes.map(({
|
4732
4086
|
address,
|
4733
|
-
|
4734
|
-
|
4735
|
-
|
4736
|
-
|
4737
|
-
|
4738
|
-
|
4739
|
-
|
4740
|
-
|
4741
|
-
|
4742
|
-
|
4743
|
-
|
4744
|
-
|
4745
|
-
|
4746
|
-
|
4747
|
-
|
4748
|
-
|
4749
|
-
|
4750
|
-
|
4751
|
-
|
4752
|
-
|
4753
|
-
|
4754
|
-
|
4087
|
+
hotkey,
|
4088
|
+
stake,
|
4089
|
+
netuid,
|
4090
|
+
dynamicInfo
|
4091
|
+
}) => {
|
4092
|
+
const {
|
4093
|
+
token_symbol,
|
4094
|
+
subnet_name,
|
4095
|
+
subnet_identity
|
4096
|
+
} = dynamicInfo ?? {};
|
4097
|
+
const tokenSymbol = new TextDecoder().decode(Uint8Array.from(token_symbol ?? []));
|
4098
|
+
const subnetName = new TextDecoder().decode(Uint8Array.from(subnet_name ?? []));
|
4099
|
+
|
4100
|
+
/** Map from Record<string, Binary> to Record<string, string> */
|
4101
|
+
const binaryToText = input => Object.entries(input).reduce((acc, [key, value]) => {
|
4102
|
+
acc[key] = value.asText();
|
4103
|
+
return acc;
|
4104
|
+
}, {});
|
4105
|
+
const subnetIdentity = subnet_identity ? binaryToText(subnet_identity) : undefined;
|
4106
|
+
|
4107
|
+
// Add 1n balance if failed to fetch dynamic info, so the position is not ignored by Balance lib and is displayed in the UI.
|
4108
|
+
const alphaStakedInTao = dynamicInfo ? calculateTaoFromDynamicInfo({
|
4109
|
+
dynamicInfo,
|
4110
|
+
alphaStaked: stake
|
4111
|
+
}) : 1n;
|
4112
|
+
const alphaToTaoRate = calculateTaoFromDynamicInfo({
|
4113
|
+
dynamicInfo: dynamicInfo ?? null,
|
4114
|
+
alphaStaked: ONE_ALPHA_TOKEN
|
4115
|
+
}).toString();
|
4116
|
+
const stakeByNetuid = Number(netuid) === SUBTENSOR_ROOT_NETUID ? stake : alphaStakedInTao;
|
4117
|
+
return {
|
4118
|
+
source: "substrate-native",
|
4119
|
+
status: "live",
|
4120
|
+
address,
|
4121
|
+
networkId: chainId,
|
4122
|
+
tokenId,
|
4123
|
+
values: [{
|
4124
|
+
source: "subtensor-staking",
|
4125
|
+
type: "subtensor",
|
4126
|
+
label: "subtensor-staking",
|
4127
|
+
amount: stakeByNetuid.toString(),
|
4128
|
+
meta: {
|
4129
|
+
type: "subtensor-staking",
|
4130
|
+
hotkey,
|
4131
|
+
netuid,
|
4132
|
+
amountStaked: stake.toString(),
|
4133
|
+
alphaToTaoRate,
|
4134
|
+
dynamicInfo: {
|
4135
|
+
tokenSymbol,
|
4136
|
+
subnetName,
|
4137
|
+
subnetIdentity: {
|
4138
|
+
...subnetIdentity,
|
4139
|
+
subnetName: subnetIdentity?.subnet_name || subnetName
|
4140
|
+
}
|
4755
4141
|
}
|
4756
4142
|
}
|
4757
|
-
}
|
4758
|
-
}
|
4759
|
-
};
|
4760
|
-
|
4143
|
+
}]
|
4144
|
+
};
|
4145
|
+
})));
|
4146
|
+
|
4147
|
+
// This observable will run the subtensorQueries on a 30s (30_000ms) interval.
|
4148
|
+
// However, if the last run has not yet completed (e.g. its been 30s but we're still fetching some balances),
|
4149
|
+
// then exhaustMap will wait until the next interval (so T: 60s, T: 90s, T: 120s, etc) before re-executing the subtensorQueries.
|
4150
|
+
const subtensorQueriesInterval = interval(30_000).pipe(startWith(0),
|
4151
|
+
// start immediately
|
4152
|
+
exhaustMap(() => {
|
4153
|
+
return subtensorQueries;
|
4154
|
+
}));
|
4761
4155
|
|
4762
|
-
|
4763
|
-
|
4764
|
-
|
4765
|
-
|
4766
|
-
|
4767
|
-
exhaustMap(() => {
|
4768
|
-
return subtensorQueries;
|
4769
|
-
}));
|
4156
|
+
// subscribe to the balances
|
4157
|
+
const subscription = subtensorQueriesInterval.subscribe({
|
4158
|
+
next: balances => callback(null, balances),
|
4159
|
+
error: error => callback(error)
|
4160
|
+
});
|
4770
4161
|
|
4771
|
-
|
4772
|
-
|
4773
|
-
|
4774
|
-
|
4162
|
+
// use the abortController to tear the subscription down when we don't need it anymore
|
4163
|
+
abortController.signal.addEventListener("abort", () => {
|
4164
|
+
subscription.unsubscribe();
|
4165
|
+
});
|
4166
|
+
}
|
4167
|
+
return () => abortController.abort();
|
4168
|
+
} catch (err) {
|
4169
|
+
if (!isAbortError(err)) log.error("Error subscribing to subtensor staking", {
|
4170
|
+
err
|
4775
4171
|
});
|
4776
|
-
|
4777
|
-
// use the abortController to tear the subscription down when we don't need it anymore
|
4778
|
-
abortController.signal.onabort = () => subscription.unsubscribe();
|
4172
|
+
return () => {};
|
4779
4173
|
}
|
4780
|
-
return () => abortController.abort();
|
4781
4174
|
}
|
4782
4175
|
|
4783
4176
|
const getOtherType = input => `other-${input}`;
|
@@ -4833,18 +4226,14 @@ const filterBaseLocks = locks => {
|
|
4833
4226
|
};
|
4834
4227
|
|
4835
4228
|
// TODO: Make these titles translatable
|
4836
|
-
const getLockTitle = (lock,
|
4229
|
+
const getLockTitle = (lock,
|
4230
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
4231
|
+
{
|
4837
4232
|
balance
|
4838
4233
|
} = {}) => {
|
4839
4234
|
if (!lock.label) return lock.label;
|
4840
4235
|
if (lock.label === "democracy") return "Governance";
|
4841
|
-
if (lock.label === "crowdloan")
|
4842
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
4843
|
-
const paraId = lock.meta?.paraId;
|
4844
|
-
if (!paraId) return "Crowdloan";
|
4845
|
-
const name = balance?.chain?.parathreads?.find(parathread => parathread?.paraId === paraId)?.name;
|
4846
|
-
return `${name ? name : `Parachain ${paraId}`} Crowdloan`;
|
4847
|
-
}
|
4236
|
+
if (lock.label === "crowdloan") return "Crowdloan";
|
4848
4237
|
if (lock.label === "nompools-staking") return "Pooled Staking";
|
4849
4238
|
if (lock.label === "nompools-unbonding") return "Pooled Staking";
|
4850
4239
|
if (lock.label === "subtensor-staking") return "Root Staking";
|
@@ -4856,7 +4245,6 @@ const getLockTitle = (lock, {
|
|
4856
4245
|
};
|
4857
4246
|
|
4858
4247
|
const moduleType$2 = "substrate-native";
|
4859
|
-
const subNativeTokenId = chainId => `${chainId}-substrate-native`.toLowerCase().replace(/ /g, "-");
|
4860
4248
|
|
4861
4249
|
/**
|
4862
4250
|
* Function to merge two 'sub sources' of the same balance together, or
|
@@ -4934,7 +4322,7 @@ async function buildQueries$1(chains, tokens, chainStorageCoders, miniMetadatas,
|
|
4934
4322
|
log.debug(`This module doesn't handle tokens of type ${token.type}`);
|
4935
4323
|
return outerResult;
|
4936
4324
|
}
|
4937
|
-
const chainId = token.
|
4325
|
+
const chainId = token.networkId;
|
4938
4326
|
if (!chainId) {
|
4939
4327
|
log.warn(`Token ${tokenId} has no chain`);
|
4940
4328
|
return outerResult;
|
@@ -4944,10 +4332,10 @@ async function buildQueries$1(chains, tokens, chainStorageCoders, miniMetadatas,
|
|
4944
4332
|
log.warn(`Chain ${chainId} for token ${tokenId} not found`);
|
4945
4333
|
return outerResult;
|
4946
4334
|
}
|
4947
|
-
const
|
4335
|
+
const miniMetadata = miniMetadatas.get(chainId);
|
4948
4336
|
const {
|
4949
4337
|
useLegacyTransferableCalculation
|
4950
|
-
} =
|
4338
|
+
} = miniMetadata?.extra ?? {};
|
4951
4339
|
addresses.flat().forEach(address => {
|
4952
4340
|
const queryKey = `${tokenId}-${address}`;
|
4953
4341
|
// We share this balanceJson between the base and the lock query for this address
|
@@ -4955,10 +4343,7 @@ async function buildQueries$1(chains, tokens, chainStorageCoders, miniMetadatas,
|
|
4955
4343
|
source: "substrate-native",
|
4956
4344
|
status: "live",
|
4957
4345
|
address,
|
4958
|
-
|
4959
|
-
subChainId: chainId
|
4960
|
-
},
|
4961
|
-
chainId,
|
4346
|
+
networkId: chainId,
|
4962
4347
|
tokenId,
|
4963
4348
|
values: []
|
4964
4349
|
};
|
@@ -5230,75 +4615,19 @@ const updateStakingLocksUsingUnbondingLocks = values => {
|
|
5230
4615
|
return [...otherValues, ...stakingLocks];
|
5231
4616
|
};
|
5232
4617
|
|
5233
|
-
const detectMiniMetadataChanges = () => {
|
5234
|
-
let previousMap = null;
|
5235
|
-
return pipe(map(currMap => {
|
5236
|
-
if (!currMap) return null;
|
5237
|
-
const changes = new Set();
|
5238
|
-
if (previousMap) {
|
5239
|
-
// Check for added or changed keys/values
|
5240
|
-
for (const [key, value] of currMap) {
|
5241
|
-
if (!previousMap.has(key) || !isEqual(previousMap.get(key), value)) {
|
5242
|
-
changes.add(value.chainId);
|
5243
|
-
}
|
5244
|
-
}
|
5245
|
-
|
5246
|
-
// Check for removed keys
|
5247
|
-
for (const [key, value] of previousMap) {
|
5248
|
-
if (!currMap.has(key)) {
|
5249
|
-
changes.add(value.chainId);
|
5250
|
-
}
|
5251
|
-
}
|
5252
|
-
}
|
5253
|
-
previousMap = currMap;
|
5254
|
-
return changes.size > 0 ? changes : null;
|
5255
|
-
}),
|
5256
|
-
// Filter out null emissions (no changes)
|
5257
|
-
filter(changes => changes !== null));
|
5258
|
-
};
|
5259
|
-
|
5260
|
-
// NOTE: `liveQuery` is not initialized until commonMetadataObservable is subscribed to.
|
5261
|
-
const commonMetadataObservable = from(liveQuery(() => db.miniMetadatas.where("source").equals("substrate-native").toArray())).pipe(map(items => new Map(items.map(item => [item.id, item]))),
|
5262
|
-
// `refCount: true` will unsubscribe from the DB when commonMetadataObservable has no more subscribers
|
5263
|
-
shareReplay({
|
5264
|
-
bufferSize: 1,
|
5265
|
-
refCount: true
|
5266
|
-
}));
|
5267
4618
|
class QueryCache {
|
4619
|
+
#chaindataProvider;
|
4620
|
+
#chainConnector;
|
4621
|
+
miniMetadatas = new Map();
|
5268
4622
|
balanceQueryCache = new Map();
|
5269
|
-
|
5270
|
-
constructor(chaindataProvider) {
|
4623
|
+
constructor(chaindataProvider, chainConnector) {
|
5271
4624
|
this.chaindataProvider = chaindataProvider;
|
5272
|
-
|
5273
|
-
|
5274
|
-
if (this.metadataSub) return;
|
5275
|
-
this.metadataSub = commonMetadataObservable.pipe(firstThenDebounce(500), detectMiniMetadataChanges(), combineLatestWith(this.chaindataProvider.tokensObservable), distinctUntilChanged()).subscribe(([miniMetadataChanges, tokens]) => {
|
5276
|
-
// invalidate cache entries for any chains with new metadata
|
5277
|
-
const tokensByChainId = tokens.filter(token => token.type === "substrate-native").reduce((result, token) => {
|
5278
|
-
if (!token.chain?.id) return result;
|
5279
|
-
result[token.chain.id] ? result[token.chain.id].push(token) : result[token.chain.id] = [token];
|
5280
|
-
return result;
|
5281
|
-
}, {});
|
5282
|
-
miniMetadataChanges.forEach(chainId => {
|
5283
|
-
const chainTokens = tokensByChainId[chainId];
|
5284
|
-
if (!chainTokens) return;
|
5285
|
-
chainTokens.forEach(token => {
|
5286
|
-
const tokenId = token.id;
|
5287
|
-
const cacheKeys = this.balanceQueryCache.keys();
|
5288
|
-
for (const key of cacheKeys) {
|
5289
|
-
if (key.startsWith(`${tokenId}-`)) this.balanceQueryCache.delete(key);
|
5290
|
-
}
|
5291
|
-
});
|
5292
|
-
});
|
5293
|
-
});
|
5294
|
-
}
|
5295
|
-
destroy() {
|
5296
|
-
this.metadataSub?.unsubscribe();
|
4625
|
+
this.#chaindataProvider = chaindataProvider;
|
4626
|
+
this.#chainConnector = chainConnector;
|
5297
4627
|
}
|
5298
4628
|
async getQueries(addressesByToken) {
|
5299
|
-
this.
|
5300
|
-
const
|
5301
|
-
const tokens = await this.chaindataProvider.tokensById();
|
4629
|
+
const chains = await this.chaindataProvider.getNetworksMapById("polkadot");
|
4630
|
+
const tokens = await this.chaindataProvider.getTokensMapById();
|
5302
4631
|
const queryResults = Object.entries(addressesByToken).reduce((result, [tokenId, addresses]) => {
|
5303
4632
|
addresses.forEach(address => {
|
5304
4633
|
const key = `${tokenId}-${address}`;
|
@@ -5314,15 +4643,19 @@ class QueryCache {
|
|
5314
4643
|
existing: [],
|
5315
4644
|
newAddressesByToken: {}
|
5316
4645
|
});
|
4646
|
+
const byNetwork = getAddresssesByTokenByNetwork(addressesByToken);
|
4647
|
+
for (const networkId of keys(byNetwork)) {
|
4648
|
+
if (this.miniMetadatas.has(networkId)) continue;
|
4649
|
+
const miniMetadata = await getMiniMetadata(this.#chaindataProvider, this.#chainConnector, networkId, "substrate-native");
|
4650
|
+
this.miniMetadatas.set(networkId, miniMetadata);
|
4651
|
+
}
|
5317
4652
|
|
5318
4653
|
// build queries for token/address pairs which have not been queried before
|
5319
|
-
const
|
5320
|
-
const uniqueChainIds = getUniqueChainIds(queryResults.newAddressesByToken, tokens);
|
4654
|
+
const uniqueChainIds = keys(byNetwork); // getUniqueChainIds(queryResults.newAddressesByToken, tokens)
|
5321
4655
|
const chainStorageCoders = buildStorageCoders({
|
5322
4656
|
chainIds: uniqueChainIds,
|
5323
4657
|
chains,
|
5324
|
-
miniMetadatas,
|
5325
|
-
moduleType: "substrate-native",
|
4658
|
+
miniMetadatas: this.miniMetadatas,
|
5326
4659
|
coders: {
|
5327
4660
|
base: ["System", "Account"],
|
5328
4661
|
stakingLedger: ["Staking", "Ledger"],
|
@@ -5332,7 +4665,7 @@ class QueryCache {
|
|
5332
4665
|
freezes: ["Balances", "Freezes"]
|
5333
4666
|
}
|
5334
4667
|
});
|
5335
|
-
const queries = await buildQueries$1(chains, tokens, chainStorageCoders, miniMetadatas, queryResults.newAddressesByToken);
|
4668
|
+
const queries = await buildQueries$1(chains, tokens, chainStorageCoders, this.miniMetadatas, queryResults.newAddressesByToken);
|
5336
4669
|
// now update the cache
|
5337
4670
|
Object.entries(queries).forEach(([key, query]) => {
|
5338
4671
|
this.balanceQueryCache.set(key, query);
|
@@ -5341,14 +4674,11 @@ class QueryCache {
|
|
5341
4674
|
}
|
5342
4675
|
}
|
5343
4676
|
|
5344
|
-
const
|
5345
|
-
const
|
5346
|
-
const sortChains = (a, b) => {
|
4677
|
+
const IMPORTANT_TOKENS = [subNativeTokenId("polkadot"), subNativeTokenId("kusama"), subNativeTokenId("polkadot-asset-hub"), subNativeTokenId("kusama-asset-hub"), subNativeTokenId("bittensor")];
|
4678
|
+
const sortChainsNativeTokensByPriority = (a, b) => {
|
5347
4679
|
// polkadot and kusama should be checked first
|
5348
|
-
if (
|
5349
|
-
if (
|
5350
|
-
if (PUBLIC_GOODS_TOKENS.includes(a)) return -1;
|
5351
|
-
if (PUBLIC_GOODS_TOKENS.includes(b)) return 1;
|
4680
|
+
if (IMPORTANT_TOKENS.includes(a)) return -1;
|
4681
|
+
if (IMPORTANT_TOKENS.includes(b)) return 1;
|
5352
4682
|
return 0;
|
5353
4683
|
};
|
5354
4684
|
|
@@ -5364,10 +4694,30 @@ class SubNativeBalanceError extends Error {
|
|
5364
4694
|
}
|
5365
4695
|
}
|
5366
4696
|
|
5367
|
-
const
|
5368
|
-
|
4697
|
+
const DotNetworkPropertiesSimple = z.object({
|
4698
|
+
tokenDecimals: z.number().optional().default(0),
|
4699
|
+
tokenSymbol: z.string().optional().default("Unit")
|
4700
|
+
});
|
4701
|
+
const DotNetworkPropertiesArray = z.object({
|
4702
|
+
tokenDecimals: z.array(z.number()).nonempty(),
|
4703
|
+
tokenSymbol: z.array(z.string()).nonempty()
|
4704
|
+
});
|
4705
|
+
const DotNetworkPropertiesSchema = z.union([DotNetworkPropertiesSimple, DotNetworkPropertiesArray]).transform(val => ({
|
4706
|
+
tokenDecimals: Array.isArray(val.tokenDecimals) ? val.tokenDecimals[0] : val.tokenDecimals,
|
4707
|
+
tokenSymbol: Array.isArray(val.tokenSymbol) ? val.tokenSymbol[0] : val.tokenSymbol
|
4708
|
+
}));
|
4709
|
+
const getChainProperties = async (chainConnector, networkId) => {
|
4710
|
+
const properties = await chainConnector.send(networkId, "system_properties", [], true);
|
4711
|
+
return DotNetworkPropertiesSchema.parse(properties);
|
4712
|
+
};
|
4713
|
+
|
5369
4714
|
const POLLING_WINDOW_SIZE = 20;
|
5370
4715
|
const MAX_SUBSCRIPTION_SIZE = 40;
|
4716
|
+
const EMPTY_CHAIN_META = {
|
4717
|
+
miniMetadata: null,
|
4718
|
+
extra: null
|
4719
|
+
};
|
4720
|
+
const SubNativeTokenConfigSchema = TokenConfigBaseSchema;
|
5371
4721
|
const SubNativeModule = hydrate => {
|
5372
4722
|
const {
|
5373
4723
|
chainConnectors,
|
@@ -5375,33 +4725,198 @@ const SubNativeModule = hydrate => {
|
|
5375
4725
|
} = hydrate;
|
5376
4726
|
const chainConnector = chainConnectors.substrate;
|
5377
4727
|
assert(chainConnector, "This module requires a substrate chain connector");
|
5378
|
-
const queryCache = new QueryCache(chaindataProvider);
|
4728
|
+
const queryCache = new QueryCache(chaindataProvider, chainConnector);
|
5379
4729
|
const getModuleTokens = async () => {
|
5380
|
-
return await chaindataProvider.
|
4730
|
+
return await chaindataProvider.getTokensMapById(moduleType$2);
|
5381
4731
|
};
|
5382
|
-
return {
|
5383
|
-
...DefaultBalanceModule(moduleType$2),
|
5384
|
-
async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc, systemProperties) {
|
5385
|
-
const isTestnet = (await chaindataProvider.chainById(chainId))?.isTestnet || false;
|
5386
|
-
if (moduleConfig?.disable === true || metadataRpc === undefined) return {
|
5387
|
-
isTestnet
|
5388
|
-
};
|
5389
4732
|
|
5390
|
-
|
5391
|
-
|
5392
|
-
|
4733
|
+
// subscribeBalances was split by network to prevent all subs to wait for all minimetadatas to be ready.
|
4734
|
+
// however the multichain logic in there is so deep in the function below that i had to keep it as-is, and call it by per-network chunks
|
4735
|
+
// TODO refactor this be actually network specific
|
4736
|
+
// Note: had to extract this function from the result object or this.subscribeBalances wouldn't be typed correctly
|
4737
|
+
const subscribeChainBalances = (chainId, opts, callback, signal) => {
|
4738
|
+
const {
|
4739
|
+
addressesByToken,
|
4740
|
+
initialBalances
|
4741
|
+
} = opts;
|
4742
|
+
// full record of balances for this module
|
4743
|
+
const subNativeBalances = new BehaviorSubject(Object.fromEntries(initialBalances?.map(b => [getBalanceId(b), b]) ?? []));
|
4744
|
+
// tokens which have a known positive balance
|
4745
|
+
const positiveBalanceTokens = subNativeBalances.pipe(map(balances => Array.from(new Set(Object.values(balances).map(b => b.tokenId)))), share());
|
4746
|
+
|
4747
|
+
// tokens that will be subscribed to, simply a slice of the positive balance tokens of size MAX_SUBSCRIPTION_SIZE
|
4748
|
+
const subscriptionTokens = positiveBalanceTokens.pipe(map(tokens => tokens.sort(sortChainsNativeTokensByPriority).slice(0, MAX_SUBSCRIPTION_SIZE)));
|
4749
|
+
|
4750
|
+
// an initialised balance is one where we have received a response for any type of 'subsource',
|
4751
|
+
// until then they are initialising. We only need to maintain one map of tokens to addresses for this
|
4752
|
+
const initialisingBalances = Object.entries(addressesByToken).reduce((acc, [tokenId, addresses]) => {
|
4753
|
+
acc.set(tokenId, new Set(addresses));
|
4754
|
+
return acc;
|
4755
|
+
}, new Map());
|
4756
|
+
|
4757
|
+
// after thirty seconds, we need to kill the initialising balances
|
4758
|
+
const initBalancesTimeout = setTimeout(() => {
|
4759
|
+
initialisingBalances.clear();
|
4760
|
+
// manually call the callback to ensure the caller gets the correct status
|
4761
|
+
callback(null, {
|
4762
|
+
status: "live",
|
4763
|
+
data: Object.values(subNativeBalances.getValue())
|
4764
|
+
});
|
4765
|
+
}, 30_000);
|
4766
|
+
const _callbackSub = subNativeBalances.pipe(debounceTime(100)).subscribe({
|
4767
|
+
next: balances => {
|
4768
|
+
callback(null, {
|
4769
|
+
status: initialisingBalances.size > 0 ? "initialising" : "live",
|
4770
|
+
data: Object.values(balances)
|
4771
|
+
});
|
4772
|
+
},
|
4773
|
+
error: error => callback(error),
|
4774
|
+
complete: () => {
|
4775
|
+
initialisingBalances.clear();
|
4776
|
+
clearTimeout(initBalancesTimeout);
|
4777
|
+
}
|
4778
|
+
});
|
4779
|
+
const unsubDeferred = Deferred();
|
4780
|
+
// we return this to the caller so that they can let us know when they're no longer interested in this subscription
|
4781
|
+
const callerUnsubscribe = () => {
|
4782
|
+
subNativeBalances.complete();
|
4783
|
+
_callbackSub.unsubscribe();
|
4784
|
+
return unsubDeferred.reject(new Error(`Caller unsubscribed`));
|
4785
|
+
};
|
4786
|
+
// we queue up our work to clean up our subscription when this promise rejects
|
4787
|
+
const callerUnsubscribed = unsubDeferred.promise;
|
4788
|
+
|
4789
|
+
// The update handler is to allow us to merge balances with the same id, and manage initialising and positive balances state for each
|
4790
|
+
// balance type and network
|
4791
|
+
const handleUpdateForSource = source => (error, result) => {
|
4792
|
+
if (result) {
|
4793
|
+
const currentBalances = subNativeBalances.getValue();
|
4794
|
+
|
4795
|
+
// first merge any balances with the same id within the result
|
4796
|
+
const accumulatedUpdates = result.filter(b => b.values.length > 0).reduce((acc, b) => {
|
4797
|
+
const bId = getBalanceId(b);
|
4798
|
+
acc[bId] = mergeBalances(acc[bId], b, source, false);
|
4799
|
+
return acc;
|
4800
|
+
}, {});
|
4801
|
+
|
4802
|
+
// then merge these with the current balances
|
4803
|
+
const mergedBalances = {};
|
4804
|
+
Object.entries(accumulatedUpdates).forEach(([bId, b]) => {
|
4805
|
+
// merge the values from the new balance into the existing balance, if there is one
|
4806
|
+
mergedBalances[bId] = mergeBalances(currentBalances[bId], b, source, true);
|
4807
|
+
|
4808
|
+
// update initialisingBalances to remove balances which have been updated
|
4809
|
+
const intialisingForToken = initialisingBalances.get(b.tokenId);
|
4810
|
+
if (intialisingForToken) {
|
4811
|
+
intialisingForToken.delete(b.address);
|
4812
|
+
if (intialisingForToken.size === 0) initialisingBalances.delete(b.tokenId);else initialisingBalances.set(b.tokenId, intialisingForToken);
|
4813
|
+
}
|
4814
|
+
});
|
4815
|
+
subNativeBalances.next({
|
4816
|
+
...currentBalances,
|
4817
|
+
...mergedBalances
|
4818
|
+
});
|
4819
|
+
}
|
4820
|
+
if (error) {
|
4821
|
+
if (error instanceof SubNativeBalanceError) {
|
4822
|
+
// this type of error doesn't need to be handled by the caller
|
4823
|
+
initialisingBalances.delete(error.tokenId);
|
4824
|
+
} else return callback(error);
|
4825
|
+
}
|
4826
|
+
};
|
5393
4827
|
|
5394
|
-
|
5395
|
-
|
5396
|
-
|
5397
|
-
|
5398
|
-
|
5399
|
-
|
4828
|
+
// subscribe to addresses and tokens for which we have a known positive balance
|
4829
|
+
const positiveSub = subscriptionTokens.pipe(debounceTime(1000), takeUntil(callerUnsubscribed), map(tokenIds => tokenIds.reduce((acc, tokenId) => {
|
4830
|
+
acc[tokenId] = addressesByToken[tokenId];
|
4831
|
+
return acc;
|
4832
|
+
}, {})), distinctUntilChanged(isEqual), switchMap(newAddressesByToken => {
|
4833
|
+
return from(queryCache.getQueries(newAddressesByToken)).pipe(switchMap(baseQueries => {
|
4834
|
+
return new Observable(subscriber => {
|
4835
|
+
if (!chainConnectors.substrate) return;
|
4836
|
+
const unsubSubtensorStaking = subscribeSubtensorStaking(chaindataProvider, chainConnectors.substrate, newAddressesByToken, handleUpdateForSource("subtensor-staking"), signal);
|
4837
|
+
const unsubNompoolStaking = subscribeNompoolStaking(chaindataProvider, chainConnectors.substrate, newAddressesByToken, handleUpdateForSource("nompools-staking"), signal);
|
4838
|
+
const unsubBase = subscribeBase(baseQueries, chainConnectors.substrate, handleUpdateForSource("base"));
|
4839
|
+
subscriber.add(async () => (await unsubSubtensorStaking)());
|
4840
|
+
subscriber.add(async () => (await unsubNompoolStaking)());
|
4841
|
+
subscriber.add(async () => (await unsubBase)());
|
4842
|
+
});
|
4843
|
+
}));
|
4844
|
+
})).subscribe();
|
4845
|
+
|
4846
|
+
// for chains where we don't have a known positive balance, poll rather than subscribe
|
4847
|
+
const poll = async (addressesByToken = {}) => {
|
4848
|
+
const handleUpdate = handleUpdateForSource("base");
|
4849
|
+
try {
|
4850
|
+
const balances = await fetchBalances(addressesByToken);
|
4851
|
+
handleUpdate(null, Object.values(balances.toJSON()));
|
4852
|
+
} catch (error) {
|
4853
|
+
if (error instanceof ChainConnectionError) {
|
4854
|
+
// coerce ChainConnection errors into SubNativeBalance errors
|
4855
|
+
const errorChainId = error.chainId;
|
4856
|
+
Object.entries(await getModuleTokens()).filter(([, token]) => token.networkId === errorChainId).forEach(([tokenId]) => {
|
4857
|
+
const wrappedError = new SubNativeBalanceError(tokenId, error.message);
|
4858
|
+
handleUpdate(wrappedError);
|
4859
|
+
});
|
4860
|
+
} else {
|
4861
|
+
log.error("unknown substrate native balance error", error);
|
4862
|
+
handleUpdate(error);
|
4863
|
+
}
|
4864
|
+
}
|
4865
|
+
};
|
4866
|
+
// do one poll to get things started
|
4867
|
+
const currentBalances = subNativeBalances.getValue();
|
4868
|
+
const currentTokens = new Set(Object.values(currentBalances).map(b => b.tokenId));
|
4869
|
+
const nonCurrentTokens = Object.keys(addressesByToken).filter(tokenId => !currentTokens.has(tokenId)).sort(sortChainsNativeTokensByPriority);
|
4870
|
+
|
4871
|
+
// break nonCurrentTokens into chunks of POLLING_WINDOW_SIZE
|
4872
|
+
const pool = new PQueue({
|
4873
|
+
concurrency: POLLING_WINDOW_SIZE
|
4874
|
+
});
|
4875
|
+
nonCurrentTokens.forEach(nonCurrentTokenId => pool.add(() => poll({
|
4876
|
+
[nonCurrentTokenId]: addressesByToken[nonCurrentTokenId]
|
4877
|
+
}), {
|
4878
|
+
signal
|
4879
|
+
}));
|
4880
|
+
|
4881
|
+
// now poll every 30s on chains which are not subscriptionTokens
|
4882
|
+
// we chunk this observable into batches of positive token ids, to prevent eating all the websocket connections
|
4883
|
+
const pollingSub = interval(30_000) // emit values every 30 seconds
|
4884
|
+
.pipe(takeUntil(callerUnsubscribed), withLatestFrom(subscriptionTokens),
|
4885
|
+
// Combine latest value from subscriptionTokens with each interval tick
|
4886
|
+
map(([, subscribedTokenIds]) =>
|
4887
|
+
// Filter out tokens that are not subscribed
|
4888
|
+
Object.keys(addressesByToken).filter(tokenId => !subscribedTokenIds.includes(tokenId))), exhaustMap(tokenIds => from(arrayChunk(tokenIds, POLLING_WINDOW_SIZE)).pipe(concatMap(async tokenChunk => {
|
4889
|
+
// tokenChunk is a chunk of tokenIds with size POLLING_WINDOW_SIZE
|
4890
|
+
const pollingTokenAddresses = Object.fromEntries(tokenChunk.map(tokenId => [tokenId, addressesByToken[tokenId]]));
|
4891
|
+
await pool.add(() => poll(pollingTokenAddresses), {
|
4892
|
+
signal
|
4893
|
+
});
|
4894
|
+
return true;
|
4895
|
+
})))).subscribe();
|
4896
|
+
return () => {
|
4897
|
+
callerUnsubscribe();
|
4898
|
+
positiveSub.unsubscribe();
|
4899
|
+
pollingSub.unsubscribe();
|
4900
|
+
};
|
4901
|
+
};
|
4902
|
+
const fetchBalances = async addressesByToken => {
|
4903
|
+
assert(chainConnectors.substrate, "This module requires a substrate chain connector");
|
4904
|
+
const queries = await queryCache.getQueries(addressesByToken);
|
4905
|
+
assert(chainConnectors.substrate, "This module requires a substrate chain connector");
|
4906
|
+
const result = await new RpcStateQueryHelper(chainConnectors.substrate, queries).fetch();
|
4907
|
+
return new Balances(result ?? []);
|
4908
|
+
};
|
4909
|
+
return {
|
4910
|
+
...DefaultBalanceModule(moduleType$2),
|
4911
|
+
async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) {
|
4912
|
+
if (moduleConfig?.disable) return EMPTY_CHAIN_META;
|
4913
|
+
if (!metadataRpc) return EMPTY_CHAIN_META;
|
5400
4914
|
|
5401
4915
|
//
|
5402
4916
|
// process metadata into SCALE encoders/decoders
|
5403
4917
|
//
|
5404
4918
|
const metadataVersion = getMetadataVersion(metadataRpc);
|
4919
|
+
if (metadataVersion < 14) return EMPTY_CHAIN_META;
|
5405
4920
|
const metadata = decAnyMetadata(metadataRpc);
|
5406
4921
|
const unifiedMetadata = unifyMetadata(metadata);
|
5407
4922
|
|
@@ -5421,7 +4936,6 @@ const SubNativeModule = hydrate => {
|
|
5421
4936
|
};
|
5422
4937
|
const existentialDeposit = getConstantValue("Balances", "ExistentialDeposit")?.toString();
|
5423
4938
|
const nominationPoolsPalletId = getConstantValue("NominationPools", "PalletId")?.asText();
|
5424
|
-
const crowdloanPalletId = getConstantValue("Crowdloan", "PalletId")?.asText();
|
5425
4939
|
const hasSubtensorPallet = getConstantValue("SubtensorModule", "KeySwapCost") !== undefined;
|
5426
4940
|
|
5427
4941
|
//
|
@@ -5441,15 +4955,10 @@ const SubNativeModule = hydrate => {
|
|
5441
4955
|
}, {
|
5442
4956
|
pallet: "Staking",
|
5443
4957
|
items: ["Ledger"]
|
5444
|
-
}, {
|
5445
|
-
pallet: "Crowdloan",
|
5446
|
-
items: ["Funds"]
|
5447
|
-
}, {
|
5448
|
-
pallet: "Paras",
|
5449
|
-
items: ["Parachains"]
|
5450
4958
|
},
|
5451
4959
|
// TotalColdkeyStake is used until v.2.2.1, then it is replaced by StakingHotkeys+Stake
|
5452
4960
|
// Need to keep TotalColdkeyStake for a while so chaindata keeps including it in miniMetadatas, so it doesnt break old versions of the wallet
|
4961
|
+
// TODO: Since chaindata v4 this is safe to now delete
|
5453
4962
|
{
|
5454
4963
|
pallet: "SubtensorModule",
|
5455
4964
|
items: ["TotalColdkeyStake", "StakingHotkeys", "Stake"]
|
@@ -5468,47 +4977,40 @@ const SubNativeModule = hydrate => {
|
|
5468
4977
|
}) => name === "Freezes"));
|
5469
4978
|
const useLegacyTransferableCalculation = !hasFreezesItem;
|
5470
4979
|
const chainMeta = {
|
5471
|
-
isTestnet,
|
5472
|
-
useLegacyTransferableCalculation,
|
5473
|
-
symbol,
|
5474
|
-
decimals,
|
5475
|
-
existentialDeposit,
|
5476
|
-
nominationPoolsPalletId,
|
5477
|
-
crowdloanPalletId,
|
5478
|
-
hasSubtensorPallet,
|
5479
4980
|
miniMetadata,
|
5480
|
-
|
4981
|
+
extra: {
|
4982
|
+
useLegacyTransferableCalculation,
|
4983
|
+
existentialDeposit,
|
4984
|
+
nominationPoolsPalletId,
|
4985
|
+
hasSubtensorPallet
|
4986
|
+
}
|
5481
4987
|
};
|
5482
|
-
if (!useLegacyTransferableCalculation) delete chainMeta.useLegacyTransferableCalculation;
|
5483
|
-
if (!hasSubtensorPallet) delete chainMeta.hasSubtensorPallet;
|
4988
|
+
if (!useLegacyTransferableCalculation) delete chainMeta.extra?.useLegacyTransferableCalculation;
|
4989
|
+
if (!hasSubtensorPallet) delete chainMeta.extra?.hasSubtensorPallet;
|
5484
4990
|
return chainMeta;
|
5485
4991
|
},
|
5486
4992
|
async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) {
|
5487
4993
|
if (moduleConfig?.disable === true) return {};
|
5488
4994
|
const {
|
5489
|
-
|
5490
|
-
|
5491
|
-
|
4995
|
+
tokenSymbol: symbol,
|
4996
|
+
tokenDecimals: decimals
|
4997
|
+
} = await getChainProperties(chainConnector, chainId);
|
4998
|
+
const {
|
5492
4999
|
existentialDeposit
|
5493
|
-
} = chainMeta;
|
5000
|
+
} = chainMeta.extra ?? {};
|
5001
|
+
if (existentialDeposit === undefined) log.warn("Substrate native module: existentialDeposit is undefined for %s, using 0", chainId);
|
5494
5002
|
const id = subNativeTokenId(chainId);
|
5495
5003
|
const nativeToken = {
|
5496
5004
|
id,
|
5497
5005
|
type: "substrate-native",
|
5498
|
-
|
5499
|
-
isDefault:
|
5500
|
-
symbol: symbol
|
5501
|
-
|
5502
|
-
|
5006
|
+
platform: "polkadot",
|
5007
|
+
isDefault: true,
|
5008
|
+
symbol: symbol,
|
5009
|
+
name: symbol,
|
5010
|
+
decimals: decimals,
|
5503
5011
|
existentialDeposit: existentialDeposit ?? "0",
|
5504
|
-
|
5505
|
-
id: chainId
|
5506
|
-
}
|
5012
|
+
networkId: chainId
|
5507
5013
|
};
|
5508
|
-
if (moduleConfig?.symbol) nativeToken.symbol = moduleConfig?.symbol;
|
5509
|
-
if (moduleConfig?.coingeckoId) nativeToken.coingeckoId = moduleConfig?.coingeckoId;
|
5510
|
-
if (moduleConfig?.dcentName) nativeToken.dcentName = moduleConfig?.dcentName;
|
5511
|
-
if (moduleConfig?.mirrorOf) nativeToken.mirrorOf = moduleConfig?.mirrorOf;
|
5512
5014
|
return {
|
5513
5015
|
[nativeToken.id]: nativeToken
|
5514
5016
|
};
|
@@ -5518,169 +5020,36 @@ const SubNativeModule = hydrate => {
|
|
5518
5020
|
initialBalances
|
5519
5021
|
}, callback) {
|
5520
5022
|
assert(chainConnectors.substrate, "This module requires a substrate chain connector");
|
5521
|
-
|
5522
|
-
|
5523
|
-
const
|
5524
|
-
|
5525
|
-
|
5526
|
-
|
5527
|
-
|
5528
|
-
|
5529
|
-
|
5530
|
-
// an initialised balance is one where we have received a response for any type of 'subsource',
|
5531
|
-
// until then they are initialising. We only need to maintain one map of tokens to addresses for this
|
5532
|
-
const initialisingBalances = Object.entries(addressesByToken).reduce((acc, [tokenId, addresses]) => {
|
5533
|
-
acc.set(tokenId, new Set(addresses));
|
5534
|
-
return acc;
|
5535
|
-
}, new Map());
|
5536
|
-
|
5537
|
-
// after thirty seconds, we need to kill the initialising balances
|
5538
|
-
const initBalancesTimeout = setTimeout(() => {
|
5539
|
-
initialisingBalances.clear();
|
5540
|
-
// manually call the callback to ensure the caller gets the correct status
|
5541
|
-
callback(null, {
|
5542
|
-
status: "live",
|
5543
|
-
data: Object.values(subNativeBalances.getValue())
|
5544
|
-
});
|
5545
|
-
}, 30_000);
|
5546
|
-
const _callbackSub = subNativeBalances.pipe(debounceTime(100)).subscribe({
|
5547
|
-
next: balances => {
|
5548
|
-
callback(null, {
|
5549
|
-
status: initialisingBalances.size > 0 ? "initialising" : "live",
|
5550
|
-
data: Object.values(balances)
|
5551
|
-
});
|
5552
|
-
},
|
5553
|
-
error: error => callback(error),
|
5554
|
-
complete: () => {
|
5555
|
-
initialisingBalances.clear();
|
5556
|
-
clearTimeout(initBalancesTimeout);
|
5557
|
-
}
|
5558
|
-
});
|
5559
|
-
const unsubDeferred = Deferred();
|
5560
|
-
// we return this to the caller so that they can let us know when they're no longer interested in this subscription
|
5561
|
-
const callerUnsubscribe = () => {
|
5562
|
-
subNativeBalances.complete();
|
5563
|
-
_callbackSub.unsubscribe();
|
5564
|
-
return unsubDeferred.reject(new Error(`Caller unsubscribed`));
|
5023
|
+
const addressesByTokenByNetwork = getAddresssesByTokenByNetwork(addressesByToken);
|
5024
|
+
const initialBalancesByNetwork = groupBy$1(initialBalances ?? [], "networkId");
|
5025
|
+
const controller = new AbortController();
|
5026
|
+
const safeCallback = (error, result) => {
|
5027
|
+
if (controller.signal.aborted) return;
|
5028
|
+
if (isAbortError(error)) return;
|
5029
|
+
// typescript isnt happy with fowarding parameters as is
|
5030
|
+
return error ? callback(error, undefined) : callback(error, result);
|
5565
5031
|
};
|
5566
|
-
|
5567
|
-
const callerUnsubscribed = unsubDeferred.promise;
|
5568
|
-
|
5569
|
-
// The update handler is to allow us to merge balances with the same id, and manage initialising and positive balances state for each
|
5570
|
-
// balance type and network
|
5571
|
-
const handleUpdateForSource = source => (error, result) => {
|
5572
|
-
if (result) {
|
5573
|
-
const currentBalances = subNativeBalances.getValue();
|
5574
|
-
|
5575
|
-
// first merge any balances with the same id within the result
|
5576
|
-
const accumulatedUpdates = result.filter(b => b.values.length > 0).reduce((acc, b) => {
|
5577
|
-
const bId = getBalanceId(b);
|
5578
|
-
acc[bId] = mergeBalances(acc[bId], b, source, false);
|
5579
|
-
return acc;
|
5580
|
-
}, {});
|
5581
|
-
|
5582
|
-
// then merge these with the current balances
|
5583
|
-
const mergedBalances = {};
|
5584
|
-
Object.entries(accumulatedUpdates).forEach(([bId, b]) => {
|
5585
|
-
// merge the values from the new balance into the existing balance, if there is one
|
5586
|
-
mergedBalances[bId] = mergeBalances(currentBalances[bId], b, source, true);
|
5587
|
-
|
5588
|
-
// update initialisingBalances to remove balances which have been updated
|
5589
|
-
const intialisingForToken = initialisingBalances.get(b.tokenId);
|
5590
|
-
if (intialisingForToken) {
|
5591
|
-
intialisingForToken.delete(b.address);
|
5592
|
-
if (intialisingForToken.size === 0) initialisingBalances.delete(b.tokenId);else initialisingBalances.set(b.tokenId, intialisingForToken);
|
5593
|
-
}
|
5594
|
-
});
|
5595
|
-
subNativeBalances.next({
|
5596
|
-
...currentBalances,
|
5597
|
-
...mergedBalances
|
5598
|
-
});
|
5599
|
-
}
|
5600
|
-
if (error) {
|
5601
|
-
if (error instanceof SubNativeBalanceError) {
|
5602
|
-
// this type of error doesn't need to be handled by the caller
|
5603
|
-
initialisingBalances.delete(error.tokenId);
|
5604
|
-
} else return callback(error);
|
5605
|
-
}
|
5606
|
-
};
|
5607
|
-
|
5608
|
-
// subscribe to addresses and tokens for which we have a known positive balance
|
5609
|
-
const positiveSub = subscriptionTokens.pipe(debounceTime(1000), takeUntil(callerUnsubscribed), map(tokenIds => tokenIds.reduce((acc, tokenId) => {
|
5610
|
-
acc[tokenId] = addressesByToken[tokenId];
|
5611
|
-
return acc;
|
5612
|
-
}, {})), distinctUntilChanged(isEqual), switchMap(newAddressesByToken => {
|
5613
|
-
return from(queryCache.getQueries(newAddressesByToken)).pipe(switchMap(baseQueries => {
|
5614
|
-
return new Observable(subscriber => {
|
5615
|
-
if (!chainConnectors.substrate) return;
|
5616
|
-
const unsubSubtensorStaking = subscribeSubtensorStaking(chaindataProvider, chainConnectors.substrate, newAddressesByToken, handleUpdateForSource("subtensor-staking"));
|
5617
|
-
const unsubNompoolStaking = subscribeNompoolStaking(chaindataProvider, chainConnectors.substrate, newAddressesByToken, handleUpdateForSource("nompools-staking"));
|
5618
|
-
const unsubCrowdloans = subscribeCrowdloans(chaindataProvider, chainConnectors.substrate, newAddressesByToken, handleUpdateForSource("crowdloan"));
|
5619
|
-
const unsubBase = subscribeBase(baseQueries, chainConnectors.substrate, handleUpdateForSource("base"));
|
5620
|
-
subscriber.add(async () => (await unsubSubtensorStaking)());
|
5621
|
-
subscriber.add(async () => (await unsubNompoolStaking)());
|
5622
|
-
subscriber.add(async () => (await unsubCrowdloans)());
|
5623
|
-
subscriber.add(async () => (await unsubBase)());
|
5624
|
-
});
|
5625
|
-
}));
|
5626
|
-
})).subscribe();
|
5627
|
-
|
5628
|
-
// for chains where we don't have a known positive balance, poll rather than subscribe
|
5629
|
-
const poll = async (addressesByToken = {}) => {
|
5630
|
-
const handleUpdate = handleUpdateForSource("base");
|
5032
|
+
const unsubsribeFns = Promise.all(keys(addressesByTokenByNetwork).map(async networkId => {
|
5631
5033
|
try {
|
5632
|
-
|
5633
|
-
|
5634
|
-
|
5635
|
-
|
5636
|
-
|
5637
|
-
|
5638
|
-
Object.entries(await getModuleTokens()).filter(([, token]) => token.chain?.id === errorChainId).forEach(([tokenId]) => {
|
5639
|
-
const wrappedError = new SubNativeBalanceError(tokenId, error.message);
|
5640
|
-
handleUpdate(wrappedError);
|
5641
|
-
});
|
5642
|
-
} else {
|
5643
|
-
log.error("unknown substrate native balance error", error);
|
5644
|
-
handleUpdate(error);
|
5645
|
-
}
|
5034
|
+
// this is what we want to be done separately for each network
|
5035
|
+
// this will update the DB so minimetadata will be available when it's used, everywhere else down the tree of subscribeChainBalances
|
5036
|
+
await getMiniMetadata(chaindataProvider, chainConnector, networkId, moduleType$2, controller.signal);
|
5037
|
+
} catch (err) {
|
5038
|
+
if (!isAbortError(err)) log.warn("Failed to get native token miniMetadata for network", networkId, err);
|
5039
|
+
return () => {};
|
5646
5040
|
}
|
5647
|
-
|
5648
|
-
|
5649
|
-
|
5650
|
-
|
5651
|
-
|
5652
|
-
|
5653
|
-
// break nonCurrentTokens into chunks of POLLING_WINDOW_SIZE
|
5654
|
-
await PromisePool$1.withConcurrency(POLLING_WINDOW_SIZE).for(nonCurrentTokens).process(async nonCurrentTokenId => await poll({
|
5655
|
-
[nonCurrentTokenId]: addressesByToken[nonCurrentTokenId]
|
5041
|
+
if (controller.signal.aborted) return () => {};
|
5042
|
+
return subscribeChainBalances(networkId, {
|
5043
|
+
addressesByToken: addressesByTokenByNetwork[networkId] ?? {},
|
5044
|
+
initialBalances: initialBalancesByNetwork[networkId] ?? []
|
5045
|
+
}, safeCallback, controller.signal);
|
5656
5046
|
}));
|
5657
|
-
|
5658
|
-
// now poll every 30s on chains which are not subscriptionTokens
|
5659
|
-
// we chunk this observable into batches of positive token ids, to prevent eating all the websocket connections
|
5660
|
-
const pollingSub = interval(30_000) // emit values every 30 seconds
|
5661
|
-
.pipe(takeUntil(callerUnsubscribed), withLatestFrom(subscriptionTokens),
|
5662
|
-
// Combine latest value from subscriptionTokens with each interval tick
|
5663
|
-
map(([, subscribedTokenIds]) =>
|
5664
|
-
// Filter out tokens that are not subscribed
|
5665
|
-
Object.keys(addressesByToken).filter(tokenId => !subscribedTokenIds.includes(tokenId))), exhaustMap(tokenIds => from(arrayChunk(tokenIds, POLLING_WINDOW_SIZE)).pipe(concatMap(async tokenChunk => {
|
5666
|
-
// tokenChunk is a chunk of tokenIds with size POLLING_WINDOW_SIZE
|
5667
|
-
const pollingTokenAddresses = Object.fromEntries(tokenChunk.map(tokenId => [tokenId, addressesByToken[tokenId]]));
|
5668
|
-
await poll(pollingTokenAddresses);
|
5669
|
-
return true;
|
5670
|
-
})))).subscribe();
|
5671
5047
|
return () => {
|
5672
|
-
|
5673
|
-
|
5674
|
-
pollingSub.unsubscribe();
|
5048
|
+
unsubsribeFns.then(fns => fns.forEach(unsubscribe => unsubscribe()));
|
5049
|
+
controller.abort();
|
5675
5050
|
};
|
5676
5051
|
},
|
5677
|
-
|
5678
|
-
assert(chainConnectors.substrate, "This module requires a substrate chain connector");
|
5679
|
-
const queries = await queryCache.getQueries(addressesByToken);
|
5680
|
-
assert(chainConnectors.substrate, "This module requires a substrate chain connector");
|
5681
|
-
const result = await new RpcStateQueryHelper(chainConnectors.substrate, queries).fetch();
|
5682
|
-
return new Balances(result ?? []);
|
5683
|
-
},
|
5052
|
+
fetchBalances,
|
5684
5053
|
async transferToken({
|
5685
5054
|
tokenId,
|
5686
5055
|
from,
|
@@ -5697,11 +5066,10 @@ const SubNativeModule = hydrate => {
|
|
5697
5066
|
transferMethod,
|
5698
5067
|
userExtensions
|
5699
5068
|
}) {
|
5700
|
-
const token = await chaindataProvider.
|
5069
|
+
const token = await chaindataProvider.getTokenById(tokenId, "substrate-native");
|
5701
5070
|
assert(token, `Token ${tokenId} not found in store`);
|
5702
|
-
|
5703
|
-
const
|
5704
|
-
const chain = await chaindataProvider.chainById(chainId);
|
5071
|
+
const chainId = token.networkId;
|
5072
|
+
const chain = await chaindataProvider.getNetworkById(chainId, "polkadot");
|
5705
5073
|
assert(chain?.genesisHash, `Chain ${chainId} not found in store`);
|
5706
5074
|
const {
|
5707
5075
|
genesisHash
|
@@ -6895,7 +6263,10 @@ var psp22Abi = {
|
|
6895
6263
|
};
|
6896
6264
|
|
6897
6265
|
const moduleType$1 = "substrate-psp22";
|
6898
|
-
const
|
6266
|
+
const SubPsp22TokenConfigSchema = z.strictObject({
|
6267
|
+
contractAddress: SubPsp22TokenSchema.shape.contractAddress,
|
6268
|
+
...TokenConfigBaseSchema.shape
|
6269
|
+
});
|
6899
6270
|
const SubPsp22Module = hydrate => {
|
6900
6271
|
const {
|
6901
6272
|
chainConnectors,
|
@@ -6905,16 +6276,15 @@ const SubPsp22Module = hydrate => {
|
|
6905
6276
|
assert(chainConnector, "This module requires a substrate chain connector");
|
6906
6277
|
return {
|
6907
6278
|
...DefaultBalanceModule(moduleType$1),
|
6908
|
-
async fetchSubstrateChainMeta(
|
6909
|
-
|
6279
|
+
async fetchSubstrateChainMeta(_chainId) {
|
6280
|
+
// we dont need anything
|
6910
6281
|
return {
|
6911
|
-
|
6282
|
+
miniMetadata: null,
|
6283
|
+
extra: null
|
6912
6284
|
};
|
6913
6285
|
},
|
6914
|
-
async fetchSubstrateChainTokens(chainId,
|
6915
|
-
|
6916
|
-
isTestnet
|
6917
|
-
} = chainMeta;
|
6286
|
+
async fetchSubstrateChainTokens(chainId, _chainMeta, moduleConfig, tokens) {
|
6287
|
+
if (!tokens?.length) return {};
|
6918
6288
|
const registry = new TypeRegistry();
|
6919
6289
|
const Psp22Abi = new Abi(psp22Abi);
|
6920
6290
|
|
@@ -6924,12 +6294,11 @@ const SubPsp22Module = hydrate => {
|
|
6924
6294
|
chainId,
|
6925
6295
|
registry
|
6926
6296
|
});
|
6927
|
-
const
|
6928
|
-
for (const tokenConfig of
|
6297
|
+
const tokenList = {};
|
6298
|
+
for (const tokenConfig of tokens ?? []) {
|
6929
6299
|
try {
|
6930
6300
|
let symbol = tokenConfig?.symbol ?? "Unit";
|
6931
6301
|
let decimals = tokenConfig?.decimals ?? 0;
|
6932
|
-
const existentialDeposit = tokenConfig?.ed ?? "0";
|
6933
6302
|
const contractAddress = tokenConfig?.contractAddress ?? undefined;
|
6934
6303
|
if (contractAddress === undefined) continue;
|
6935
6304
|
await (async () => {
|
@@ -6945,35 +6314,28 @@ const SubPsp22Module = hydrate => {
|
|
6945
6314
|
const decimalsData = decimalsResult.toJSON()?.result?.ok?.data;
|
6946
6315
|
decimals = typeof decimalsData === "string" && decimalsData.startsWith("0x") ? hexToNumber(decimalsData) : decimals;
|
6947
6316
|
})();
|
6948
|
-
const id = subPsp22TokenId(chainId,
|
6317
|
+
const id = subPsp22TokenId(chainId, contractAddress);
|
6949
6318
|
const token = {
|
6950
6319
|
id,
|
6951
6320
|
type: "substrate-psp22",
|
6952
|
-
|
6321
|
+
platform: "polkadot",
|
6953
6322
|
isDefault: tokenConfig.isDefault ?? true,
|
6954
6323
|
symbol,
|
6955
6324
|
decimals,
|
6956
|
-
|
6957
|
-
|
6325
|
+
name: tokenConfig?.name || symbol,
|
6326
|
+
logo: tokenConfig?.logo,
|
6958
6327
|
contractAddress,
|
6959
|
-
|
6960
|
-
id: chainId
|
6961
|
-
}
|
6328
|
+
networkId: chainId
|
6962
6329
|
};
|
6963
|
-
if (tokenConfig?.symbol) {
|
6964
|
-
token.symbol = tokenConfig?.symbol;
|
6965
|
-
token.id = subPsp22TokenId(chainId, token.symbol);
|
6966
|
-
}
|
6967
6330
|
if (tokenConfig?.coingeckoId) token.coingeckoId = tokenConfig?.coingeckoId;
|
6968
|
-
if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName;
|
6969
6331
|
if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf;
|
6970
|
-
|
6332
|
+
tokenList[token.id] = token;
|
6971
6333
|
} catch (error) {
|
6972
6334
|
log.error(`Failed to build substrate-psp22 token ${tokenConfig.contractAddress} (${tokenConfig.symbol}) on ${chainId}`, error?.message ?? error);
|
6973
6335
|
continue;
|
6974
6336
|
}
|
6975
6337
|
}
|
6976
|
-
return
|
6338
|
+
return tokenList;
|
6977
6339
|
},
|
6978
6340
|
// TODO: Don't create empty subscriptions
|
6979
6341
|
async subscribeBalances({
|
@@ -6983,7 +6345,7 @@ const SubPsp22Module = hydrate => {
|
|
6983
6345
|
const subscriptionInterval = 12_000; // 12_000ms == 12 seconds
|
6984
6346
|
const initDelay = 3_000; // 3000ms == 3 seconds
|
6985
6347
|
const cache = new Map();
|
6986
|
-
const tokens = await chaindataProvider.
|
6348
|
+
const tokens = await chaindataProvider.getTokensMapById();
|
6987
6349
|
const poll = async () => {
|
6988
6350
|
if (!subscriptionActive) return;
|
6989
6351
|
try {
|
@@ -7010,7 +6372,7 @@ const SubPsp22Module = hydrate => {
|
|
7010
6372
|
},
|
7011
6373
|
async fetchBalances(addressesByToken) {
|
7012
6374
|
assert(chainConnectors.substrate, "This module requires a substrate chain connector");
|
7013
|
-
const tokens = await chaindataProvider.
|
6375
|
+
const tokens = await chaindataProvider.getTokensMapById();
|
7014
6376
|
return fetchBalances(chainConnectors.substrate, tokens, addressesByToken);
|
7015
6377
|
},
|
7016
6378
|
async transferToken({
|
@@ -7028,11 +6390,11 @@ const SubPsp22Module = hydrate => {
|
|
7028
6390
|
tip,
|
7029
6391
|
userExtensions
|
7030
6392
|
}) {
|
7031
|
-
const token = await chaindataProvider.
|
6393
|
+
const token = await chaindataProvider.getTokenById(tokenId, "substrate-psp22");
|
7032
6394
|
assert(token, `Token ${tokenId} not found in store`);
|
7033
6395
|
if (token.type !== "substrate-psp22") throw new Error(`This module doesn't handle tokens of type ${token.type}`);
|
7034
|
-
const chainId = token.
|
7035
|
-
const chain = await chaindataProvider.
|
6396
|
+
const chainId = token.networkId;
|
6397
|
+
const chain = await chaindataProvider.getNetworkById(chainId, "polkadot");
|
7036
6398
|
assert(chain?.genesisHash, `Chain ${chainId} not found in store`);
|
7037
6399
|
const {
|
7038
6400
|
genesisHash
|
@@ -7108,7 +6470,7 @@ const fetchBalances = async (chainConnector, tokens, addressesByToken) => {
|
|
7108
6470
|
// TODO: Use `decodeOutput` from `./util/decodeOutput`
|
7109
6471
|
const contractCall = makeContractCaller({
|
7110
6472
|
chainConnector,
|
7111
|
-
chainId: token.
|
6473
|
+
chainId: token.networkId,
|
7112
6474
|
registry
|
7113
6475
|
});
|
7114
6476
|
if (token.contractAddress === undefined) {
|
@@ -7125,10 +6487,7 @@ const fetchBalances = async (chainConnector, tokens, addressesByToken) => {
|
|
7125
6487
|
source: "substrate-psp22",
|
7126
6488
|
status: "live",
|
7127
6489
|
address,
|
7128
|
-
|
7129
|
-
subChainId: token.chain.id
|
7130
|
-
},
|
7131
|
-
chainId: token.chain.id,
|
6490
|
+
networkId: token.networkId,
|
7132
6491
|
tokenId,
|
7133
6492
|
value: balance
|
7134
6493
|
};
|
@@ -7151,8 +6510,16 @@ const fetchBalances = async (chainConnector, tokens, addressesByToken) => {
|
|
7151
6510
|
};
|
7152
6511
|
|
7153
6512
|
const moduleType = "substrate-tokens";
|
6513
|
+
const SubTokensTokenConfigSchema = z.strictObject({
|
6514
|
+
onChainId: SubTokensTokenSchema.shape.onChainId,
|
6515
|
+
...TokenConfigBaseSchema.shape,
|
6516
|
+
existentialDeposit: SubTokensTokenSchema.shape.existentialDeposit.optional()
|
6517
|
+
});
|
7154
6518
|
const defaultPalletId = "Tokens";
|
7155
|
-
const
|
6519
|
+
const UNSUPPORTED_CHAIN_META = {
|
6520
|
+
miniMetadata: null,
|
6521
|
+
extra: {}
|
6522
|
+
};
|
7156
6523
|
const SubTokensModule = hydrate => {
|
7157
6524
|
const {
|
7158
6525
|
chainConnectors,
|
@@ -7163,14 +6530,7 @@ const SubTokensModule = hydrate => {
|
|
7163
6530
|
return {
|
7164
6531
|
...DefaultBalanceModule(moduleType),
|
7165
6532
|
async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) {
|
7166
|
-
|
7167
|
-
if (metadataRpc === undefined) return {
|
7168
|
-
isTestnet
|
7169
|
-
};
|
7170
|
-
if ((moduleConfig?.tokens ?? []).length < 1) return {
|
7171
|
-
isTestnet
|
7172
|
-
};
|
7173
|
-
const metadataVersion = getMetadataVersion(metadataRpc);
|
6533
|
+
if (metadataRpc === undefined) return UNSUPPORTED_CHAIN_META;
|
7174
6534
|
const metadata = decAnyMetadata(metadataRpc);
|
7175
6535
|
const palletId = moduleConfig?.palletId ?? defaultPalletId;
|
7176
6536
|
compactMetadata(metadata, [{
|
@@ -7178,74 +6538,83 @@ const SubTokensModule = hydrate => {
|
|
7178
6538
|
items: ["Accounts"]
|
7179
6539
|
}]);
|
7180
6540
|
const miniMetadata = encodeMetadata(metadata);
|
7181
|
-
return
|
7182
|
-
isTestnet,
|
7183
|
-
miniMetadata,
|
7184
|
-
metadataVersion
|
7185
|
-
} : {
|
7186
|
-
isTestnet,
|
7187
|
-
palletId,
|
6541
|
+
return {
|
7188
6542
|
miniMetadata,
|
7189
|
-
|
6543
|
+
extra: {
|
6544
|
+
palletId
|
6545
|
+
}
|
7190
6546
|
};
|
7191
6547
|
},
|
7192
|
-
async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) {
|
7193
|
-
const {
|
7194
|
-
|
7195
|
-
} = chainMeta;
|
7196
|
-
const tokens = {};
|
7197
|
-
for (const tokenConfig of moduleConfig?.tokens ?? []) {
|
6548
|
+
async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig, tokens) {
|
6549
|
+
const tokenList = {};
|
6550
|
+
for (const tokenConfig of tokens ?? []) {
|
7198
6551
|
try {
|
6552
|
+
// TODO fetch metadata from chain, like we do for assets
|
7199
6553
|
const symbol = tokenConfig?.symbol ?? "Unit";
|
7200
6554
|
const decimals = tokenConfig?.decimals ?? 0;
|
7201
|
-
const existentialDeposit = tokenConfig?.
|
6555
|
+
const existentialDeposit = tokenConfig?.existentialDeposit ?? "0";
|
7202
6556
|
const onChainId = tokenConfig?.onChainId ?? undefined;
|
7203
6557
|
if (onChainId === undefined) continue;
|
7204
6558
|
const id = subTokensTokenId(chainId, onChainId);
|
7205
6559
|
const token = {
|
7206
6560
|
id,
|
7207
6561
|
type: "substrate-tokens",
|
7208
|
-
|
6562
|
+
platform: "polkadot",
|
7209
6563
|
isDefault: tokenConfig.isDefault ?? true,
|
7210
6564
|
symbol,
|
7211
6565
|
decimals,
|
7212
|
-
|
6566
|
+
name: tokenConfig?.name ?? symbol,
|
6567
|
+
logo: tokenConfig?.logo,
|
7213
6568
|
existentialDeposit,
|
7214
6569
|
onChainId,
|
7215
|
-
|
7216
|
-
id: chainId
|
7217
|
-
}
|
6570
|
+
networkId: chainId
|
7218
6571
|
};
|
7219
|
-
if (tokenConfig?.symbol) {
|
7220
|
-
token.symbol = tokenConfig?.symbol;
|
7221
|
-
token.id = subTokensTokenId(chainId, token.onChainId);
|
7222
|
-
}
|
7223
6572
|
if (tokenConfig?.coingeckoId) token.coingeckoId = tokenConfig?.coingeckoId;
|
7224
|
-
if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName;
|
7225
6573
|
if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf;
|
7226
|
-
|
6574
|
+
tokenList[token.id] = token;
|
7227
6575
|
} catch (error) {
|
7228
6576
|
log.error(`Failed to build substrate-tokens token ${tokenConfig.onChainId} (${tokenConfig.symbol}) on ${chainId}`, error?.message ?? error);
|
7229
6577
|
continue;
|
7230
6578
|
}
|
7231
6579
|
}
|
7232
|
-
return
|
6580
|
+
return tokenList;
|
7233
6581
|
},
|
7234
6582
|
// TODO: Don't create empty subscriptions
|
7235
6583
|
async subscribeBalances({
|
7236
6584
|
addressesByToken
|
7237
6585
|
}, callback) {
|
7238
|
-
const
|
7239
|
-
|
7240
|
-
if (
|
7241
|
-
|
7242
|
-
|
7243
|
-
});
|
7244
|
-
|
6586
|
+
const byNetwork = keys(addressesByToken).reduce((acc, tokenId) => {
|
6587
|
+
const networkId = parseSubTokensTokenId(tokenId).networkId;
|
6588
|
+
if (!acc[networkId]) acc[networkId] = {};
|
6589
|
+
acc[networkId][tokenId] = addressesByToken[tokenId];
|
6590
|
+
return acc;
|
6591
|
+
}, {});
|
6592
|
+
const controller = new AbortController();
|
6593
|
+
const pUnsubs = Promise.all(toPairs(byNetwork).map(async ([networkId, addressesByToken]) => {
|
6594
|
+
try {
|
6595
|
+
const queries = await buildNetworkQueries(networkId, chainConnector, chaindataProvider, addressesByToken, controller.signal);
|
6596
|
+
if (controller.signal.aborted) return () => {};
|
6597
|
+
const stateHelper = new RpcStateQueryHelper(chainConnector, queries);
|
6598
|
+
return await stateHelper.subscribe((error, result) => {
|
6599
|
+
if (error) return callback(error);
|
6600
|
+
const balances = result?.filter(b => b !== null) ?? [];
|
6601
|
+
if (balances.length > 0) callback(null, new Balances(balances));
|
6602
|
+
});
|
6603
|
+
} catch (err) {
|
6604
|
+
if (!isAbortError(err)) log.error(`Failed to subscribe balances for network ${networkId}`, err);
|
6605
|
+
return () => {};
|
6606
|
+
}
|
6607
|
+
}));
|
6608
|
+
return () => {
|
6609
|
+
pUnsubs.then(unsubs => {
|
6610
|
+
unsubs.forEach(unsubscribe => unsubscribe());
|
6611
|
+
});
|
6612
|
+
controller.abort();
|
6613
|
+
};
|
7245
6614
|
},
|
7246
6615
|
async fetchBalances(addressesByToken) {
|
7247
6616
|
assert(chainConnectors.substrate, "This module requires a substrate chain connector");
|
7248
|
-
const queries = await buildQueries(chaindataProvider, addressesByToken);
|
6617
|
+
const queries = await buildQueries(chainConnector, chaindataProvider, addressesByToken);
|
7249
6618
|
const result = await new RpcStateQueryHelper(chainConnectors.substrate, queries).fetch();
|
7250
6619
|
const balances = result?.filter(b => b !== null) ?? [];
|
7251
6620
|
return new Balances(balances);
|
@@ -7257,15 +6626,13 @@ const SubTokensModule = hydrate => {
|
|
7257
6626
|
transferMethod,
|
7258
6627
|
metadataRpc
|
7259
6628
|
}) {
|
7260
|
-
const token = await chaindataProvider.
|
6629
|
+
const token = await chaindataProvider.getTokenById(tokenId, "substrate-tokens");
|
7261
6630
|
assert(token, `Token ${tokenId} not found in store`);
|
7262
|
-
|
7263
|
-
const
|
7264
|
-
const chain = await chaindataProvider.chainById(chainId);
|
6631
|
+
const chainId = token.networkId;
|
6632
|
+
const chain = await chaindataProvider.getNetworkById(chainId, "polkadot");
|
7265
6633
|
assert(chain?.genesisHash, `Chain ${chainId} not found in store`);
|
7266
|
-
const
|
7267
|
-
const
|
7268
|
-
const tokensPallet = chainMeta?.palletId ?? defaultPalletId;
|
6634
|
+
const miniMetadata = await getMiniMetadata(chaindataProvider, chainConnector, chainId, moduleType);
|
6635
|
+
const tokensPallet = miniMetadata?.extra?.palletId ?? defaultPalletId;
|
7269
6636
|
const onChainId = (() => {
|
7270
6637
|
try {
|
7271
6638
|
return papiParse(token.onChainId);
|
@@ -7364,23 +6731,15 @@ const SubTokensModule = hydrate => {
|
|
7364
6731
|
}
|
7365
6732
|
};
|
7366
6733
|
};
|
7367
|
-
async function
|
7368
|
-
const
|
7369
|
-
const
|
7370
|
-
const
|
7371
|
-
|
7372
|
-
|
7373
|
-
const
|
7374
|
-
const
|
7375
|
-
|
7376
|
-
chains,
|
7377
|
-
miniMetadatas,
|
7378
|
-
moduleType: "substrate-tokens",
|
7379
|
-
coders: {
|
7380
|
-
storage: ({
|
7381
|
-
chainId
|
7382
|
-
}) => [tokensPalletByChain.get(chainId) ?? defaultPalletId, "Accounts"]
|
7383
|
-
}
|
6734
|
+
async function buildNetworkQueries(networkId, chainConnector, chaindataProvider, addressesByToken, signal) {
|
6735
|
+
const miniMetadata = await getMiniMetadata(chaindataProvider, chainConnector, networkId, moduleType, signal);
|
6736
|
+
const chain = await chaindataProvider.getNetworkById(networkId, "polkadot");
|
6737
|
+
const tokens = await chaindataProvider.getTokensMapById();
|
6738
|
+
if (!chain) return [];
|
6739
|
+
signal?.throwIfAborted();
|
6740
|
+
const palletId = miniMetadata.extra.palletId ?? defaultPalletId;
|
6741
|
+
const networkStorageCoders = buildNetworkStorageCoders(networkId, miniMetadata, {
|
6742
|
+
storage: [palletId, "Accounts"]
|
7384
6743
|
});
|
7385
6744
|
return Object.entries(addressesByToken).flatMap(([tokenId, addresses]) => {
|
7386
6745
|
const token = tokens[tokenId];
|
@@ -7392,18 +6751,8 @@ async function buildQueries(chaindataProvider, addressesByToken) {
|
|
7392
6751
|
log.debug(`This module doesn't handle tokens of type ${token.type}`);
|
7393
6752
|
return [];
|
7394
6753
|
}
|
7395
|
-
const chainId = token.chain?.id;
|
7396
|
-
if (!chainId) {
|
7397
|
-
log.warn(`Token ${tokenId} has no chain`);
|
7398
|
-
return [];
|
7399
|
-
}
|
7400
|
-
const chain = chains[chainId];
|
7401
|
-
if (!chain) {
|
7402
|
-
log.warn(`Chain ${chainId} for token ${tokenId} not found`);
|
7403
|
-
return [];
|
7404
|
-
}
|
7405
6754
|
return addresses.flatMap(address => {
|
7406
|
-
const scaleCoder =
|
6755
|
+
const scaleCoder = networkStorageCoders?.storage;
|
7407
6756
|
const onChainId = (() => {
|
7408
6757
|
try {
|
7409
6758
|
return papiParse(token.onChainId);
|
@@ -7411,12 +6760,12 @@ async function buildQueries(chaindataProvider, addressesByToken) {
|
|
7411
6760
|
return token.onChainId;
|
7412
6761
|
}
|
7413
6762
|
})();
|
7414
|
-
const stateKey = encodeStateKey(scaleCoder, `Invalid address / token onChainId in ${
|
6763
|
+
const stateKey = encodeStateKey(scaleCoder, `Invalid address / token onChainId in ${networkId} storage query ${address} / ${token.onChainId}`, address, onChainId);
|
7415
6764
|
if (!stateKey) return [];
|
7416
6765
|
const decodeResult = change => {
|
7417
6766
|
/** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
|
7418
6767
|
|
7419
|
-
const decoded = decodeScale(scaleCoder, change, `Failed to decode substrate-tokens balance on chain ${
|
6768
|
+
const decoded = decodeScale(scaleCoder, change, `Failed to decode substrate-tokens balance on chain ${networkId}`) ?? {
|
7420
6769
|
free: 0n,
|
7421
6770
|
reserved: 0n,
|
7422
6771
|
frozen: 0n
|
@@ -7441,49 +6790,31 @@ async function buildQueries(chaindataProvider, addressesByToken) {
|
|
7441
6790
|
source: "substrate-tokens",
|
7442
6791
|
status: "live",
|
7443
6792
|
address,
|
7444
|
-
|
7445
|
-
subChainId: chainId
|
7446
|
-
},
|
7447
|
-
chainId,
|
6793
|
+
networkId,
|
7448
6794
|
tokenId: token.id,
|
7449
6795
|
values: balanceValues
|
7450
6796
|
};
|
7451
6797
|
};
|
7452
6798
|
return {
|
7453
|
-
chainId,
|
6799
|
+
chainId: networkId,
|
7454
6800
|
stateKey,
|
7455
6801
|
decodeResult
|
7456
6802
|
};
|
7457
6803
|
});
|
7458
6804
|
});
|
7459
6805
|
}
|
6806
|
+
async function buildQueries(chainConnector, chaindataProvider, addressesByToken, signal) {
|
6807
|
+
const byNetwork = keys(addressesByToken).reduce((acc, tokenId) => {
|
6808
|
+
const networkId = parseSubTokensTokenId(tokenId).networkId;
|
6809
|
+
if (!acc[networkId]) acc[networkId] = {};
|
6810
|
+
acc[networkId][tokenId] = addressesByToken[tokenId];
|
6811
|
+
return acc;
|
6812
|
+
}, {});
|
6813
|
+
return (await Promise.all(toPairs(byNetwork).map(([networkId, addressesByToken]) => {
|
6814
|
+
return buildNetworkQueries(networkId, chainConnector, chaindataProvider, addressesByToken, signal);
|
6815
|
+
}))).flat();
|
6816
|
+
}
|
7460
6817
|
|
7461
|
-
const defaultBalanceModules = [EvmErc20Module, EvmNativeModule, EvmUniswapV2Module, SubAssetsModule,
|
7462
|
-
|
7463
|
-
/** Pulls the latest chaindata from https://github.com/TalismanSociety/chaindata */
|
7464
|
-
const hydrateChaindataAndMiniMetadata = async (chaindataProvider, miniMetadataUpdater) => {
|
7465
|
-
// need chains to be provisioned first, or substrate balances won't fetch on first subscription
|
7466
|
-
await chaindataProvider.hydrateChains();
|
7467
|
-
await Promise.all([miniMetadataUpdater.hydrateFromChaindata(), miniMetadataUpdater.hydrateCustomChains()]);
|
7468
|
-
const chains = await chaindataProvider.chains();
|
7469
|
-
const {
|
7470
|
-
statusesByChain
|
7471
|
-
} = await miniMetadataUpdater.statuses(chains);
|
7472
|
-
const goodChains = [...statusesByChain.entries()].flatMap(([chainId, status]) => status === "good" ? chainId : []);
|
7473
|
-
await chaindataProvider.hydrateSubstrateTokens(goodChains);
|
7474
|
-
};
|
7475
|
-
|
7476
|
-
/** Builds any missing miniMetadatas (e.g. for the user's custom substrate chains) */
|
7477
|
-
const updateCustomMiniMetadata = async (chaindataProvider, miniMetadataUpdater) => {
|
7478
|
-
const chainIds = await chaindataProvider.chainIds();
|
7479
|
-
await miniMetadataUpdater.update(chainIds);
|
7480
|
-
};
|
7481
|
-
|
7482
|
-
/** Fetches any missing Evm Tokens */
|
7483
|
-
const updateEvmTokens = async (chaindataProvider, evmTokenFetcher) => {
|
7484
|
-
await chaindataProvider.hydrateEvmNetworks();
|
7485
|
-
const evmNetworkIds = await chaindataProvider.evmNetworkIds();
|
7486
|
-
await evmTokenFetcher.update(evmNetworkIds);
|
7487
|
-
};
|
6818
|
+
const defaultBalanceModules = [EvmErc20Module, EvmNativeModule, EvmUniswapV2Module, SubAssetsModule, SubForeignAssetsModule, SubNativeModule, SubPsp22Module, SubTokensModule];
|
7488
6819
|
|
7489
|
-
export { Balance, BalanceFormatter, BalanceValueGetter, Balances, Change24hCurrencyFormatter, DefaultBalanceModule, EvmErc20Module, EvmNativeModule,
|
6820
|
+
export { Balance, BalanceFormatter, BalanceValueGetter, Balances, Change24hCurrencyFormatter, DefaultBalanceModule, EvmErc20Module, EvmErc20TokenConfigSchema, EvmNativeModule, EvmNativeTokenConfigSchema, EvmUniswapV2Module, EvmUniswapV2TokenConfigSchema, FiatSumBalancesFormatter, ONE_ALPHA_TOKEN, PlanckSumBalancesFormatter, RpcStateQueryHelper, SCALE_FACTOR, SUBTENSOR_MIN_STAKE_AMOUNT_PLANK, SUBTENSOR_ROOT_NETUID, SubAssetsModule, SubAssetsTokenConfigSchema, SubForeignAssetsModule, SubForeignAssetsTokenConfigSchema, SubNativeModule, SubNativeTokenConfigSchema, SubPsp22Module, SubPsp22TokenConfigSchema, SubTokensModule, SubTokensTokenConfigSchema, SumBalancesFormatter, TalismanBalancesDatabase, abiMulticall, balances, buildNetworkStorageCoders, buildStorageCoders, calculateAlphaPrice, calculateTaoAmountFromAlpha, calculateTaoFromDynamicInfo, compress, configureStore, db, decodeOutput, decompress, defaultBalanceModules, deriveMiniMetadataId, detectTransferMethod, erc20Abi, erc20BalancesAggregatorAbi, excludeFromFeePayableLocks, excludeFromTransferableAmount, filterBaseLocks, filterMirrorTokens, getBalanceId, getLockTitle, getUniqueChainIds, getValueId, includeInTotalExtraAmount, makeContractCaller, uniswapV2PairAbi };
|