@talismn/balances 0.0.0-pr2043-20250703063959 → 0.0.0-pr2059-20250626001054
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 +12 -23
- package/dist/declarations/src/EvmTokenFetcher.d.ts +11 -0
- package/dist/declarations/src/MiniMetadataUpdater.d.ts +43 -0
- package/dist/declarations/src/index.d.ts +3 -0
- package/dist/declarations/src/modules/EvmErc20Module.d.ts +23 -20
- package/dist/declarations/src/modules/EvmNativeModule.d.ts +18 -19
- package/dist/declarations/src/modules/EvmUniswapV2Module.d.ts +30 -28
- package/dist/declarations/src/modules/SubstrateAssetsModule.d.ts +18 -20
- package/dist/declarations/src/modules/SubstrateEquilibriumModule.d.ts +39 -0
- package/dist/declarations/src/modules/SubstrateForeignAssetsModule.d.ts +18 -20
- package/dist/declarations/src/modules/SubstrateNativeModule/index.d.ts +4 -17
- package/dist/declarations/src/modules/SubstrateNativeModule/subscribeCrowdloans.d.ts +5 -0
- 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 +23 -6
- package/dist/declarations/src/modules/SubstrateNativeModule/util/QueryCache.d.ts +6 -6
- package/dist/declarations/src/modules/SubstrateNativeModule/util/buildQueries.d.ts +4 -5
- package/dist/declarations/src/modules/SubstrateNativeModule/util/crowdloanFundContributionsChildKey.d.ts +4 -0
- package/dist/declarations/src/modules/SubstrateNativeModule/util/detectMiniMetadataChanges.d.ts +2 -0
- package/dist/declarations/src/modules/SubstrateNativeModule/util/sortChains.d.ts +1 -1
- package/dist/declarations/src/modules/SubstratePsp22Module.d.ts +19 -20
- package/dist/declarations/src/modules/SubstrateTokensModule.d.ts +22 -22
- package/dist/declarations/src/modules/index.d.ts +38 -213
- package/dist/declarations/src/modules/util/InferBalanceModuleTypes.d.ts +3 -5
- package/dist/declarations/src/modules/util/buildStorageCoders.d.ts +7 -16
- package/dist/declarations/src/modules/util/findChainMeta.d.ts +8 -0
- package/dist/declarations/src/modules/util/getUniqueChainIds.d.ts +2 -2
- package/dist/declarations/src/modules/util/index.d.ts +1 -0
- package/dist/declarations/src/types/balances.d.ts +72 -203
- package/dist/declarations/src/types/balancetypes.d.ts +18 -8
- package/dist/declarations/src/types/minimetadatas.d.ts +24 -6
- package/dist/declarations/src/util/hydrateChaindata.d.ts +8 -0
- package/dist/declarations/src/util/index.d.ts +1 -0
- package/dist/talismn-balances.cjs.dev.js +2099 -1424
- package/dist/talismn-balances.cjs.prod.js +2099 -1424
- package/dist/talismn-balances.esm.js +2083 -1414
- package/package.json +8 -9
- package/dist/declarations/src/getMiniMetadata/getMetadataRpc.d.ts +0 -3
- package/dist/declarations/src/getMiniMetadata/getMiniMetadatas.d.ts +0 -4
- package/dist/declarations/src/getMiniMetadata/getSpecVersion.d.ts +0 -6
- package/dist/declarations/src/getMiniMetadata/getUpdatedMiniMetadatas.d.ts +0 -4
- package/dist/declarations/src/getMiniMetadata/index.d.ts +0 -5
- package/dist/declarations/src/libVersion.d.ts +0 -1
- package/dist/declarations/src/modules/SubstrateNativeModule/util/systemProperties.d.ts +0 -5
- package/dist/declarations/src/modules/util/getAddresssesByTokenByNetwork.d.ts +0 -3
- package/dist/declarations/src/types/tokens.d.ts +0 -11
@@ -1,30 +1,29 @@
|
|
1
|
-
import {
|
1
|
+
import PromisePool$1, { PromisePool } from '@supercharge/promise-pool';
|
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';
|
2
7
|
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';
|
4
8
|
import { newTokenRates } from '@talismn/token-rates';
|
5
|
-
import { isBigInt, BigMath, planckToTokens,
|
9
|
+
import { isBigInt, BigMath, planckToTokens, isTruthy, isArrayOf, isEthereumAddress, hasOwnProperty, decodeAnyAddress, isNotNil, blake2Concat, firstThenDebounce, Deferred } from '@talismn/util';
|
6
10
|
import BigNumber from 'bignumber.js';
|
7
|
-
import { u8aToHex, assert, stringCamelCase, u8aConcatStrict, arrayChunk, u8aToString, hexToNumber, hexToU8a } from '@polkadot/util';
|
8
|
-
import { xxhashAsU8a } from '@polkadot/util-crypto';
|
11
|
+
import { u8aToHex, assert, stringCamelCase, u8aConcatStrict, u8aConcat, arrayChunk, u8aToString, hexToNumber, hexToU8a } from '@polkadot/util';
|
12
|
+
import { xxhashAsU8a, blake2AsU8a } from '@polkadot/util-crypto';
|
9
13
|
import pako from 'pako';
|
10
|
-
import z from 'zod/v4';
|
11
14
|
import { parseAbi, isHex, hexToBigInt } from 'viem';
|
12
|
-
import { fromPairs, toPairs, keys, groupBy as groupBy$1 } from 'lodash';
|
13
|
-
import isEqual from 'lodash/isEqual';
|
14
15
|
import { defineMethod } from '@substrate/txwrapper-core';
|
15
|
-
import { unifyMetadata, decAnyMetadata, getDynamicBuilder, getLookupFn, compactMetadata, encodeMetadata, decodeScale,
|
16
|
+
import { unifyMetadata, decAnyMetadata, getDynamicBuilder, getLookupFn, getMetadataVersion, compactMetadata, encodeMetadata, decodeScale, encodeStateKey, papiParse } from '@talismn/scale';
|
16
17
|
import camelCase from 'lodash/camelCase';
|
17
|
-
import PQueue from 'p-queue';
|
18
|
-
import { fetchBestMetadata, getScaleApi } from '@talismn/sapi';
|
19
18
|
import { Metadata, TypeRegistry } from '@polkadot/types';
|
20
19
|
import groupBy from 'lodash/groupBy';
|
21
20
|
import { mergeUint8, toHex } from '@polkadot-api/utils';
|
22
21
|
import { Binary, AccountId } from 'polkadot-api';
|
23
22
|
import { ChainConnectionError } from '@talismn/chain-connector';
|
24
|
-
import {
|
25
|
-
import { u32, Struct, u128 } from 'scale-ts';
|
23
|
+
import { u32, u128, Struct } from 'scale-ts';
|
26
24
|
import upperFirst from 'lodash/upperFirst';
|
27
25
|
import { Abi } from '@polkadot/api-contract';
|
26
|
+
import { compressToEncodedURIComponent } from 'lz-string';
|
28
27
|
|
29
28
|
// TODO: Document default balances module purpose/usage
|
30
29
|
const DefaultBalanceModule = type => ({
|
@@ -59,11 +58,48 @@ const DefaultBalanceModule = type => ({
|
|
59
58
|
// internal
|
60
59
|
//
|
61
60
|
|
62
|
-
|
63
|
-
|
64
|
-
|
61
|
+
/**
|
62
|
+
* Fetches tokens for EVM networks.
|
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"};
|
65
101
|
|
66
|
-
var log = anylogger(
|
102
|
+
var log = anylogger(packageJson.name);
|
67
103
|
|
68
104
|
function excludeFromTransferableAmount(locks) {
|
69
105
|
if (typeof locks === "string") return BigInt(locks);
|
@@ -273,13 +309,15 @@ class Balances {
|
|
273
309
|
return new SumBalancesFormatter(this);
|
274
310
|
}
|
275
311
|
}
|
312
|
+
const isBalanceEvm = balance => "evmNetworkId" in balance;
|
276
313
|
const getBalanceId = balance => {
|
277
314
|
const {
|
278
315
|
source,
|
279
316
|
address,
|
280
317
|
tokenId
|
281
318
|
} = balance;
|
282
|
-
|
319
|
+
const locationId = isBalanceEvm(balance) ? balance.evmNetworkId : balance.chainId;
|
320
|
+
return [source, address, locationId, tokenId].filter(isTruthy).join("::");
|
283
321
|
};
|
284
322
|
|
285
323
|
/**
|
@@ -341,17 +379,23 @@ class Balance {
|
|
341
379
|
get address() {
|
342
380
|
return this.#storage.address;
|
343
381
|
}
|
344
|
-
get
|
345
|
-
return this.#storage.
|
382
|
+
get chainId() {
|
383
|
+
return isBalanceEvm(this.#storage) ? undefined : this.#storage.chainId;
|
384
|
+
}
|
385
|
+
get chain() {
|
386
|
+
return this.#db?.chains && this.chainId && this.#db?.chains[this.chainId] || null;
|
346
387
|
}
|
347
|
-
get
|
348
|
-
return this.#
|
388
|
+
get evmNetworkId() {
|
389
|
+
return isBalanceEvm(this.#storage) ? this.#storage.evmNetworkId : undefined;
|
390
|
+
}
|
391
|
+
get evmNetwork() {
|
392
|
+
return this.#db?.evmNetworks && this.evmNetworkId && this.#db?.evmNetworks[this.evmNetworkId] || null;
|
349
393
|
}
|
350
394
|
get tokenId() {
|
351
395
|
return this.#storage.tokenId;
|
352
396
|
}
|
353
397
|
get token() {
|
354
|
-
return this.#db?.tokens?.[this.tokenId] || null;
|
398
|
+
return this.#db?.tokens && this.#db?.tokens[this.tokenId] || null;
|
355
399
|
}
|
356
400
|
get decimals() {
|
357
401
|
return this.token?.decimals || null;
|
@@ -364,9 +408,9 @@ class Balance {
|
|
364
408
|
//
|
365
409
|
// This means that those rates are always available for calculating the uniswapv2 rates,
|
366
410
|
// regardless of whether or not the underlying erc20s are actually in chaindata and enabled.
|
367
|
-
if (this.isSource("evm-uniswapv2") && this.token?.type === "evm-uniswapv2") {
|
368
|
-
const tokenId0 = evmErc20TokenId(this.
|
369
|
-
const tokenId1 = evmErc20TokenId(this.
|
411
|
+
if (this.isSource("evm-uniswapv2") && this.token?.type === "evm-uniswapv2" && this.evmNetworkId) {
|
412
|
+
const tokenId0 = evmErc20TokenId$1(this.evmNetworkId, this.token.tokenAddress0);
|
413
|
+
const tokenId1 = evmErc20TokenId$1(this.evmNetworkId, this.token.tokenAddress1);
|
370
414
|
const decimals = this.token.decimals;
|
371
415
|
const decimals0 = this.token.decimals0;
|
372
416
|
const decimals1 = this.token.decimals1;
|
@@ -445,7 +489,9 @@ class Balance {
|
|
445
489
|
const nomPoolStakedPlancks = this.locks.some(lock => lock.source === "substrate-native-holds" && lock.label === "DelegatedStaking") ? 0n : this.nompools.map(({
|
446
490
|
amount
|
447
491
|
}) => amount.planck).reduce((a, b) => a + b, 0n);
|
448
|
-
return this.#format(this.free.planck + this.reserved.planck + nomPoolStakedPlancks + this.
|
492
|
+
return this.#format(this.free.planck + this.reserved.planck + nomPoolStakedPlancks + this.crowdloans.map(({
|
493
|
+
amount
|
494
|
+
}) => amount.planck).reduce((a, b) => a + b, 0n) + this.subtensor.map(({
|
449
495
|
amount
|
450
496
|
}) => amount.planck).reduce((a, b) => a + b, 0n) + includeInTotalExtraAmount(extra));
|
451
497
|
}
|
@@ -481,6 +527,9 @@ class Balance {
|
|
481
527
|
get locks() {
|
482
528
|
return this.getValue("locked");
|
483
529
|
}
|
530
|
+
get crowdloans() {
|
531
|
+
return this.getValue("crowdloan");
|
532
|
+
}
|
484
533
|
get nompools() {
|
485
534
|
return this.getValue("nompool");
|
486
535
|
}
|
@@ -559,7 +608,7 @@ class Balance {
|
|
559
608
|
const nomPoolStakedPlancks = this.locks.some(lock => lock.source === "substrate-native-holds" && lock.label === "DelegatedStaking") ? 0n : this.nompools.map(({
|
560
609
|
amount
|
561
610
|
}) => amount.planck).reduce((a, b) => a + b, 0n);
|
562
|
-
const otherUnavailable = nomPoolStakedPlancks + this.subtensor.reduce((total, each) => total + each.amount.planck, 0n);
|
611
|
+
const otherUnavailable = nomPoolStakedPlancks + this.crowdloans.reduce((total, each) => total + each.amount.planck, 0n) + this.subtensor.reduce((total, each) => total + each.amount.planck, 0n);
|
563
612
|
return this.#format(baseUnavailable + otherUnavailable);
|
564
613
|
}
|
565
614
|
|
@@ -794,6 +843,10 @@ const filterMirrorTokens = (balance, i, balances) => {
|
|
794
843
|
return !mirrorOf || !balances.find(b => b.tokenId === mirrorOf);
|
795
844
|
};
|
796
845
|
|
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
|
+
|
797
850
|
/**
|
798
851
|
* `BalanceTypes` is an automatically determined sub-selection of `PluginBalanceTypes`.
|
799
852
|
*
|
@@ -824,6 +877,7 @@ const getValueId = amount => {
|
|
824
877
|
const getMetaId = () => {
|
825
878
|
const meta = amount.meta;
|
826
879
|
if (!meta) return "";
|
880
|
+
if (amount.type === "crowdloan") return meta.paraId?.toString() ?? "";
|
827
881
|
if (amount.type === "nompool") return meta.poolId?.toString() ?? "";
|
828
882
|
if (amount.type === "subtensor") {
|
829
883
|
const {
|
@@ -847,9 +901,10 @@ const getValueId = amount => {
|
|
847
901
|
const deriveMiniMetadataId = ({
|
848
902
|
source,
|
849
903
|
chainId,
|
904
|
+
specName,
|
850
905
|
specVersion,
|
851
|
-
|
852
|
-
}) => u8aToHex(xxhashAsU8a(new TextEncoder().encode(`${source}${chainId}${specVersion}${
|
906
|
+
balancesConfig
|
907
|
+
}) => u8aToHex(xxhashAsU8a(new TextEncoder().encode(`${source}${chainId}${specName}${specVersion}${balancesConfig}`), 64), undefined, false);
|
853
908
|
|
854
909
|
// for DB version 3, Wallet version 1.21.0
|
855
910
|
const upgradeRemoveSymbolFromNativeTokenId = async tx => {
|
@@ -944,9 +999,230 @@ class TalismanBalancesDatabase extends Dexie {
|
|
944
999
|
}
|
945
1000
|
const db = new TalismanBalancesDatabase();
|
946
1001
|
|
947
|
-
const
|
948
|
-
|
949
|
-
|
1002
|
+
const minimumHydrationInterval = 300_000; // 300_000ms = 300s = 5 minutes
|
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
|
+
}
|
950
1226
|
|
951
1227
|
const erc20Abi = [{
|
952
1228
|
constant: true,
|
@@ -1178,11 +1454,8 @@ const erc20Abi = [{
|
|
1178
1454
|
|
1179
1455
|
const erc20BalancesAggregatorAbi = parseAbi(["struct AccountToken {address account; address token;}", "function balances(AccountToken[] memory accountTokens) public view returns (uint256[] memory)"]);
|
1180
1456
|
|
1181
|
-
const moduleType$
|
1182
|
-
const
|
1183
|
-
contractAddress: EvmErc20TokenSchema.shape.contractAddress,
|
1184
|
-
...TokenConfigBaseSchema.shape
|
1185
|
-
});
|
1457
|
+
const moduleType$8 = "evm-erc20";
|
1458
|
+
const evmErc20TokenId = (chainId, tokenContractAddress) => `${chainId}-evm-erc20-${tokenContractAddress}`.toLowerCase();
|
1186
1459
|
const EvmErc20Module = hydrate => {
|
1187
1460
|
const {
|
1188
1461
|
chainConnectors,
|
@@ -1206,36 +1479,38 @@ const EvmErc20Module = hydrate => {
|
|
1206
1479
|
}, {});
|
1207
1480
|
};
|
1208
1481
|
const getModuleTokens = async () => {
|
1209
|
-
return await chaindataProvider.
|
1482
|
+
return await chaindataProvider.tokensByIdForType(moduleType$8);
|
1210
1483
|
};
|
1211
1484
|
const getErc20Aggregators = async () => {
|
1212
|
-
const evmNetworks = await chaindataProvider.
|
1213
|
-
return Object.fromEntries(evmNetworks.filter(n => n.
|
1485
|
+
const evmNetworks = await chaindataProvider.evmNetworks();
|
1486
|
+
return Object.fromEntries(evmNetworks.filter(n => n.erc20aggregator).map(n => [n.id, n.erc20aggregator]));
|
1214
1487
|
};
|
1215
1488
|
return {
|
1216
|
-
...DefaultBalanceModule(moduleType$
|
1489
|
+
...DefaultBalanceModule(moduleType$8),
|
1217
1490
|
/**
|
1218
1491
|
* This method is currently executed on [a squid](https://github.com/TalismanSociety/chaindata-squid/blob/0ee02818bf5caa7362e3f3664e55ef05ec8df078/src/steps/updateEvmNetworksFromGithub.ts#L280-L284).
|
1219
1492
|
* 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.
|
1220
1493
|
*/
|
1221
|
-
async fetchEvmChainMeta(
|
1494
|
+
async fetchEvmChainMeta(chainId) {
|
1495
|
+
const isTestnet = (await chaindataProvider.evmNetworkById(chainId))?.isTestnet || false;
|
1222
1496
|
return {
|
1223
|
-
|
1224
|
-
extra: null
|
1497
|
+
isTestnet
|
1225
1498
|
};
|
1226
1499
|
},
|
1227
1500
|
/**
|
1228
1501
|
* This method is currently executed on [a squid](https://github.com/TalismanSociety/chaindata-squid/blob/0ee02818bf5caa7362e3f3664e55ef05ec8df078/src/steps/updateEvmNetworksFromGithub.ts#L338-L343).
|
1229
1502
|
* 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.
|
1230
1503
|
*/
|
1231
|
-
async fetchEvmChainTokens(chainId,
|
1504
|
+
async fetchEvmChainTokens(chainId, chainMeta, moduleConfig) {
|
1505
|
+
const {
|
1506
|
+
isTestnet
|
1507
|
+
} = chainMeta;
|
1232
1508
|
const chainTokens = {};
|
1233
|
-
for (const tokenConfig of tokens ?? []) {
|
1509
|
+
for (const tokenConfig of moduleConfig?.tokens ?? []) {
|
1234
1510
|
const {
|
1235
1511
|
contractAddress,
|
1236
1512
|
symbol: contractSymbol,
|
1237
|
-
decimals: contractDecimals
|
1238
|
-
name: contractName
|
1513
|
+
decimals: contractDecimals
|
1239
1514
|
} = tokenConfig;
|
1240
1515
|
// TODO : in chaindata's build, filter out all tokens that don't have any of these
|
1241
1516
|
if (!contractAddress || !contractSymbol || contractDecimals === undefined) {
|
@@ -1249,25 +1524,22 @@ const EvmErc20Module = hydrate => {
|
|
1249
1524
|
const token = {
|
1250
1525
|
id,
|
1251
1526
|
type: "evm-erc20",
|
1252
|
-
|
1527
|
+
isTestnet,
|
1253
1528
|
isDefault: tokenConfig.isDefault ?? true,
|
1254
1529
|
symbol,
|
1255
1530
|
decimals,
|
1256
|
-
|
1257
|
-
logo: tokenConfig?.logo,
|
1531
|
+
logo: tokenConfig?.logo || githubTokenLogoUrl(id),
|
1258
1532
|
contractAddress,
|
1259
|
-
|
1533
|
+
evmNetwork: {
|
1534
|
+
id: chainId
|
1535
|
+
}
|
1260
1536
|
};
|
1261
1537
|
if (tokenConfig?.symbol) token.symbol = tokenConfig?.symbol;
|
1262
1538
|
if (tokenConfig?.coingeckoId) token.coingeckoId = tokenConfig?.coingeckoId;
|
1539
|
+
if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName;
|
1263
1540
|
if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf;
|
1264
1541
|
if (tokenConfig?.noDiscovery) token.noDiscovery = tokenConfig?.noDiscovery;
|
1265
|
-
|
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
|
-
}
|
1542
|
+
chainTokens[token.id] = token;
|
1271
1543
|
}
|
1272
1544
|
return chainTokens;
|
1273
1545
|
},
|
@@ -1279,18 +1551,18 @@ const EvmErc20Module = hydrate => {
|
|
1279
1551
|
const subscriptionInterval = 6_000; // 6_000ms == 6 seconds
|
1280
1552
|
const initDelay = 1_500; // 1_500ms == 1.5 seconds
|
1281
1553
|
const initialisingBalances = new Set();
|
1282
|
-
const positiveBalanceNetworks = new Set(initialBalances?.map(b => b.
|
1554
|
+
const positiveBalanceNetworks = new Set(initialBalances?.map(b => b.evmNetworkId));
|
1283
1555
|
const tokens = await getModuleTokens();
|
1284
1556
|
|
1285
1557
|
// for chains with a zero balance we only call fetchBalances once every 5 subscriptionIntervals
|
1286
1558
|
// if subscriptionInterval is 6 seconds, this means we only poll chains with a zero balance every 30 seconds
|
1287
1559
|
let zeroBalanceSubscriptionIntervalCounter = 0;
|
1288
|
-
const evmNetworks = await chaindataProvider.
|
1560
|
+
const evmNetworks = await chaindataProvider.evmNetworksById();
|
1289
1561
|
const ethAddressesByToken = Object.fromEntries(Object.entries(addressesByToken).map(([tokenId, addresses]) => {
|
1290
1562
|
const ethAddresses = addresses.filter(isEthereumAddress);
|
1291
1563
|
if (ethAddresses.length === 0) return null;
|
1292
1564
|
const token = tokens[tokenId];
|
1293
|
-
const evmNetworkId = token.
|
1565
|
+
const evmNetworkId = token.evmNetwork?.id;
|
1294
1566
|
if (!evmNetworkId) return null;
|
1295
1567
|
return [tokenId, ethAddresses];
|
1296
1568
|
}).filter(x => Boolean(x)));
|
@@ -1416,17 +1688,20 @@ const fetchBalances$3 = async (evmChainConnector, tokenAddressesByNetwork, erc20
|
|
1416
1688
|
results: [],
|
1417
1689
|
errors: []
|
1418
1690
|
};
|
1419
|
-
await Promise.all(Object.entries(tokenAddressesByNetwork).map(async ([
|
1420
|
-
const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(
|
1421
|
-
if (!publicClient) throw new EvmErc20NetworkError(`Could not get rpc provider for evm network ${
|
1422
|
-
const balances = await getEvmTokenBalances(publicClient, networkParams, result.errors, erc20Aggregators[
|
1691
|
+
await Promise.all(Object.entries(tokenAddressesByNetwork).map(async ([evmNetworkId, networkParams]) => {
|
1692
|
+
const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(evmNetworkId);
|
1693
|
+
if (!publicClient) throw new EvmErc20NetworkError(`Could not get rpc provider for evm network ${evmNetworkId}`, evmNetworkId);
|
1694
|
+
const balances = await getEvmTokenBalances(publicClient, networkParams, result.errors, erc20Aggregators[evmNetworkId]);
|
1423
1695
|
|
1424
1696
|
// consider only non null balances in the results
|
1425
1697
|
result.results.push(...balances.filter(isTruthy).map((free, i) => ({
|
1426
1698
|
source: "evm-erc20",
|
1427
1699
|
status: "live",
|
1428
1700
|
address: networkParams[i].address,
|
1429
|
-
|
1701
|
+
multiChainId: {
|
1702
|
+
evmChainId: evmNetworkId
|
1703
|
+
},
|
1704
|
+
evmNetworkId,
|
1430
1705
|
tokenId: networkParams[i].token.id,
|
1431
1706
|
value: free
|
1432
1707
|
})));
|
@@ -1497,7 +1772,7 @@ function groupAddressesByTokenByEvmNetwork$1(addressesByToken, tokens) {
|
|
1497
1772
|
log.error(`Token ${tokenId} not found`);
|
1498
1773
|
return byChain;
|
1499
1774
|
}
|
1500
|
-
const chainId = token.
|
1775
|
+
const chainId = token.evmNetwork?.id;
|
1501
1776
|
if (!chainId) {
|
1502
1777
|
log.error(`Token ${tokenId} has no evm network`);
|
1503
1778
|
return byChain;
|
@@ -1510,38 +1785,67 @@ function groupAddressesByTokenByEvmNetwork$1(addressesByToken, tokens) {
|
|
1510
1785
|
|
1511
1786
|
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)"]);
|
1512
1787
|
|
1513
|
-
const moduleType$
|
1514
|
-
const
|
1788
|
+
const moduleType$7 = "evm-native";
|
1789
|
+
const evmNativeTokenId = chainId => `${chainId}-evm-native`.toLowerCase().replace(/ /g, "-");
|
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
|
+
};
|
1515
1795
|
const EvmNativeModule = hydrate => {
|
1516
1796
|
const {
|
1517
1797
|
chainConnectors,
|
1518
1798
|
chaindataProvider
|
1519
1799
|
} = hydrate;
|
1520
1800
|
const getModuleTokens = async () => {
|
1521
|
-
return await chaindataProvider.
|
1801
|
+
return await chaindataProvider.tokensByIdForType(moduleType$7);
|
1522
1802
|
};
|
1523
1803
|
return {
|
1524
|
-
...DefaultBalanceModule(moduleType$
|
1804
|
+
...DefaultBalanceModule(moduleType$7),
|
1525
1805
|
get tokens() {
|
1526
|
-
return chaindataProvider.
|
1806
|
+
return chaindataProvider.tokensByIdForType(moduleType$7);
|
1527
1807
|
},
|
1528
1808
|
/**
|
1529
1809
|
* This method is currently executed on [a squid](https://github.com/TalismanSociety/chaindata-squid/blob/0ee02818bf5caa7362e3f3664e55ef05ec8df078/src/steps/updateEvmNetworksFromGithub.ts#L280-L284).
|
1530
1810
|
* 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.
|
1531
1811
|
*/
|
1532
|
-
async fetchEvmChainMeta(
|
1812
|
+
async fetchEvmChainMeta(chainId) {
|
1813
|
+
const isTestnet = (await chaindataProvider.evmNetworkById(chainId))?.isTestnet || false;
|
1533
1814
|
return {
|
1534
|
-
|
1535
|
-
extra: null
|
1815
|
+
isTestnet
|
1536
1816
|
};
|
1537
1817
|
},
|
1538
1818
|
/**
|
1539
1819
|
* This method is currently executed on [a squid](https://github.com/TalismanSociety/chaindata-squid/blob/0ee02818bf5caa7362e3f3664e55ef05ec8df078/src/steps/updateEvmNetworksFromGithub.ts#L338-L343).
|
1540
1820
|
* 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.
|
1541
1821
|
*/
|
1542
|
-
async fetchEvmChainTokens() {
|
1543
|
-
|
1544
|
-
|
1822
|
+
async fetchEvmChainTokens(chainId, chainMeta, moduleConfig) {
|
1823
|
+
const {
|
1824
|
+
isTestnet
|
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
|
+
};
|
1545
1849
|
},
|
1546
1850
|
async subscribeBalances({
|
1547
1851
|
addressesByToken,
|
@@ -1552,11 +1856,11 @@ const EvmNativeModule = hydrate => {
|
|
1552
1856
|
const initDelay = 500; // 500ms == 0.5 seconds
|
1553
1857
|
|
1554
1858
|
const tokens = await getModuleTokens();
|
1555
|
-
const ethAddressesByToken =
|
1859
|
+
const ethAddressesByToken = Object.fromEntries(Object.entries(addressesByToken).map(([tokenId, addresses]) => {
|
1556
1860
|
const ethAddresses = addresses.filter(isEthereumAddress);
|
1557
1861
|
if (ethAddresses.length === 0) return null;
|
1558
1862
|
const token = tokens[tokenId];
|
1559
|
-
const evmNetworkId = token.
|
1863
|
+
const evmNetworkId = token.evmNetwork?.id;
|
1560
1864
|
if (!evmNetworkId) return null;
|
1561
1865
|
return [tokenId, ethAddresses];
|
1562
1866
|
}).filter(x => Boolean(x)));
|
@@ -1566,16 +1870,16 @@ const EvmNativeModule = hydrate => {
|
|
1566
1870
|
let zeroBalanceSubscriptionIntervalCounter = 0;
|
1567
1871
|
|
1568
1872
|
// setup initialising balances for all active evm networks
|
1569
|
-
const activeEvmNetworkIds = keys(ethAddressesByToken).map(
|
1873
|
+
const activeEvmNetworkIds = Object.keys(ethAddressesByToken).map(getEvmNetworkIdFromTokenId);
|
1570
1874
|
const initialisingBalances = new Set(activeEvmNetworkIds);
|
1571
|
-
const positiveBalanceNetworks = new Set(initialBalances?.map(b => b.
|
1875
|
+
const positiveBalanceNetworks = new Set(initialBalances?.map(b => b.evmNetworkId));
|
1572
1876
|
const poll = async () => {
|
1573
1877
|
if (!subscriptionActive) return;
|
1574
1878
|
zeroBalanceSubscriptionIntervalCounter = (zeroBalanceSubscriptionIntervalCounter + 1) % 5;
|
1575
1879
|
try {
|
1576
1880
|
// fetch balance for each network sequentially to prevent creating a big queue of http requests (browser can only handle 2 at a time)
|
1577
1881
|
for (const [tokenId, addresses] of Object.entries(ethAddressesByToken)) {
|
1578
|
-
const evmNetworkId =
|
1882
|
+
const evmNetworkId = getEvmNetworkIdFromTokenId(tokenId);
|
1579
1883
|
|
1580
1884
|
// a zero balance network is one that has initialised and does not have a positive balance
|
1581
1885
|
const isZeroBalanceNetwork = !initialisingBalances.has(evmNetworkId) && !positiveBalanceNetworks.has(evmNetworkId);
|
@@ -1595,10 +1899,10 @@ const EvmNativeModule = hydrate => {
|
|
1595
1899
|
log.error(balance.message, balance.networkId);
|
1596
1900
|
initialisingBalances.delete(balance.networkId);
|
1597
1901
|
} else {
|
1598
|
-
if (balance.
|
1599
|
-
initialisingBalances.delete(balance.
|
1902
|
+
if (balance.evmNetworkId) {
|
1903
|
+
initialisingBalances.delete(balance.evmNetworkId);
|
1600
1904
|
if (BigInt(balance.value) > 0n) {
|
1601
|
-
positiveBalanceNetworks.add(balance.
|
1905
|
+
positiveBalanceNetworks.add(balance.evmNetworkId);
|
1602
1906
|
}
|
1603
1907
|
resultBalances.push(balance);
|
1604
1908
|
}
|
@@ -1648,20 +1952,23 @@ const fetchBalances$2 = async (evmChainConnector, addressesByToken, tokens) => {
|
|
1648
1952
|
if (!evmChainConnector) throw new Error(`This module requires an evm chain connector`);
|
1649
1953
|
return Promise.all(Object.entries(addressesByToken).map(async ([tokenId, addresses]) => {
|
1650
1954
|
const token = tokens[tokenId];
|
1651
|
-
const
|
1652
|
-
if (!
|
1653
|
-
const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(
|
1654
|
-
if (!publicClient) throw new Error(`Could not get rpc provider for evm network ${
|
1955
|
+
const evmNetworkId = token.evmNetwork?.id;
|
1956
|
+
if (!evmNetworkId) throw new Error(`Token ${token.id} has no evm network`);
|
1957
|
+
const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(evmNetworkId);
|
1958
|
+
if (!publicClient) throw new Error(`Could not get rpc provider for evm network ${evmNetworkId}`);
|
1655
1959
|
|
1656
1960
|
// fetch all balances
|
1657
1961
|
const freeBalances = await getFreeBalances(publicClient, addresses);
|
1658
1962
|
const balanceResults = addresses.map((address, i) => {
|
1659
|
-
if (freeBalances[i] === "error") return new EvmNativeBalanceError("Could not fetch balance ",
|
1963
|
+
if (freeBalances[i] === "error") return new EvmNativeBalanceError("Could not fetch balance ", evmNetworkId);
|
1660
1964
|
return {
|
1661
1965
|
source: "evm-native",
|
1662
1966
|
status: "live",
|
1663
1967
|
address: address,
|
1664
|
-
|
1968
|
+
multiChainId: {
|
1969
|
+
evmChainId: evmNetworkId
|
1970
|
+
},
|
1971
|
+
evmNetworkId,
|
1665
1972
|
tokenId,
|
1666
1973
|
value: freeBalances[i].toString()
|
1667
1974
|
};
|
@@ -1714,7 +2021,7 @@ async function getFreeBalances(publicClient, addresses) {
|
|
1714
2021
|
});
|
1715
2022
|
} catch (err) {
|
1716
2023
|
const errorMessage = hasOwnProperty(err, "shortMessage") ? err.shortMessage : hasOwnProperty(err, "message") ? err.message : err;
|
1717
|
-
log.warn(`Failed to get balance from chain ${publicClient.chain
|
2024
|
+
log.warn(`Failed to get balance from chain ${publicClient.chain?.id} for ${ethAddresses.length} addresses: ${errorMessage}`);
|
1718
2025
|
return ethAddresses.map(() => "error");
|
1719
2026
|
}
|
1720
2027
|
}
|
@@ -2282,20 +2589,8 @@ const uniswapV2PairAbi = [{
|
|
2282
2589
|
type: "function"
|
2283
2590
|
}];
|
2284
2591
|
|
2285
|
-
const moduleType$
|
2286
|
-
const
|
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
|
-
});
|
2592
|
+
const moduleType$6 = "evm-uniswapv2";
|
2593
|
+
const evmUniswapV2TokenId = (chainId, contractAddress) => `${chainId}-evm-uniswapv2-${contractAddress}`.toLowerCase();
|
2299
2594
|
const EvmUniswapV2Module = hydrate => {
|
2300
2595
|
const {
|
2301
2596
|
chainConnectors,
|
@@ -2304,16 +2599,19 @@ const EvmUniswapV2Module = hydrate => {
|
|
2304
2599
|
const chainConnector = chainConnectors.evm;
|
2305
2600
|
assert(chainConnector, "This module requires an evm chain connector");
|
2306
2601
|
return {
|
2307
|
-
...DefaultBalanceModule(moduleType$
|
2308
|
-
async fetchEvmChainMeta(
|
2602
|
+
...DefaultBalanceModule(moduleType$6),
|
2603
|
+
async fetchEvmChainMeta(chainId) {
|
2604
|
+
const isTestnet = (await chaindataProvider.evmNetworkById(chainId))?.isTestnet || false;
|
2309
2605
|
return {
|
2310
|
-
|
2311
|
-
extra: null
|
2606
|
+
isTestnet
|
2312
2607
|
};
|
2313
2608
|
},
|
2314
|
-
async fetchEvmChainTokens(chainId,
|
2609
|
+
async fetchEvmChainTokens(chainId, chainMeta, moduleConfig) {
|
2610
|
+
const {
|
2611
|
+
isTestnet
|
2612
|
+
} = chainMeta;
|
2315
2613
|
const tokens = {};
|
2316
|
-
for (const tokenConfig of pools ?? []) {
|
2614
|
+
for (const tokenConfig of moduleConfig?.pools ?? []) {
|
2317
2615
|
const {
|
2318
2616
|
contractAddress,
|
2319
2617
|
decimals,
|
@@ -2324,8 +2622,7 @@ const EvmUniswapV2Module = hydrate => {
|
|
2324
2622
|
tokenAddress0,
|
2325
2623
|
tokenAddress1,
|
2326
2624
|
coingeckoId0,
|
2327
|
-
coingeckoId1
|
2328
|
-
name
|
2625
|
+
coingeckoId1
|
2329
2626
|
} = tokenConfig;
|
2330
2627
|
if (!contractAddress || decimals === undefined || symbol0 === undefined || decimals0 === undefined || symbol1 === undefined || decimals1 === undefined || tokenAddress0 === undefined || tokenAddress1 === undefined) {
|
2331
2628
|
log.warn("ignoring token on chain %s", chainId, tokenConfig);
|
@@ -2335,12 +2632,11 @@ const EvmUniswapV2Module = hydrate => {
|
|
2335
2632
|
const token = {
|
2336
2633
|
id,
|
2337
2634
|
type: "evm-uniswapv2",
|
2338
|
-
|
2635
|
+
isTestnet,
|
2339
2636
|
isDefault: tokenConfig.isDefault ?? false,
|
2340
2637
|
symbol: `${symbol0 ?? "UNKNOWN"}/${symbol1 ?? "UNKNOWN"}`,
|
2341
|
-
name: name ?? `${symbol0 ?? "UNKNOWN"}/${symbol1 ?? "UNKNOWN"}`,
|
2342
2638
|
decimals,
|
2343
|
-
logo: tokenConfig?.logo ||
|
2639
|
+
logo: tokenConfig?.logo || githubTokenLogoUrl("uniswap"),
|
2344
2640
|
symbol0,
|
2345
2641
|
decimals0,
|
2346
2642
|
symbol1,
|
@@ -2350,10 +2646,13 @@ const EvmUniswapV2Module = hydrate => {
|
|
2350
2646
|
tokenAddress1,
|
2351
2647
|
coingeckoId0,
|
2352
2648
|
coingeckoId1,
|
2353
|
-
|
2649
|
+
evmNetwork: {
|
2650
|
+
id: chainId
|
2651
|
+
}
|
2354
2652
|
};
|
2355
2653
|
if (tokenConfig?.symbol) token.symbol = tokenConfig?.symbol;
|
2356
2654
|
if (tokenConfig?.coingeckoId) token.coingeckoId = tokenConfig?.coingeckoId;
|
2655
|
+
if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName;
|
2357
2656
|
if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf;
|
2358
2657
|
if (tokenConfig?.noDiscovery) token.noDiscovery = tokenConfig?.noDiscovery;
|
2359
2658
|
tokens[token.id] = token;
|
@@ -2369,9 +2668,9 @@ const EvmUniswapV2Module = hydrate => {
|
|
2369
2668
|
const initDelay = 1_500; // 1_500ms == 1.5 seconds
|
2370
2669
|
|
2371
2670
|
const initialBalancesByNetwork = initialBalances?.reduce((result, b) => {
|
2372
|
-
if (!b.
|
2373
|
-
if (!result[b.
|
2374
|
-
result[b.
|
2671
|
+
if (!b.evmNetworkId) return result;
|
2672
|
+
if (!result[b.evmNetworkId]) result[b.evmNetworkId] = {};
|
2673
|
+
result[b.evmNetworkId][getBalanceId(b)] = b;
|
2375
2674
|
return result;
|
2376
2675
|
}, {});
|
2377
2676
|
const cache = new Map(Object.entries(initialBalancesByNetwork ?? {}));
|
@@ -2379,8 +2678,8 @@ const EvmUniswapV2Module = hydrate => {
|
|
2379
2678
|
// for chains with a zero balance we only call fetchBalances once every 5 subscriptionIntervals
|
2380
2679
|
// if subscriptionInterval is 6 seconds, this means we only poll chains with a zero balance every 30 seconds
|
2381
2680
|
let zeroBalanceSubscriptionIntervalCounter = 0;
|
2382
|
-
const evmNetworks = await chaindataProvider.
|
2383
|
-
const tokens = await chaindataProvider.
|
2681
|
+
const evmNetworks = await chaindataProvider.evmNetworksById();
|
2682
|
+
const tokens = await chaindataProvider.tokensById();
|
2384
2683
|
const poll = async () => {
|
2385
2684
|
if (!subscriptionActive) return;
|
2386
2685
|
zeroBalanceSubscriptionIntervalCounter = (zeroBalanceSubscriptionIntervalCounter + 1) % 5;
|
@@ -2423,20 +2722,20 @@ const EvmUniswapV2Module = hydrate => {
|
|
2423
2722
|
},
|
2424
2723
|
async fetchBalances(addressesByToken) {
|
2425
2724
|
if (!chainConnectors.evm) throw new Error(`This module requires an evm chain connector`);
|
2426
|
-
const evmNetworks = await chaindataProvider.
|
2427
|
-
const tokens = await chaindataProvider.
|
2725
|
+
const evmNetworks = await chaindataProvider.evmNetworksById();
|
2726
|
+
const tokens = await chaindataProvider.tokensById();
|
2428
2727
|
return fetchBalances$1(chainConnectors.evm, evmNetworks, tokens, addressesByToken);
|
2429
2728
|
}
|
2430
2729
|
};
|
2431
2730
|
};
|
2432
2731
|
const fetchBalances$1 = async (evmChainConnector, evmNetworks, tokens, addressesByToken) => {
|
2433
2732
|
const addressesByTokenGroupedByEvmNetwork = groupAddressesByTokenByEvmNetwork(addressesByToken, tokens);
|
2434
|
-
const balances = (await Promise.allSettled(Object.entries(addressesByTokenGroupedByEvmNetwork).map(async ([
|
2733
|
+
const balances = (await Promise.allSettled(Object.entries(addressesByTokenGroupedByEvmNetwork).map(async ([evmNetworkId, addressesByToken]) => {
|
2435
2734
|
if (!evmChainConnector) throw new Error(`This module requires an evm chain connector`);
|
2436
|
-
const evmNetwork = evmNetworks[
|
2437
|
-
if (!evmNetwork) throw new Error(`Evm network ${
|
2438
|
-
const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(
|
2439
|
-
if (!publicClient) throw new Error(`Could not get rpc provider for evm network ${
|
2735
|
+
const evmNetwork = evmNetworks[evmNetworkId];
|
2736
|
+
if (!evmNetwork) throw new Error(`Evm network ${evmNetworkId} not found`);
|
2737
|
+
const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(evmNetworkId);
|
2738
|
+
if (!publicClient) throw new Error(`Could not get rpc provider for evm network ${evmNetworkId}`);
|
2440
2739
|
const tokensAndAddresses = Object.entries(addressesByToken).reduce((tokensAndAddresses, [tokenId, addresses]) => {
|
2441
2740
|
const token = tokens[tokenId];
|
2442
2741
|
if (!token) {
|
@@ -2458,7 +2757,10 @@ const fetchBalances$1 = async (evmChainConnector, evmNetworks, tokens, addresses
|
|
2458
2757
|
source: "evm-uniswapv2",
|
2459
2758
|
status: "live",
|
2460
2759
|
address: address,
|
2461
|
-
|
2760
|
+
multiChainId: {
|
2761
|
+
evmChainId: evmNetwork.id
|
2762
|
+
},
|
2763
|
+
evmNetworkId,
|
2462
2764
|
tokenId: token.id,
|
2463
2765
|
values: await getPoolBalance(publicClient, token.contractAddress, address)
|
2464
2766
|
}));
|
@@ -2494,7 +2796,7 @@ function groupAddressesByTokenByEvmNetwork(addressesByToken, tokens) {
|
|
2494
2796
|
log.error(`Token ${tokenId} not found`);
|
2495
2797
|
return byChain;
|
2496
2798
|
}
|
2497
|
-
const chainId = token.
|
2799
|
+
const chainId = token.evmNetwork?.id;
|
2498
2800
|
if (!chainId) {
|
2499
2801
|
log.error(`Token ${tokenId} has no evm network`);
|
2500
2802
|
return byChain;
|
@@ -2569,170 +2871,6 @@ async function getPoolBalance(publicClient, contractAddress, accountAddress) {
|
|
2569
2871
|
}
|
2570
2872
|
}
|
2571
2873
|
|
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
|
-
|
2736
2874
|
/**
|
2737
2875
|
* Wraps a BalanceModule's fetch/subscribe methods with a single `balances` method.
|
2738
2876
|
* This `balances` method will subscribe if a callback parameter is provided, or otherwise fetch.
|
@@ -2748,16 +2886,44 @@ async function balances(balanceModule, addressesByToken, callback) {
|
|
2748
2886
|
return await balanceModule.fetchBalances(addressesByToken);
|
2749
2887
|
}
|
2750
2888
|
|
2751
|
-
|
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
|
+
|
2752
2917
|
const buildStorageCoders = ({
|
2753
2918
|
chainIds,
|
2754
2919
|
chains,
|
2755
2920
|
miniMetadatas,
|
2921
|
+
moduleType,
|
2756
2922
|
coders
|
2757
2923
|
}) => new Map([...chainIds].flatMap(chainId => {
|
2758
2924
|
const chain = chains[chainId];
|
2759
2925
|
if (!chain) return [];
|
2760
|
-
const miniMetadata =
|
2926
|
+
const [, miniMetadata] = findChainMeta(miniMetadatas, moduleType, chain);
|
2761
2927
|
if (!miniMetadata) return [];
|
2762
2928
|
if (!miniMetadata.data) return [];
|
2763
2929
|
const metadata = unifyMetadata(decAnyMetadata(miniMetadata.data));
|
@@ -2780,28 +2946,6 @@ const buildStorageCoders = ({
|
|
2780
2946
|
return [];
|
2781
2947
|
}
|
2782
2948
|
}));
|
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
|
-
};
|
2805
2949
|
|
2806
2950
|
/**
|
2807
2951
|
* Decodes & unwraps outputs and errors of a given result, contract, and method.
|
@@ -2883,7 +3027,7 @@ const detectTransferMethod = metadataRpc => {
|
|
2883
3027
|
return hasDeprecatedTransferCall ? "transfer" : "transfer_allow_death";
|
2884
3028
|
};
|
2885
3029
|
|
2886
|
-
const getUniqueChainIds = (addressesByToken, tokens) => [...new Set(Object.keys(addressesByToken).map(tokenId => tokens[tokenId]?.
|
3030
|
+
const getUniqueChainIds = (addressesByToken, tokens) => [...new Set(Object.keys(addressesByToken).map(tokenId => tokens[tokenId]?.chain?.id).flatMap(chainId => chainId ? [chainId] : []))];
|
2887
3031
|
|
2888
3032
|
const makeContractCaller = ({
|
2889
3033
|
chainConnector,
|
@@ -2992,15 +3136,8 @@ const decompress = data => {
|
|
2992
3136
|
return JSON.parse(decompressed);
|
2993
3137
|
};
|
2994
3138
|
|
2995
|
-
const moduleType$
|
2996
|
-
const
|
2997
|
-
assetId: SubAssetsTokenSchema.shape.assetId,
|
2998
|
-
...TokenConfigBaseSchema.shape
|
2999
|
-
});
|
3000
|
-
const UNSUPPORTED_CHAIN_META$2 = {
|
3001
|
-
miniMetadata: null,
|
3002
|
-
extra: null
|
3003
|
-
};
|
3139
|
+
const moduleType$5 = "substrate-assets";
|
3140
|
+
const subAssetTokenId = (chainId, assetId, tokenSymbol) => `${chainId}-substrate-assets-${assetId}-${tokenSymbol}`.toLowerCase().replace(/ /g, "-");
|
3004
3141
|
const SubAssetsModule = hydrate => {
|
3005
3142
|
const {
|
3006
3143
|
chainConnectors,
|
@@ -3009,10 +3146,16 @@ const SubAssetsModule = hydrate => {
|
|
3009
3146
|
const chainConnector = chainConnectors.substrate;
|
3010
3147
|
assert(chainConnector, "This module requires a substrate chain connector");
|
3011
3148
|
return {
|
3012
|
-
...DefaultBalanceModule(moduleType$
|
3013
|
-
// TODO make synchronous at the module definition level ?
|
3149
|
+
...DefaultBalanceModule(moduleType$5),
|
3014
3150
|
async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) {
|
3015
|
-
|
3151
|
+
const isTestnet = (await chaindataProvider.chainById(chainId))?.isTestnet || false;
|
3152
|
+
if (metadataRpc === undefined) return {
|
3153
|
+
isTestnet
|
3154
|
+
};
|
3155
|
+
if ((moduleConfig?.tokens ?? []).length < 1) return {
|
3156
|
+
isTestnet
|
3157
|
+
};
|
3158
|
+
const metadataVersion = getMetadataVersion(metadataRpc);
|
3016
3159
|
const metadata = decAnyMetadata(metadataRpc);
|
3017
3160
|
compactMetadata(metadata, [{
|
3018
3161
|
pallet: "Assets",
|
@@ -3020,98 +3163,82 @@ const SubAssetsModule = hydrate => {
|
|
3020
3163
|
}]);
|
3021
3164
|
const miniMetadata = encodeMetadata(metadata);
|
3022
3165
|
return {
|
3166
|
+
isTestnet,
|
3023
3167
|
miniMetadata,
|
3024
|
-
|
3168
|
+
metadataVersion
|
3025
3169
|
};
|
3026
3170
|
},
|
3027
|
-
async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig
|
3028
|
-
if (
|
3171
|
+
async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) {
|
3172
|
+
if ((moduleConfig?.tokens ?? []).length < 1) return {};
|
3029
3173
|
const {
|
3030
|
-
|
3174
|
+
isTestnet,
|
3175
|
+
miniMetadata,
|
3176
|
+
metadataVersion
|
3031
3177
|
} = chainMeta;
|
3032
|
-
if (
|
3178
|
+
if (miniMetadata === undefined || metadataVersion === undefined) return {};
|
3179
|
+
if (metadataVersion < 14) return {};
|
3033
3180
|
const metadata = unifyMetadata(decAnyMetadata(miniMetadata));
|
3034
3181
|
const scaleBuilder = getDynamicBuilder(getLookupFn(metadata));
|
3035
3182
|
const assetCoder = scaleBuilder.buildStorage("Assets", "Asset");
|
3036
3183
|
const metadataCoder = scaleBuilder.buildStorage("Assets", "Metadata");
|
3037
|
-
const
|
3038
|
-
for (const tokenConfig of tokens ?? []) {
|
3184
|
+
const tokens = {};
|
3185
|
+
for (const tokenConfig of moduleConfig?.tokens ?? []) {
|
3039
3186
|
try {
|
3040
|
-
const assetId =
|
3187
|
+
const assetId = typeof tokenConfig.assetId === "number" ? tokenConfig.assetId.toString() : tokenConfig.assetId;
|
3041
3188
|
const assetStateKey = tryEncode(assetCoder, BigInt(assetId)) ?? tryEncode(assetCoder, assetId);
|
3042
3189
|
const metadataStateKey = tryEncode(metadataCoder, BigInt(assetId)) ?? tryEncode(metadataCoder, assetId);
|
3043
3190
|
if (assetStateKey === null || metadataStateKey === null) throw new Error(`Failed to encode stateKey for asset ${assetId} on chain ${chainId}`);
|
3044
3191
|
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)]);
|
3045
3192
|
const existentialDeposit = assetsAsset?.min_balance?.toString?.() ?? "0";
|
3046
3193
|
const symbol = assetsMetadata?.symbol?.asText?.() ?? "Unit";
|
3047
|
-
const name = assetsMetadata?.name?.asText?.() ?? symbol;
|
3048
3194
|
const decimals = assetsMetadata?.decimals ?? 0;
|
3049
3195
|
const isFrozen = assetsMetadata?.is_frozen ?? false;
|
3050
|
-
const id = subAssetTokenId(chainId, assetId);
|
3196
|
+
const id = subAssetTokenId(chainId, assetId, symbol);
|
3051
3197
|
const token = {
|
3052
3198
|
id,
|
3053
3199
|
type: "substrate-assets",
|
3054
|
-
|
3200
|
+
isTestnet,
|
3055
3201
|
isDefault: tokenConfig?.isDefault ?? true,
|
3056
|
-
symbol
|
3057
|
-
name: tokenConfig?.name ?? name,
|
3202
|
+
symbol,
|
3058
3203
|
decimals,
|
3059
|
-
logo: tokenConfig?.logo,
|
3204
|
+
logo: tokenConfig?.logo || githubTokenLogoUrl(id),
|
3060
3205
|
existentialDeposit,
|
3061
3206
|
assetId,
|
3062
3207
|
isFrozen,
|
3063
|
-
|
3208
|
+
chain: {
|
3209
|
+
id: chainId
|
3210
|
+
}
|
3064
3211
|
};
|
3065
3212
|
if (tokenConfig?.symbol) {
|
3066
3213
|
token.symbol = tokenConfig?.symbol;
|
3067
|
-
token.id = subAssetTokenId(chainId, assetId);
|
3214
|
+
token.id = subAssetTokenId(chainId, assetId, token.symbol);
|
3068
3215
|
}
|
3069
3216
|
if (tokenConfig?.coingeckoId) token.coingeckoId = tokenConfig?.coingeckoId;
|
3217
|
+
if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName;
|
3070
3218
|
if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf;
|
3071
|
-
|
3219
|
+
tokens[token.id] = token;
|
3072
3220
|
} catch (error) {
|
3073
3221
|
log.error(`Failed to build substrate-assets token ${tokenConfig.assetId} (${tokenConfig.symbol}) on ${chainId}`, error);
|
3074
3222
|
continue;
|
3075
3223
|
}
|
3076
3224
|
}
|
3077
|
-
return
|
3225
|
+
return tokens;
|
3078
3226
|
},
|
3079
3227
|
// TODO: Don't create empty subscriptions
|
3080
3228
|
async subscribeBalances({
|
3081
3229
|
addressesByToken
|
3082
3230
|
}, callback) {
|
3083
|
-
const
|
3084
|
-
|
3085
|
-
if (
|
3086
|
-
|
3087
|
-
|
3088
|
-
}
|
3089
|
-
|
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
|
-
};
|
3231
|
+
const queries = await buildQueries$4(chaindataProvider, addressesByToken);
|
3232
|
+
const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe((error, result) => {
|
3233
|
+
if (error) return callback(error);
|
3234
|
+
const balances = result?.filter(b => b !== null) ?? [];
|
3235
|
+
if (balances.length > 0) callback(null, new Balances(balances));
|
3236
|
+
});
|
3237
|
+
return unsubscribe;
|
3111
3238
|
},
|
3112
3239
|
async fetchBalances(addressesByToken) {
|
3113
3240
|
assert(chainConnectors.substrate, "This module requires a substrate chain connector");
|
3114
|
-
const queries = await buildQueries$
|
3241
|
+
const queries = await buildQueries$4(chaindataProvider, addressesByToken);
|
3115
3242
|
const result = await new RpcStateQueryHelper(chainConnectors.substrate, queries).fetch();
|
3116
3243
|
const balances = result?.filter(b => b !== null) ?? [];
|
3117
3244
|
return new Balances(balances);
|
@@ -3132,10 +3259,11 @@ const SubAssetsModule = hydrate => {
|
|
3132
3259
|
transferMethod,
|
3133
3260
|
userExtensions
|
3134
3261
|
}) {
|
3135
|
-
const token = await chaindataProvider.
|
3262
|
+
const token = await chaindataProvider.tokenById(tokenId);
|
3136
3263
|
assert(token, `Token ${tokenId} not found in store`);
|
3137
|
-
|
3138
|
-
const
|
3264
|
+
if (token.type !== "substrate-assets") throw new Error(`This module doesn't handle tokens of type ${token.type}`);
|
3265
|
+
const chainId = token.chain.id;
|
3266
|
+
const chain = await chaindataProvider.chainById(chainId);
|
3139
3267
|
assert(chain?.genesisHash, `Chain ${chainId} not found in store`);
|
3140
3268
|
const {
|
3141
3269
|
genesisHash
|
@@ -3180,16 +3308,23 @@ const SubAssetsModule = hydrate => {
|
|
3180
3308
|
}
|
3181
3309
|
};
|
3182
3310
|
};
|
3183
|
-
async function
|
3184
|
-
const
|
3185
|
-
const
|
3186
|
-
const
|
3187
|
-
|
3188
|
-
const
|
3189
|
-
|
3311
|
+
async function buildQueries$4(chaindataProvider, addressesByToken) {
|
3312
|
+
const allChains = await chaindataProvider.chainsById();
|
3313
|
+
const tokens = await chaindataProvider.tokensById();
|
3314
|
+
const miniMetadatas = new Map((await db.miniMetadatas.toArray()).map(miniMetadata => [miniMetadata.id, miniMetadata]));
|
3315
|
+
const uniqueChainIds = getUniqueChainIds(addressesByToken, tokens);
|
3316
|
+
const chains = Object.fromEntries(uniqueChainIds.map(chainId => [chainId, allChains[chainId]]));
|
3317
|
+
const chainStorageCoders = buildStorageCoders({
|
3318
|
+
chainIds: uniqueChainIds,
|
3319
|
+
chains,
|
3320
|
+
miniMetadatas,
|
3321
|
+
moduleType: "substrate-assets",
|
3322
|
+
coders: {
|
3323
|
+
storage: ["Assets", "Account"]
|
3324
|
+
}
|
3190
3325
|
});
|
3191
3326
|
return Object.entries(addressesByToken).flatMap(([tokenId, addresses]) => {
|
3192
|
-
const token =
|
3327
|
+
const token = tokens[tokenId];
|
3193
3328
|
if (!token) {
|
3194
3329
|
log.warn(`Token ${tokenId} not found`);
|
3195
3330
|
return [];
|
@@ -3198,22 +3333,27 @@ async function buildNetworkQueries$2(networkId, chainConnector, chaindataProvide
|
|
3198
3333
|
log.debug(`This module doesn't handle tokens of type ${token.type}`);
|
3199
3334
|
return [];
|
3200
3335
|
}
|
3201
|
-
|
3336
|
+
const chainId = token.chain?.id;
|
3337
|
+
if (!chainId) {
|
3338
|
+
log.warn(`Token ${tokenId} has no chain`);
|
3339
|
+
return [];
|
3340
|
+
}
|
3341
|
+
const chain = chains[chainId];
|
3202
3342
|
if (!chain) {
|
3203
|
-
log.warn(`Chain ${
|
3343
|
+
log.warn(`Chain ${chainId} for token ${tokenId} not found`);
|
3204
3344
|
return [];
|
3205
3345
|
}
|
3206
3346
|
return addresses.flatMap(address => {
|
3207
|
-
const scaleCoder =
|
3208
|
-
const stateKey = tryEncode(scaleCoder,
|
3347
|
+
const scaleCoder = chainStorageCoders.get(chainId)?.storage;
|
3348
|
+
const stateKey = tryEncode(scaleCoder, BigInt(token.assetId), address) ?? tryEncode(scaleCoder, token.assetId, address);
|
3209
3349
|
if (!stateKey) {
|
3210
|
-
log.warn(`Invalid assetId / address in ${
|
3350
|
+
log.warn(`Invalid assetId / address in ${chainId} storage query ${token.assetId} / ${address}`);
|
3211
3351
|
return [];
|
3212
3352
|
}
|
3213
3353
|
const decodeResult = change => {
|
3214
3354
|
/** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
|
3215
3355
|
|
3216
|
-
const decoded = decodeScale(scaleCoder, change, `Failed to decode substrate-assets balance on chain ${
|
3356
|
+
const decoded = decodeScale(scaleCoder, change, `Failed to decode substrate-assets balance on chain ${chainId}`) ?? {
|
3217
3357
|
balance: 0n,
|
3218
3358
|
status: {
|
3219
3359
|
type: "Liquid"
|
@@ -3247,30 +3387,22 @@ async function buildNetworkQueries$2(networkId, chainConnector, chaindataProvide
|
|
3247
3387
|
source: "substrate-assets",
|
3248
3388
|
status: "live",
|
3249
3389
|
address,
|
3250
|
-
|
3390
|
+
multiChainId: {
|
3391
|
+
subChainId: chainId
|
3392
|
+
},
|
3393
|
+
chainId,
|
3251
3394
|
tokenId: token.id,
|
3252
3395
|
values: balanceValues
|
3253
3396
|
};
|
3254
3397
|
};
|
3255
3398
|
return {
|
3256
|
-
chainId
|
3399
|
+
chainId,
|
3257
3400
|
stateKey,
|
3258
3401
|
decodeResult
|
3259
3402
|
};
|
3260
3403
|
});
|
3261
3404
|
});
|
3262
3405
|
}
|
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
|
-
}
|
3274
3406
|
// NOTE: Different chains need different formats for assetId when encoding the stateKey
|
3275
3407
|
// E.g. Polkadot Asset Hub needs it to be a string, Astar needs it to be a bigint
|
3276
3408
|
//
|
@@ -3283,16 +3415,9 @@ const tryEncode = (scaleCoder, ...args) => {
|
|
3283
3415
|
}
|
3284
3416
|
};
|
3285
3417
|
|
3286
|
-
const moduleType$
|
3287
|
-
const
|
3288
|
-
|
3289
|
-
extra: null
|
3290
|
-
};
|
3291
|
-
const SubForeignAssetsTokenConfigSchema = z.strictObject({
|
3292
|
-
onChainId: SubForeignAssetsTokenSchema.shape.onChainId,
|
3293
|
-
...TokenConfigBaseSchema.shape
|
3294
|
-
});
|
3295
|
-
const SubForeignAssetsModule = hydrate => {
|
3418
|
+
const moduleType$4 = "substrate-equilibrium";
|
3419
|
+
const subEquilibriumTokenId = (chainId, tokenSymbol) => `${chainId}-substrate-equilibrium-${tokenSymbol}`.toLowerCase().replace(/ /g, "-");
|
3420
|
+
const SubEquilibriumModule = hydrate => {
|
3296
3421
|
const {
|
3297
3422
|
chainConnectors,
|
3298
3423
|
chaindataProvider
|
@@ -3300,113 +3425,374 @@ const SubForeignAssetsModule = hydrate => {
|
|
3300
3425
|
const chainConnector = chainConnectors.substrate;
|
3301
3426
|
assert(chainConnector, "This module requires a substrate chain connector");
|
3302
3427
|
return {
|
3303
|
-
...DefaultBalanceModule(moduleType$
|
3428
|
+
...DefaultBalanceModule(moduleType$4),
|
3304
3429
|
async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) {
|
3305
|
-
|
3430
|
+
const isTestnet = (await chaindataProvider.chainById(chainId))?.isTestnet || false;
|
3431
|
+
if (metadataRpc === undefined) return {
|
3432
|
+
isTestnet
|
3433
|
+
};
|
3434
|
+
if (moduleConfig?.disable !== false) return {
|
3435
|
+
isTestnet
|
3436
|
+
}; // default to disabled
|
3437
|
+
|
3306
3438
|
const metadataVersion = getMetadataVersion(metadataRpc);
|
3307
|
-
if (metadataVersion < 14) return UNSUPPORTED_CHAIN_META$1;
|
3308
3439
|
const metadata = decAnyMetadata(metadataRpc);
|
3309
3440
|
compactMetadata(metadata, [{
|
3310
|
-
pallet: "
|
3311
|
-
items: ["
|
3441
|
+
pallet: "EqAssets",
|
3442
|
+
items: ["Assets"]
|
3443
|
+
}, {
|
3444
|
+
pallet: "System",
|
3445
|
+
items: ["Account"]
|
3312
3446
|
}]);
|
3313
3447
|
const miniMetadata = encodeMetadata(metadata);
|
3314
3448
|
return {
|
3449
|
+
isTestnet,
|
3315
3450
|
miniMetadata,
|
3316
|
-
|
3451
|
+
metadataVersion
|
3317
3452
|
};
|
3318
3453
|
},
|
3319
|
-
async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig
|
3320
|
-
|
3454
|
+
async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) {
|
3455
|
+
// default to disabled
|
3456
|
+
if (moduleConfig?.disable !== false) return {};
|
3321
3457
|
const {
|
3322
|
-
|
3458
|
+
isTestnet,
|
3459
|
+
miniMetadata,
|
3460
|
+
metadataVersion
|
3323
3461
|
} = chainMeta;
|
3324
|
-
if (
|
3325
|
-
|
3326
|
-
|
3327
|
-
|
3328
|
-
|
3329
|
-
|
3330
|
-
|
3331
|
-
|
3332
|
-
|
3333
|
-
|
3334
|
-
|
3335
|
-
|
3336
|
-
|
3337
|
-
|
3462
|
+
if (miniMetadata === undefined || metadataVersion === undefined) return {};
|
3463
|
+
if (metadataVersion < 14) return {};
|
3464
|
+
try {
|
3465
|
+
const metadata = unifyMetadata(decAnyMetadata(miniMetadata));
|
3466
|
+
const scaleBuilder = getDynamicBuilder(getLookupFn(metadata));
|
3467
|
+
const assetsCoder = scaleBuilder.buildStorage("EqAssets", "Assets");
|
3468
|
+
const stateKey = assetsCoder.keys.enc();
|
3469
|
+
|
3470
|
+
/** NOTE: Just a guideline, the RPC can return whatever it wants */
|
3471
|
+
|
3472
|
+
const assetsResult = await chainConnector.send(chainId, "state_getStorage", [stateKey]).then(result => assetsCoder.value.dec(result) ?? null);
|
3473
|
+
const tokens = (Array.isArray(assetsResult) ? assetsResult : []).flatMap(asset => {
|
3474
|
+
if (!asset) return [];
|
3475
|
+
if (!asset?.id) return [];
|
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
|
3338
3494
|
}
|
3339
|
-
}
|
3340
|
-
if (
|
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;
|
3740
|
+
}
|
3741
|
+
})();
|
3742
|
+
if (onChainId === undefined) continue;
|
3341
3743
|
const assetStateKey = assetCoder.keys.enc(onChainId);
|
3342
3744
|
const metadataStateKey = metadataCoder.keys.enc(onChainId);
|
3343
3745
|
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)]);
|
3344
3746
|
const existentialDeposit = assetsAsset?.min_balance?.toString?.() ?? "0";
|
3345
3747
|
const symbol = assetsMetadata?.symbol?.asText?.() ?? "Unit";
|
3346
|
-
const name = assetsMetadata?.name?.asText?.() ?? symbol;
|
3347
3748
|
const decimals = assetsMetadata?.decimals ?? 0;
|
3348
3749
|
const isFrozen = assetsMetadata?.is_frozen ?? false;
|
3349
|
-
const id = subForeignAssetTokenId(chainId,
|
3750
|
+
const id = subForeignAssetTokenId(chainId, symbol);
|
3350
3751
|
const token = {
|
3351
3752
|
id,
|
3352
3753
|
type: "substrate-foreignassets",
|
3353
|
-
|
3754
|
+
isTestnet,
|
3354
3755
|
isDefault: tokenConfig?.isDefault ?? true,
|
3355
3756
|
symbol,
|
3356
3757
|
decimals,
|
3357
|
-
|
3358
|
-
logo: tokenConfig?.logo,
|
3758
|
+
logo: tokenConfig?.logo || githubTokenLogoUrl(id),
|
3359
3759
|
existentialDeposit,
|
3360
3760
|
onChainId: tokenConfig.onChainId,
|
3361
3761
|
isFrozen,
|
3362
|
-
|
3762
|
+
chain: {
|
3763
|
+
id: chainId
|
3764
|
+
}
|
3363
3765
|
};
|
3766
|
+
if (tokenConfig?.symbol) {
|
3767
|
+
token.symbol = tokenConfig?.symbol;
|
3768
|
+
token.id = subForeignAssetTokenId(chainId, token.symbol);
|
3769
|
+
}
|
3364
3770
|
if (tokenConfig?.coingeckoId) token.coingeckoId = tokenConfig?.coingeckoId;
|
3771
|
+
if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName;
|
3365
3772
|
if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf;
|
3366
|
-
|
3773
|
+
tokens[token.id] = token;
|
3367
3774
|
} catch (error) {
|
3368
3775
|
log.error(`Failed to build substrate-foreignassets token ${tokenConfig.onChainId} (${tokenConfig.symbol}) on ${chainId}`, error?.message ?? error);
|
3369
3776
|
continue;
|
3370
3777
|
}
|
3371
3778
|
}
|
3372
|
-
return
|
3779
|
+
return tokens;
|
3373
3780
|
},
|
3374
3781
|
// TODO: Don't create empty subscriptions
|
3375
3782
|
async subscribeBalances({
|
3376
3783
|
addressesByToken
|
3377
3784
|
}, callback) {
|
3378
|
-
const
|
3379
|
-
|
3380
|
-
if (
|
3381
|
-
|
3382
|
-
|
3383
|
-
}
|
3384
|
-
|
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
|
-
};
|
3785
|
+
const queries = await buildQueries$2(chaindataProvider, addressesByToken);
|
3786
|
+
const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe((error, result) => {
|
3787
|
+
if (error) return callback(error);
|
3788
|
+
const balances = result?.filter(b => b !== null) ?? [];
|
3789
|
+
if (balances.length > 0) callback(null, new Balances(balances));
|
3790
|
+
});
|
3791
|
+
return unsubscribe;
|
3406
3792
|
},
|
3407
3793
|
async fetchBalances(addressesByToken) {
|
3408
3794
|
assert(chainConnectors.substrate, "This module requires a substrate chain connector");
|
3409
|
-
const queries = await buildQueries$2(
|
3795
|
+
const queries = await buildQueries$2(chaindataProvider, addressesByToken);
|
3410
3796
|
const result = await new RpcStateQueryHelper(chainConnectors.substrate, queries).fetch();
|
3411
3797
|
const balances = result?.filter(b => b !== null) ?? [];
|
3412
3798
|
return new Balances(balances);
|
@@ -3418,10 +3804,11 @@ const SubForeignAssetsModule = hydrate => {
|
|
3418
3804
|
transferMethod,
|
3419
3805
|
metadataRpc
|
3420
3806
|
}) {
|
3421
|
-
const token = await chaindataProvider.
|
3807
|
+
const token = await chaindataProvider.tokenById(tokenId);
|
3422
3808
|
assert(token, `Token ${tokenId} not found in store`);
|
3423
|
-
|
3424
|
-
const
|
3809
|
+
if (token.type !== "substrate-foreignassets") throw new Error(`This module doesn't handle tokens of type ${token.type}`);
|
3810
|
+
const chainId = token.chain.id;
|
3811
|
+
const chain = await chaindataProvider.chainById(chainId);
|
3425
3812
|
assert(chain?.genesisHash, `Chain ${chainId} not found in store`);
|
3426
3813
|
const onChainId = (() => {
|
3427
3814
|
try {
|
@@ -3461,16 +3848,23 @@ const SubForeignAssetsModule = hydrate => {
|
|
3461
3848
|
}
|
3462
3849
|
};
|
3463
3850
|
};
|
3464
|
-
async function
|
3465
|
-
const
|
3466
|
-
const
|
3467
|
-
const
|
3468
|
-
|
3469
|
-
const
|
3470
|
-
|
3851
|
+
async function buildQueries$2(chaindataProvider, addressesByToken) {
|
3852
|
+
const allChains = await chaindataProvider.chainsById();
|
3853
|
+
const tokens = await chaindataProvider.tokensById();
|
3854
|
+
const miniMetadatas = new Map((await db.miniMetadatas.toArray()).map(miniMetadata => [miniMetadata.id, miniMetadata]));
|
3855
|
+
const uniqueChainIds = getUniqueChainIds(addressesByToken, tokens);
|
3856
|
+
const chains = Object.fromEntries(uniqueChainIds.map(chainId => [chainId, allChains[chainId]]));
|
3857
|
+
const chainStorageCoders = buildStorageCoders({
|
3858
|
+
chainIds: uniqueChainIds,
|
3859
|
+
chains,
|
3860
|
+
miniMetadatas,
|
3861
|
+
moduleType: "substrate-foreignassets",
|
3862
|
+
coders: {
|
3863
|
+
storage: ["ForeignAssets", "Account"]
|
3864
|
+
}
|
3471
3865
|
});
|
3472
3866
|
return Object.entries(addressesByToken).flatMap(([tokenId, addresses]) => {
|
3473
|
-
const token =
|
3867
|
+
const token = tokens[tokenId];
|
3474
3868
|
if (!token) {
|
3475
3869
|
log.warn(`Token ${tokenId} not found`);
|
3476
3870
|
return [];
|
@@ -3479,12 +3873,18 @@ async function buildNetworkQueries$1(networkId, chainConnector, chaindataProvide
|
|
3479
3873
|
log.debug(`This module doesn't handle tokens of type ${token.type}`);
|
3480
3874
|
return [];
|
3481
3875
|
}
|
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];
|
3482
3882
|
if (!chain) {
|
3483
|
-
log.warn(`Chain ${
|
3883
|
+
log.warn(`Chain ${chainId} for token ${tokenId} not found`);
|
3484
3884
|
return [];
|
3485
3885
|
}
|
3486
3886
|
return addresses.flatMap(address => {
|
3487
|
-
const scaleCoder =
|
3887
|
+
const scaleCoder = chainStorageCoders.get(chainId)?.storage;
|
3488
3888
|
const onChainId = (() => {
|
3489
3889
|
try {
|
3490
3890
|
return papiParse(token.onChainId);
|
@@ -3492,12 +3892,12 @@ async function buildNetworkQueries$1(networkId, chainConnector, chaindataProvide
|
|
3492
3892
|
return token.onChainId;
|
3493
3893
|
}
|
3494
3894
|
})();
|
3495
|
-
const stateKey = encodeStateKey(scaleCoder, `Invalid address / token onChainId in ${
|
3895
|
+
const stateKey = encodeStateKey(scaleCoder, `Invalid address / token onChainId in ${chainId} storage query ${address} / ${token.onChainId}`, onChainId, address);
|
3496
3896
|
if (!stateKey) return [];
|
3497
3897
|
const decodeResult = change => {
|
3498
3898
|
/** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
|
3499
3899
|
|
3500
|
-
const decoded = decodeScale(scaleCoder, change, `Failed to decode substrate-foreignassets balance on chain ${
|
3900
|
+
const decoded = decodeScale(scaleCoder, change, `Failed to decode substrate-foreignassets balance on chain ${chainId}`) ?? {
|
3501
3901
|
balance: 0n,
|
3502
3902
|
status: {
|
3503
3903
|
type: "Liquid"
|
@@ -3531,54 +3931,29 @@ async function buildNetworkQueries$1(networkId, chainConnector, chaindataProvide
|
|
3531
3931
|
source: "substrate-foreignassets",
|
3532
3932
|
status: "live",
|
3533
3933
|
address,
|
3534
|
-
|
3934
|
+
multiChainId: {
|
3935
|
+
subChainId: chainId
|
3936
|
+
},
|
3937
|
+
chainId,
|
3535
3938
|
tokenId: token.id,
|
3536
3939
|
values: balanceValues
|
3537
3940
|
};
|
3538
3941
|
};
|
3539
3942
|
return {
|
3540
|
-
chainId
|
3943
|
+
chainId,
|
3541
3944
|
stateKey,
|
3542
3945
|
decodeResult
|
3543
3946
|
};
|
3544
3947
|
});
|
3545
3948
|
});
|
3546
3949
|
}
|
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
|
-
};
|
3568
3950
|
|
3569
3951
|
async function subscribeBase(queries, chainConnector, callback) {
|
3570
|
-
|
3571
|
-
|
3572
|
-
|
3573
|
-
|
3574
|
-
|
3575
|
-
return unsubscribe;
|
3576
|
-
} catch (err) {
|
3577
|
-
if (!isAbortError(err)) log.error("Error subscribing to base queries", {
|
3578
|
-
err
|
3579
|
-
});
|
3580
|
-
return () => {};
|
3581
|
-
}
|
3952
|
+
const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe((error, result) => {
|
3953
|
+
if (error) callback(error);
|
3954
|
+
if (result && result.length > 0) callback(null, result);
|
3955
|
+
});
|
3956
|
+
return unsubscribe;
|
3582
3957
|
}
|
3583
3958
|
|
3584
3959
|
/**
|
@@ -3594,6 +3969,262 @@ const asObservable = handler => (...args) => new Observable(subscriber => {
|
|
3594
3969
|
return unsubscribe;
|
3595
3970
|
});
|
3596
3971
|
|
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
|
+
|
3597
4228
|
/**
|
3598
4229
|
* Each nominationPool in the nominationPools pallet has access to some accountIds which have no
|
3599
4230
|
* associated private key. Instead, they are derived from this function.
|
@@ -3613,293 +4244,280 @@ const nompoolAccountId = (palletId, poolId, index) => {
|
|
3613
4244
|
/** The stash account for the nomination pool */
|
3614
4245
|
const nompoolStashAccountId = (palletId, poolId) => nompoolAccountId(palletId, poolId, 0);
|
3615
4246
|
|
3616
|
-
|
3617
|
-
|
3618
|
-
|
3619
|
-
|
3620
|
-
|
3621
|
-
|
3622
|
-
|
3623
|
-
|
3624
|
-
const
|
3625
|
-
|
3626
|
-
|
3627
|
-
|
4247
|
+
async function subscribeNompoolStaking(chaindataProvider, chainConnector, addressesByToken, callback) {
|
4248
|
+
const allChains = await chaindataProvider.chainsById();
|
4249
|
+
const tokens = await chaindataProvider.tokensById();
|
4250
|
+
const miniMetadatas = new Map((await db.miniMetadatas.toArray()).map(miniMetadata => [miniMetadata.id, miniMetadata]));
|
4251
|
+
const nomPoolTokenIds = Object.entries(tokens).filter(([, token]) => {
|
4252
|
+
// ignore non-native tokens
|
4253
|
+
if (token.type !== "substrate-native") return false;
|
4254
|
+
// ignore tokens on chains with no nompools pallet
|
4255
|
+
const [chainMeta] = findChainMeta(miniMetadatas, "substrate-native", allChains[token.chain.id]);
|
4256
|
+
return typeof chainMeta?.nominationPoolsPalletId === "string";
|
4257
|
+
}).map(([tokenId]) => tokenId);
|
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"]
|
3628
4277
|
}
|
3629
|
-
|
3630
|
-
|
3631
|
-
|
3632
|
-
|
3633
|
-
|
3634
|
-
|
3635
|
-
|
3636
|
-
|
3637
|
-
|
3638
|
-
|
3639
|
-
|
3640
|
-
|
3641
|
-
|
3642
|
-
|
3643
|
-
|
3644
|
-
|
3645
|
-
|
3646
|
-
const
|
3647
|
-
|
3648
|
-
|
3649
|
-
|
3650
|
-
|
3651
|
-
|
3652
|
-
|
3653
|
-
|
3654
|
-
|
3655
|
-
|
3656
|
-
|
3657
|
-
|
3658
|
-
|
3659
|
-
|
3660
|
-
|
3661
|
-
|
3662
|
-
|
3663
|
-
|
3664
|
-
|
3665
|
-
|
3666
|
-
|
3667
|
-
|
3668
|
-
|
3669
|
-
|
3670
|
-
|
3671
|
-
|
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
|
-
});
|
3705
|
-
return {
|
3706
|
-
tokenId,
|
3707
|
-
address,
|
3708
|
-
poolId,
|
3709
|
-
points,
|
3710
|
-
unbondingEras
|
3711
|
-
};
|
3712
|
-
};
|
3713
|
-
return {
|
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?.();
|
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;
|
4299
|
+
}
|
4300
|
+
const [chainMeta] = findChainMeta(miniMetadatas, "substrate-native", chain);
|
4301
|
+
const {
|
4302
|
+
nominationPoolsPalletId
|
4303
|
+
} = chainMeta ?? {};
|
4304
|
+
const subscribePoolMembers = (addresses, callback) => {
|
4305
|
+
const scaleCoder = chainStorageCoders.get(chainId)?.poolMembers;
|
4306
|
+
const queries = addresses.flatMap(address => {
|
4307
|
+
const stateKey = encodeStateKey(scaleCoder, `Invalid address in ${chainId} poolMembers query ${address}`, address);
|
4308
|
+
if (!stateKey) return [];
|
4309
|
+
const decodeResult = change => {
|
4310
|
+
/** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
|
4311
|
+
|
4312
|
+
const decoded = decodeScale(scaleCoder, change, `Failed to decode poolMembers on chain ${chainId}`);
|
4313
|
+
const poolId = decoded?.pool_id?.toString?.();
|
4314
|
+
const points = decoded?.points?.toString?.();
|
4315
|
+
const unbondingEras = Array.from(decoded?.unbonding_eras ?? []).flatMap(entry => {
|
4316
|
+
if (entry === undefined) return [];
|
4317
|
+
const [key, value] = Array.from(entry);
|
4318
|
+
const era = key?.toString?.();
|
4319
|
+
const amount = value?.toString?.();
|
4320
|
+
if (typeof era !== "string" || typeof amount !== "string") return [];
|
3786
4321
|
return {
|
3787
|
-
|
3788
|
-
|
4322
|
+
era,
|
4323
|
+
amount
|
3789
4324
|
};
|
3790
|
-
};
|
4325
|
+
});
|
3791
4326
|
return {
|
3792
|
-
|
3793
|
-
|
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 {
|
3803
|
-
poolId,
|
3804
|
-
points,
|
3805
|
-
unbondingEras
|
3806
|
-
} = poolMembers;
|
3807
|
-
if (typeof poolId === "string" && typeof points === "string") state.set(poolMembers.address, {
|
4327
|
+
tokenId,
|
4328
|
+
address,
|
3808
4329
|
poolId,
|
3809
4330
|
points,
|
3810
4331
|
unbondingEras
|
3811
|
-
}
|
3812
|
-
}
|
3813
|
-
return
|
3814
|
-
|
3815
|
-
|
3816
|
-
|
3817
|
-
|
3818
|
-
|
3819
|
-
const
|
3820
|
-
|
3821
|
-
|
4332
|
+
};
|
4333
|
+
};
|
4334
|
+
return {
|
4335
|
+
chainId,
|
4336
|
+
stateKey,
|
4337
|
+
decodeResult
|
4338
|
+
};
|
4339
|
+
});
|
4340
|
+
const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
|
4341
|
+
return () => subscription.then(unsubscribe => unsubscribe());
|
4342
|
+
};
|
4343
|
+
const subscribePoolPoints = (poolIds, callback) => {
|
4344
|
+
if (poolIds.length === 0) callback(null, []);
|
4345
|
+
const scaleCoder = chainStorageCoders.get(chainId)?.bondedPools;
|
4346
|
+
const queries = poolIds.flatMap(poolId => {
|
4347
|
+
const stateKey = encodeStateKey(scaleCoder, `Invalid poolId in ${chainId} bondedPools query ${poolId}`, poolId);
|
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 {
|
3822
4355
|
poolId,
|
3823
4356
|
points
|
3824
|
-
}
|
3825
|
-
|
3826
|
-
|
3827
|
-
|
3828
|
-
|
3829
|
-
|
3830
|
-
|
3831
|
-
|
4357
|
+
};
|
4358
|
+
};
|
4359
|
+
return {
|
4360
|
+
chainId,
|
4361
|
+
stateKey,
|
4362
|
+
decodeResult
|
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 {
|
3832
4382
|
poolId,
|
3833
4383
|
activeStake
|
3834
|
-
}
|
3835
|
-
|
3836
|
-
|
3837
|
-
|
3838
|
-
|
3839
|
-
|
3840
|
-
|
3841
|
-
|
4384
|
+
};
|
4385
|
+
};
|
4386
|
+
return {
|
4387
|
+
chainId,
|
4388
|
+
stateKey,
|
4389
|
+
decodeResult
|
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 {
|
3842
4408
|
poolId,
|
3843
4409
|
metadata
|
3844
|
-
}
|
3845
|
-
|
3846
|
-
|
3847
|
-
|
3848
|
-
|
3849
|
-
|
3850
|
-
|
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",
|
3869
|
-
type: "nompool",
|
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)
|
4410
|
+
};
|
4411
|
+
};
|
4412
|
+
return {
|
4413
|
+
chainId,
|
4414
|
+
stateKey,
|
4415
|
+
decodeResult
|
4416
|
+
};
|
3893
4417
|
});
|
3894
|
-
|
3895
|
-
|
3896
|
-
|
3897
|
-
|
3898
|
-
|
3899
|
-
|
4418
|
+
const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
|
4419
|
+
return () => subscription.then(unsubscribe => unsubscribe());
|
4420
|
+
};
|
4421
|
+
const poolMembersByAddress$ = asObservable(subscribePoolMembers)(addresses).pipe(scan((state, next) => {
|
4422
|
+
for (const poolMembers of next) {
|
4423
|
+
const {
|
4424
|
+
poolId,
|
4425
|
+
points,
|
4426
|
+
unbondingEras
|
4427
|
+
} = poolMembers;
|
4428
|
+
if (typeof poolId === "string" && typeof points === "string") state.set(poolMembers.address, {
|
4429
|
+
poolId,
|
4430
|
+
points,
|
4431
|
+
unbondingEras
|
4432
|
+
});else state.set(poolMembers.address, null);
|
4433
|
+
}
|
4434
|
+
return state;
|
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: {
|
4497
|
+
type: "nompool",
|
4498
|
+
poolId: parsedPoolId,
|
4499
|
+
description: poolMetadata
|
4500
|
+
}
|
4501
|
+
}, {
|
4502
|
+
source: "nompools-staking",
|
4503
|
+
type: "nompool",
|
4504
|
+
label: "nompools-unbonding",
|
4505
|
+
amount: unbondingAmount.toString(),
|
4506
|
+
meta: {
|
4507
|
+
poolId: parsedPoolId,
|
4508
|
+
description: poolMetadata,
|
4509
|
+
unbonding: true
|
4510
|
+
}
|
4511
|
+
}]
|
4512
|
+
};
|
4513
|
+
}).filter(isNotNil);
|
4514
|
+
if (balances.length > 0) callback(null, balances);
|
4515
|
+
},
|
4516
|
+
error: error => callback(error)
|
3900
4517
|
});
|
3901
|
-
|
4518
|
+
resultUnsubscribes.push(() => subscription.unsubscribe());
|
3902
4519
|
}
|
4520
|
+
return () => resultUnsubscribes.forEach(unsub => unsub());
|
3903
4521
|
}
|
3904
4522
|
|
3905
4523
|
const SUBTENSOR_ROOT_NETUID = 0;
|
@@ -3941,236 +4559,225 @@ const calculateTaoFromDynamicInfo = ({
|
|
3941
4559
|
});
|
3942
4560
|
};
|
3943
4561
|
|
3944
|
-
|
3945
|
-
|
3946
|
-
|
3947
|
-
|
3948
|
-
|
3949
|
-
|
3950
|
-
|
3951
|
-
|
3952
|
-
const
|
3953
|
-
|
3954
|
-
|
3955
|
-
|
4562
|
+
async function subscribeSubtensorStaking(chaindataProvider, chainConnector, addressesByToken, callback) {
|
4563
|
+
const allChains = await chaindataProvider.chainsById();
|
4564
|
+
const tokens = await chaindataProvider.tokensById();
|
4565
|
+
const miniMetadatas = new Map((await db.miniMetadatas.toArray()).map(miniMetadata => [miniMetadata.id, miniMetadata]));
|
4566
|
+
const subtensorTokenIds = Object.entries(tokens).filter(([, token]) => {
|
4567
|
+
// ignore non-native tokens
|
4568
|
+
if (token.type !== "substrate-native") return false;
|
4569
|
+
// ignore tokens on chains with no subtensor pallet
|
4570
|
+
const [chainMeta] = findChainMeta(miniMetadatas, "substrate-native", allChains[token.chain.id]);
|
4571
|
+
return chainMeta?.hasSubtensorPallet === true;
|
4572
|
+
}).map(([tokenId]) => tokenId);
|
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;
|
3956
4597
|
}
|
3957
|
-
|
3958
|
-
|
3959
|
-
|
3960
|
-
|
3961
|
-
|
3962
|
-
|
3963
|
-
|
3964
|
-
|
3965
|
-
|
3966
|
-
|
3967
|
-
const
|
3968
|
-
|
3969
|
-
|
3970
|
-
|
3971
|
-
|
3972
|
-
|
3973
|
-
|
3974
|
-
|
3975
|
-
|
3976
|
-
|
3977
|
-
|
3978
|
-
|
3979
|
-
|
3980
|
-
|
3981
|
-
|
3982
|
-
|
3983
|
-
|
3984
|
-
|
3985
|
-
|
3986
|
-
|
3987
|
-
|
3988
|
-
|
3989
|
-
|
3990
|
-
|
3991
|
-
|
3992
|
-
|
3993
|
-
|
3994
|
-
|
3995
|
-
|
3996
|
-
|
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
|
-
}
|
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;
|
4607
|
+
}
|
4608
|
+
const scaleApi = getScaleApi({
|
4609
|
+
chainId,
|
4610
|
+
send: (...args) => chainConnector.send(chainId, ...args, {
|
4611
|
+
expectErrors: true
|
4612
|
+
} // don't pollute the wallet logs when this request fails
|
4613
|
+
)
|
4614
|
+
}, chainMeta.miniMetadata, token, chain.hasCheckMetadataHash, chain.signedExtensions, chain.registryTypes);
|
4615
|
+
|
4616
|
+
// sets the number of addresses to query in parallel (per chain, since each chain runs in parallel to the others)
|
4617
|
+
const concurrency = 4;
|
4618
|
+
// In-memory cache for successful dynamic info results
|
4619
|
+
const dynamicInfoCache = new Map();
|
4620
|
+
const fetchDynamicInfoForNetuids = async uniqueNetuids => {
|
4621
|
+
const MAX_RETRIES = 3;
|
4622
|
+
const RETRY_DELAY_MS = 500;
|
4623
|
+
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
4624
|
+
const fetchInfo = async netuid => {
|
4625
|
+
if (netuid === 0) return null;
|
4626
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
4627
|
+
try {
|
4628
|
+
const params = [netuid];
|
4629
|
+
const result = await scaleApi.getRuntimeCallValue("SubnetInfoRuntimeApi", "get_dynamic_info", params);
|
4630
|
+
dynamicInfoCache.set(netuid, result); // Cache successful response
|
4631
|
+
return result;
|
4632
|
+
} catch (error) {
|
4633
|
+
log.trace(`Attempt ${attempt} failed for netuid ${netuid}:`, error);
|
4634
|
+
if (attempt < MAX_RETRIES) {
|
4635
|
+
const backoffTime = RETRY_DELAY_MS * 2 ** (attempt - 1);
|
4636
|
+
log.trace(`Retrying in ${backoffTime}ms...`);
|
4637
|
+
await delay(backoffTime);
|
4027
4638
|
}
|
4028
4639
|
}
|
4029
|
-
|
4030
|
-
|
4031
|
-
|
4032
|
-
|
4033
|
-
|
4034
|
-
|
4035
|
-
return Promise.all(uniqueNetuids.map(fetchInfo));
|
4640
|
+
}
|
4641
|
+
if (dynamicInfoCache.has(netuid)) {
|
4642
|
+
return dynamicInfoCache.get(netuid); // Use cached value on failure
|
4643
|
+
}
|
4644
|
+
log.trace(`Failed to fetch dynamic info for netuid ${netuid} after ${MAX_RETRIES} attempts.`);
|
4645
|
+
return null;
|
4036
4646
|
};
|
4037
|
-
|
4038
|
-
|
4039
|
-
|
4040
|
-
|
4041
|
-
|
4042
|
-
|
4043
|
-
|
4044
|
-
|
4045
|
-
|
4046
|
-
|
4047
|
-
|
4048
|
-
|
4647
|
+
return Promise.all(uniqueNetuids.map(fetchInfo));
|
4648
|
+
};
|
4649
|
+
const subtensorQueries = from(addresses).pipe(
|
4650
|
+
// mergeMap lets us run N concurrent queries, where N is the value of `concurrency`
|
4651
|
+
mergeMap(async address => {
|
4652
|
+
const queryMethods = [async () => {
|
4653
|
+
if (chain.isTestnet) return [];
|
4654
|
+
const params = [address];
|
4655
|
+
const result = await scaleApi.getRuntimeCallValue("StakeInfoRuntimeApi", "get_stake_info_for_coldkey", params);
|
4656
|
+
if (!Array.isArray(result)) return [];
|
4657
|
+
const uniqueNetuids = Array.from(new Set(result.map(item => Number(item.netuid)).filter(netuid => netuid !== SUBTENSOR_ROOT_NETUID)));
|
4658
|
+
await fetchDynamicInfoForNetuids(uniqueNetuids);
|
4659
|
+
const stakes = result?.map(({
|
4660
|
+
coldkey,
|
4661
|
+
hotkey,
|
4662
|
+
netuid,
|
4663
|
+
stake
|
4664
|
+
}) => {
|
4665
|
+
return {
|
4666
|
+
address: coldkey,
|
4049
4667
|
hotkey,
|
4050
|
-
netuid,
|
4051
|
-
stake
|
4052
|
-
|
4053
|
-
|
4054
|
-
|
4055
|
-
|
4056
|
-
|
4057
|
-
|
4058
|
-
|
4059
|
-
|
4060
|
-
|
4061
|
-
|
4062
|
-
|
4063
|
-
return
|
4064
|
-
}
|
4065
|
-
|
4066
|
-
|
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
|
-
}
|
4668
|
+
netuid: Number(netuid),
|
4669
|
+
stake: BigInt(stake),
|
4670
|
+
dynamicInfo: dynamicInfoCache.get(Number(netuid))
|
4671
|
+
};
|
4672
|
+
}).filter(({
|
4673
|
+
stake
|
4674
|
+
}) => stake >= SUBTENSOR_MIN_STAKE_AMOUNT_PLANK);
|
4675
|
+
return stakes;
|
4676
|
+
}];
|
4677
|
+
const errors = [];
|
4678
|
+
for (const queryMethod of queryMethods) {
|
4679
|
+
try {
|
4680
|
+
// try each query method
|
4681
|
+
return await queryMethod();
|
4682
|
+
} catch (cause) {
|
4683
|
+
// if it fails, keep track of the error and try the next one
|
4684
|
+
errors.push(cause);
|
4074
4685
|
}
|
4686
|
+
}
|
4075
4687
|
|
4076
|
-
|
4077
|
-
|
4078
|
-
|
4079
|
-
|
4080
|
-
|
4081
|
-
|
4082
|
-
|
4083
|
-
|
4084
|
-
|
4085
|
-
|
4688
|
+
// if we get to here, that means that all query methods failed
|
4689
|
+
// let's throw the errors back to the native balance module
|
4690
|
+
throw new Error([`Failed to fetch ${tokenId} subtensor staked balance for ${address}:`, ...errors.map(error => String(error))].join("\n\t"));
|
4691
|
+
}, concurrency),
|
4692
|
+
// instead of emitting each balance as it's fetched, toArray waits for them all to fetch and then it collects them into an array
|
4693
|
+
toArray(),
|
4694
|
+
// this mergeMap flattens our Array<Array<Stakes>> into just an Array<Stakes>
|
4695
|
+
mergeMap(stakes => stakes),
|
4696
|
+
// convert our Array<Stakes> into Array<Balances>, which we can then return to the native balance module
|
4697
|
+
map(stakes => stakes.map(({
|
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",
|
4086
4732
|
address,
|
4087
|
-
|
4088
|
-
|
4089
|
-
|
4090
|
-
|
4091
|
-
|
4092
|
-
|
4093
|
-
|
4094
|
-
|
4095
|
-
|
4096
|
-
|
4097
|
-
|
4098
|
-
|
4099
|
-
|
4100
|
-
|
4101
|
-
|
4102
|
-
|
4103
|
-
|
4104
|
-
|
4105
|
-
|
4106
|
-
|
4107
|
-
|
4108
|
-
|
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
|
-
}
|
4733
|
+
multiChainId: {
|
4734
|
+
subChainId: chainId
|
4735
|
+
},
|
4736
|
+
chainId,
|
4737
|
+
tokenId,
|
4738
|
+
values: [{
|
4739
|
+
source: "subtensor-staking",
|
4740
|
+
type: "subtensor",
|
4741
|
+
label: "subtensor-staking",
|
4742
|
+
amount: stakeByNetuid.toString(),
|
4743
|
+
meta: {
|
4744
|
+
type: "subtensor-staking",
|
4745
|
+
hotkey,
|
4746
|
+
netuid,
|
4747
|
+
amountStaked: stake.toString(),
|
4748
|
+
alphaToTaoRate,
|
4749
|
+
dynamicInfo: {
|
4750
|
+
tokenSymbol,
|
4751
|
+
subnetName,
|
4752
|
+
subnetIdentity: {
|
4753
|
+
...subnetIdentity,
|
4754
|
+
subnetName: subnetIdentity?.subnet_name || subnetName
|
4141
4755
|
}
|
4142
4756
|
}
|
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
|
-
}));
|
4757
|
+
}
|
4758
|
+
}]
|
4759
|
+
};
|
4760
|
+
})));
|
4155
4761
|
|
4156
|
-
|
4157
|
-
|
4158
|
-
|
4159
|
-
|
4160
|
-
|
4762
|
+
// This observable will run the subtensorQueries on a 30s (30_000ms) interval.
|
4763
|
+
// However, if the last run has not yet completed (e.g. its been 30s but we're still fetching some balances),
|
4764
|
+
// then exhaustMap will wait until the next interval (so T: 60s, T: 90s, T: 120s, etc) before re-executing the subtensorQueries.
|
4765
|
+
const subtensorQueriesInterval = interval(30_000).pipe(startWith(0),
|
4766
|
+
// start immediately
|
4767
|
+
exhaustMap(() => {
|
4768
|
+
return subtensorQueries;
|
4769
|
+
}));
|
4161
4770
|
|
4162
|
-
|
4163
|
-
|
4164
|
-
|
4165
|
-
|
4166
|
-
}
|
4167
|
-
return () => abortController.abort();
|
4168
|
-
} catch (err) {
|
4169
|
-
if (!isAbortError(err)) log.error("Error subscribing to subtensor staking", {
|
4170
|
-
err
|
4771
|
+
// subscribe to the balances
|
4772
|
+
const subscription = subtensorQueriesInterval.subscribe({
|
4773
|
+
next: balances => callback(null, balances),
|
4774
|
+
error: error => callback(error)
|
4171
4775
|
});
|
4172
|
-
|
4776
|
+
|
4777
|
+
// use the abortController to tear the subscription down when we don't need it anymore
|
4778
|
+
abortController.signal.onabort = () => subscription.unsubscribe();
|
4173
4779
|
}
|
4780
|
+
return () => abortController.abort();
|
4174
4781
|
}
|
4175
4782
|
|
4176
4783
|
const getOtherType = input => `other-${input}`;
|
@@ -4226,14 +4833,18 @@ const filterBaseLocks = locks => {
|
|
4226
4833
|
};
|
4227
4834
|
|
4228
4835
|
// TODO: Make these titles translatable
|
4229
|
-
const getLockTitle = (lock,
|
4230
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
4231
|
-
{
|
4836
|
+
const getLockTitle = (lock, {
|
4232
4837
|
balance
|
4233
4838
|
} = {}) => {
|
4234
4839
|
if (!lock.label) return lock.label;
|
4235
4840
|
if (lock.label === "democracy") return "Governance";
|
4236
|
-
if (lock.label === "crowdloan")
|
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
|
+
}
|
4237
4848
|
if (lock.label === "nompools-staking") return "Pooled Staking";
|
4238
4849
|
if (lock.label === "nompools-unbonding") return "Pooled Staking";
|
4239
4850
|
if (lock.label === "subtensor-staking") return "Root Staking";
|
@@ -4245,6 +4856,7 @@ const getLockTitle = (lock,
|
|
4245
4856
|
};
|
4246
4857
|
|
4247
4858
|
const moduleType$2 = "substrate-native";
|
4859
|
+
const subNativeTokenId = chainId => `${chainId}-substrate-native`.toLowerCase().replace(/ /g, "-");
|
4248
4860
|
|
4249
4861
|
/**
|
4250
4862
|
* Function to merge two 'sub sources' of the same balance together, or
|
@@ -4322,7 +4934,7 @@ async function buildQueries$1(chains, tokens, chainStorageCoders, miniMetadatas,
|
|
4322
4934
|
log.debug(`This module doesn't handle tokens of type ${token.type}`);
|
4323
4935
|
return outerResult;
|
4324
4936
|
}
|
4325
|
-
const chainId = token.
|
4937
|
+
const chainId = token.chain?.id;
|
4326
4938
|
if (!chainId) {
|
4327
4939
|
log.warn(`Token ${tokenId} has no chain`);
|
4328
4940
|
return outerResult;
|
@@ -4332,10 +4944,10 @@ async function buildQueries$1(chains, tokens, chainStorageCoders, miniMetadatas,
|
|
4332
4944
|
log.warn(`Chain ${chainId} for token ${tokenId} not found`);
|
4333
4945
|
return outerResult;
|
4334
4946
|
}
|
4335
|
-
const
|
4947
|
+
const [chainMeta] = findChainMeta(miniMetadatas, "substrate-native", chain);
|
4336
4948
|
const {
|
4337
4949
|
useLegacyTransferableCalculation
|
4338
|
-
} =
|
4950
|
+
} = chainMeta ?? {};
|
4339
4951
|
addresses.flat().forEach(address => {
|
4340
4952
|
const queryKey = `${tokenId}-${address}`;
|
4341
4953
|
// We share this balanceJson between the base and the lock query for this address
|
@@ -4343,7 +4955,10 @@ async function buildQueries$1(chains, tokens, chainStorageCoders, miniMetadatas,
|
|
4343
4955
|
source: "substrate-native",
|
4344
4956
|
status: "live",
|
4345
4957
|
address,
|
4346
|
-
|
4958
|
+
multiChainId: {
|
4959
|
+
subChainId: chainId
|
4960
|
+
},
|
4961
|
+
chainId,
|
4347
4962
|
tokenId,
|
4348
4963
|
values: []
|
4349
4964
|
};
|
@@ -4615,19 +5230,75 @@ const updateStakingLocksUsingUnbondingLocks = values => {
|
|
4615
5230
|
return [...otherValues, ...stakingLocks];
|
4616
5231
|
};
|
4617
5232
|
|
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
|
+
}));
|
4618
5267
|
class QueryCache {
|
4619
|
-
#chaindataProvider;
|
4620
|
-
#chainConnector;
|
4621
|
-
miniMetadatas = new Map();
|
4622
5268
|
balanceQueryCache = new Map();
|
4623
|
-
|
5269
|
+
metadataSub = null;
|
5270
|
+
constructor(chaindataProvider) {
|
4624
5271
|
this.chaindataProvider = chaindataProvider;
|
4625
|
-
|
4626
|
-
|
5272
|
+
}
|
5273
|
+
ensureSetup() {
|
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();
|
4627
5297
|
}
|
4628
5298
|
async getQueries(addressesByToken) {
|
4629
|
-
|
4630
|
-
const
|
5299
|
+
this.ensureSetup();
|
5300
|
+
const chains = await this.chaindataProvider.chainsById();
|
5301
|
+
const tokens = await this.chaindataProvider.tokensById();
|
4631
5302
|
const queryResults = Object.entries(addressesByToken).reduce((result, [tokenId, addresses]) => {
|
4632
5303
|
addresses.forEach(address => {
|
4633
5304
|
const key = `${tokenId}-${address}`;
|
@@ -4643,19 +5314,15 @@ class QueryCache {
|
|
4643
5314
|
existing: [],
|
4644
5315
|
newAddressesByToken: {}
|
4645
5316
|
});
|
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
|
-
}
|
4652
5317
|
|
4653
5318
|
// build queries for token/address pairs which have not been queried before
|
4654
|
-
const
|
5319
|
+
const miniMetadatas = await firstValueFrom(commonMetadataObservable);
|
5320
|
+
const uniqueChainIds = getUniqueChainIds(queryResults.newAddressesByToken, tokens);
|
4655
5321
|
const chainStorageCoders = buildStorageCoders({
|
4656
5322
|
chainIds: uniqueChainIds,
|
4657
5323
|
chains,
|
4658
|
-
miniMetadatas
|
5324
|
+
miniMetadatas,
|
5325
|
+
moduleType: "substrate-native",
|
4659
5326
|
coders: {
|
4660
5327
|
base: ["System", "Account"],
|
4661
5328
|
stakingLedger: ["Staking", "Ledger"],
|
@@ -4665,7 +5332,7 @@ class QueryCache {
|
|
4665
5332
|
freezes: ["Balances", "Freezes"]
|
4666
5333
|
}
|
4667
5334
|
});
|
4668
|
-
const queries = await buildQueries$1(chains, tokens, chainStorageCoders,
|
5335
|
+
const queries = await buildQueries$1(chains, tokens, chainStorageCoders, miniMetadatas, queryResults.newAddressesByToken);
|
4669
5336
|
// now update the cache
|
4670
5337
|
Object.entries(queries).forEach(([key, query]) => {
|
4671
5338
|
this.balanceQueryCache.set(key, query);
|
@@ -4674,11 +5341,14 @@ class QueryCache {
|
|
4674
5341
|
}
|
4675
5342
|
}
|
4676
5343
|
|
4677
|
-
const
|
4678
|
-
const
|
5344
|
+
const RELAY_TOKENS = ["polkadot-substrate-native", "kusama-substrate-native"];
|
5345
|
+
const PUBLIC_GOODS_TOKENS = ["polkadot-asset-hub-substrate-native", "kusama-asset-hub-substrate-native"];
|
5346
|
+
const sortChains = (a, b) => {
|
4679
5347
|
// polkadot and kusama should be checked first
|
4680
|
-
if (
|
4681
|
-
if (
|
5348
|
+
if (RELAY_TOKENS.includes(a)) return -1;
|
5349
|
+
if (RELAY_TOKENS.includes(b)) return 1;
|
5350
|
+
if (PUBLIC_GOODS_TOKENS.includes(a)) return -1;
|
5351
|
+
if (PUBLIC_GOODS_TOKENS.includes(b)) return 1;
|
4682
5352
|
return 0;
|
4683
5353
|
};
|
4684
5354
|
|
@@ -4694,30 +5364,10 @@ class SubNativeBalanceError extends Error {
|
|
4694
5364
|
}
|
4695
5365
|
}
|
4696
5366
|
|
4697
|
-
const
|
4698
|
-
|
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
|
-
|
5367
|
+
const DEFAULT_SYMBOL = "Unit";
|
5368
|
+
const DEFAULT_DECIMALS = 0;
|
4714
5369
|
const POLLING_WINDOW_SIZE = 20;
|
4715
5370
|
const MAX_SUBSCRIPTION_SIZE = 40;
|
4716
|
-
const EMPTY_CHAIN_META = {
|
4717
|
-
miniMetadata: null,
|
4718
|
-
extra: null
|
4719
|
-
};
|
4720
|
-
const SubNativeTokenConfigSchema = TokenConfigBaseSchema;
|
4721
5371
|
const SubNativeModule = hydrate => {
|
4722
5372
|
const {
|
4723
5373
|
chainConnectors,
|
@@ -4725,198 +5375,33 @@ const SubNativeModule = hydrate => {
|
|
4725
5375
|
} = hydrate;
|
4726
5376
|
const chainConnector = chainConnectors.substrate;
|
4727
5377
|
assert(chainConnector, "This module requires a substrate chain connector");
|
4728
|
-
const queryCache = new QueryCache(chaindataProvider
|
5378
|
+
const queryCache = new QueryCache(chaindataProvider);
|
4729
5379
|
const getModuleTokens = async () => {
|
4730
|
-
return await chaindataProvider.
|
4731
|
-
};
|
4732
|
-
|
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
|
-
};
|
4827
|
-
|
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 ?? []);
|
5380
|
+
return await chaindataProvider.tokensByIdForType(moduleType$2);
|
4908
5381
|
};
|
4909
5382
|
return {
|
4910
5383
|
...DefaultBalanceModule(moduleType$2),
|
4911
|
-
async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) {
|
4912
|
-
|
4913
|
-
if (
|
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
|
+
|
5390
|
+
//
|
5391
|
+
// extract system_properties
|
5392
|
+
//
|
5393
|
+
|
5394
|
+
const {
|
5395
|
+
tokenSymbol,
|
5396
|
+
tokenDecimals
|
5397
|
+
} = systemProperties ?? {};
|
5398
|
+
const symbol = (Array.isArray(tokenSymbol) ? tokenSymbol[0] : tokenSymbol) ?? DEFAULT_SYMBOL;
|
5399
|
+
const decimals = (Array.isArray(tokenDecimals) ? tokenDecimals[0] : tokenDecimals) ?? DEFAULT_DECIMALS;
|
4914
5400
|
|
4915
5401
|
//
|
4916
5402
|
// process metadata into SCALE encoders/decoders
|
4917
5403
|
//
|
4918
5404
|
const metadataVersion = getMetadataVersion(metadataRpc);
|
4919
|
-
if (metadataVersion < 14) return EMPTY_CHAIN_META;
|
4920
5405
|
const metadata = decAnyMetadata(metadataRpc);
|
4921
5406
|
const unifiedMetadata = unifyMetadata(metadata);
|
4922
5407
|
|
@@ -4936,6 +5421,7 @@ const SubNativeModule = hydrate => {
|
|
4936
5421
|
};
|
4937
5422
|
const existentialDeposit = getConstantValue("Balances", "ExistentialDeposit")?.toString();
|
4938
5423
|
const nominationPoolsPalletId = getConstantValue("NominationPools", "PalletId")?.asText();
|
5424
|
+
const crowdloanPalletId = getConstantValue("Crowdloan", "PalletId")?.asText();
|
4939
5425
|
const hasSubtensorPallet = getConstantValue("SubtensorModule", "KeySwapCost") !== undefined;
|
4940
5426
|
|
4941
5427
|
//
|
@@ -4955,10 +5441,15 @@ const SubNativeModule = hydrate => {
|
|
4955
5441
|
}, {
|
4956
5442
|
pallet: "Staking",
|
4957
5443
|
items: ["Ledger"]
|
5444
|
+
}, {
|
5445
|
+
pallet: "Crowdloan",
|
5446
|
+
items: ["Funds"]
|
5447
|
+
}, {
|
5448
|
+
pallet: "Paras",
|
5449
|
+
items: ["Parachains"]
|
4958
5450
|
},
|
4959
5451
|
// TotalColdkeyStake is used until v.2.2.1, then it is replaced by StakingHotkeys+Stake
|
4960
5452
|
// 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
|
4962
5453
|
{
|
4963
5454
|
pallet: "SubtensorModule",
|
4964
5455
|
items: ["TotalColdkeyStake", "StakingHotkeys", "Stake"]
|
@@ -4977,40 +5468,47 @@ const SubNativeModule = hydrate => {
|
|
4977
5468
|
}) => name === "Freezes"));
|
4978
5469
|
const useLegacyTransferableCalculation = !hasFreezesItem;
|
4979
5470
|
const chainMeta = {
|
5471
|
+
isTestnet,
|
5472
|
+
useLegacyTransferableCalculation,
|
5473
|
+
symbol,
|
5474
|
+
decimals,
|
5475
|
+
existentialDeposit,
|
5476
|
+
nominationPoolsPalletId,
|
5477
|
+
crowdloanPalletId,
|
5478
|
+
hasSubtensorPallet,
|
4980
5479
|
miniMetadata,
|
4981
|
-
|
4982
|
-
useLegacyTransferableCalculation,
|
4983
|
-
existentialDeposit,
|
4984
|
-
nominationPoolsPalletId,
|
4985
|
-
hasSubtensorPallet
|
4986
|
-
}
|
5480
|
+
metadataVersion
|
4987
5481
|
};
|
4988
|
-
if (!useLegacyTransferableCalculation) delete chainMeta.
|
4989
|
-
if (!hasSubtensorPallet) delete chainMeta.
|
5482
|
+
if (!useLegacyTransferableCalculation) delete chainMeta.useLegacyTransferableCalculation;
|
5483
|
+
if (!hasSubtensorPallet) delete chainMeta.hasSubtensorPallet;
|
4990
5484
|
return chainMeta;
|
4991
5485
|
},
|
4992
5486
|
async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) {
|
4993
5487
|
if (moduleConfig?.disable === true) return {};
|
4994
5488
|
const {
|
4995
|
-
|
4996
|
-
|
4997
|
-
|
4998
|
-
const {
|
5489
|
+
isTestnet,
|
5490
|
+
symbol,
|
5491
|
+
decimals,
|
4999
5492
|
existentialDeposit
|
5000
|
-
} = chainMeta
|
5001
|
-
if (existentialDeposit === undefined) log.warn("Substrate native module: existentialDeposit is undefined for %s, using 0", chainId);
|
5493
|
+
} = chainMeta;
|
5002
5494
|
const id = subNativeTokenId(chainId);
|
5003
5495
|
const nativeToken = {
|
5004
5496
|
id,
|
5005
5497
|
type: "substrate-native",
|
5006
|
-
|
5007
|
-
isDefault: true,
|
5008
|
-
symbol: symbol,
|
5009
|
-
|
5010
|
-
|
5498
|
+
isTestnet,
|
5499
|
+
isDefault: moduleConfig?.isDefault ?? true,
|
5500
|
+
symbol: symbol ?? DEFAULT_SYMBOL,
|
5501
|
+
decimals: decimals ?? DEFAULT_DECIMALS,
|
5502
|
+
logo: moduleConfig?.logo || githubTokenLogoUrl(id),
|
5011
5503
|
existentialDeposit: existentialDeposit ?? "0",
|
5012
|
-
|
5504
|
+
chain: {
|
5505
|
+
id: chainId
|
5506
|
+
}
|
5013
5507
|
};
|
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;
|
5014
5512
|
return {
|
5015
5513
|
[nativeToken.id]: nativeToken
|
5016
5514
|
};
|
@@ -5020,36 +5518,169 @@ const SubNativeModule = hydrate => {
|
|
5020
5518
|
initialBalances
|
5021
5519
|
}, callback) {
|
5022
5520
|
assert(chainConnectors.substrate, "This module requires a substrate chain connector");
|
5023
|
-
|
5024
|
-
|
5025
|
-
const
|
5026
|
-
|
5027
|
-
|
5028
|
-
|
5029
|
-
|
5030
|
-
|
5521
|
+
|
5522
|
+
// full record of balances for this module
|
5523
|
+
const subNativeBalances = new BehaviorSubject(Object.fromEntries(initialBalances?.map(b => [getBalanceId(b), b]) ?? []));
|
5524
|
+
// tokens which have a known positive balance
|
5525
|
+
const positiveBalanceTokens = subNativeBalances.pipe(map(balances => Array.from(new Set(Object.values(balances).map(b => b.tokenId)))), share());
|
5526
|
+
|
5527
|
+
// tokens that will be subscribed to, simply a slice of the positive balance tokens of size MAX_SUBSCRIPTION_SIZE
|
5528
|
+
const subscriptionTokens = positiveBalanceTokens.pipe(map(tokens => tokens.sort(sortChains).slice(0, MAX_SUBSCRIPTION_SIZE)));
|
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`));
|
5031
5565
|
};
|
5032
|
-
|
5566
|
+
// we queue up our work to clean up our subscription when this promise rejects
|
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");
|
5033
5631
|
try {
|
5034
|
-
|
5035
|
-
|
5036
|
-
|
5037
|
-
|
5038
|
-
|
5039
|
-
|
5632
|
+
const balances = await this.fetchBalances(addressesByToken);
|
5633
|
+
handleUpdate(null, Object.values(balances.toJSON()));
|
5634
|
+
} catch (error) {
|
5635
|
+
if (error instanceof ChainConnectionError) {
|
5636
|
+
// coerce ChainConnection errors into SubNativeBalance errors
|
5637
|
+
const errorChainId = error.chainId;
|
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
|
+
}
|
5040
5646
|
}
|
5041
|
-
|
5042
|
-
|
5043
|
-
|
5044
|
-
|
5045
|
-
|
5647
|
+
};
|
5648
|
+
// do one poll to get things started
|
5649
|
+
const currentBalances = subNativeBalances.getValue();
|
5650
|
+
const currentTokens = new Set(Object.values(currentBalances).map(b => b.tokenId));
|
5651
|
+
const nonCurrentTokens = Object.keys(addressesByToken).filter(tokenId => !currentTokens.has(tokenId)).sort(sortChains);
|
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]
|
5046
5656
|
}));
|
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();
|
5047
5671
|
return () => {
|
5048
|
-
|
5049
|
-
|
5672
|
+
callerUnsubscribe();
|
5673
|
+
positiveSub.unsubscribe();
|
5674
|
+
pollingSub.unsubscribe();
|
5050
5675
|
};
|
5051
5676
|
},
|
5052
|
-
fetchBalances
|
5677
|
+
async fetchBalances(addressesByToken) {
|
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
|
+
},
|
5053
5684
|
async transferToken({
|
5054
5685
|
tokenId,
|
5055
5686
|
from,
|
@@ -5066,10 +5697,11 @@ const SubNativeModule = hydrate => {
|
|
5066
5697
|
transferMethod,
|
5067
5698
|
userExtensions
|
5068
5699
|
}) {
|
5069
|
-
const token = await chaindataProvider.
|
5700
|
+
const token = await chaindataProvider.tokenById(tokenId);
|
5070
5701
|
assert(token, `Token ${tokenId} not found in store`);
|
5071
|
-
|
5072
|
-
const
|
5702
|
+
if (token.type !== "substrate-native") throw new Error(`This module doesn't handle tokens of type ${token.type}`);
|
5703
|
+
const chainId = token.chain.id;
|
5704
|
+
const chain = await chaindataProvider.chainById(chainId);
|
5073
5705
|
assert(chain?.genesisHash, `Chain ${chainId} not found in store`);
|
5074
5706
|
const {
|
5075
5707
|
genesisHash
|
@@ -6263,10 +6895,7 @@ var psp22Abi = {
|
|
6263
6895
|
};
|
6264
6896
|
|
6265
6897
|
const moduleType$1 = "substrate-psp22";
|
6266
|
-
const
|
6267
|
-
contractAddress: SubPsp22TokenSchema.shape.contractAddress,
|
6268
|
-
...TokenConfigBaseSchema.shape
|
6269
|
-
});
|
6898
|
+
const subPsp22TokenId = (chainId, tokenSymbol) => `${chainId}-substrate-psp22-${tokenSymbol}`.toLowerCase().replace(/ /g, "-");
|
6270
6899
|
const SubPsp22Module = hydrate => {
|
6271
6900
|
const {
|
6272
6901
|
chainConnectors,
|
@@ -6276,15 +6905,16 @@ const SubPsp22Module = hydrate => {
|
|
6276
6905
|
assert(chainConnector, "This module requires a substrate chain connector");
|
6277
6906
|
return {
|
6278
6907
|
...DefaultBalanceModule(moduleType$1),
|
6279
|
-
async fetchSubstrateChainMeta(
|
6280
|
-
|
6908
|
+
async fetchSubstrateChainMeta(chainId) {
|
6909
|
+
const isTestnet = (await chaindataProvider.chainById(chainId))?.isTestnet || false;
|
6281
6910
|
return {
|
6282
|
-
|
6283
|
-
extra: null
|
6911
|
+
isTestnet
|
6284
6912
|
};
|
6285
6913
|
},
|
6286
|
-
async fetchSubstrateChainTokens(chainId,
|
6287
|
-
|
6914
|
+
async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) {
|
6915
|
+
const {
|
6916
|
+
isTestnet
|
6917
|
+
} = chainMeta;
|
6288
6918
|
const registry = new TypeRegistry();
|
6289
6919
|
const Psp22Abi = new Abi(psp22Abi);
|
6290
6920
|
|
@@ -6294,11 +6924,12 @@ const SubPsp22Module = hydrate => {
|
|
6294
6924
|
chainId,
|
6295
6925
|
registry
|
6296
6926
|
});
|
6297
|
-
const
|
6298
|
-
for (const tokenConfig of tokens ?? []) {
|
6927
|
+
const tokens = {};
|
6928
|
+
for (const tokenConfig of moduleConfig?.tokens ?? []) {
|
6299
6929
|
try {
|
6300
6930
|
let symbol = tokenConfig?.symbol ?? "Unit";
|
6301
6931
|
let decimals = tokenConfig?.decimals ?? 0;
|
6932
|
+
const existentialDeposit = tokenConfig?.ed ?? "0";
|
6302
6933
|
const contractAddress = tokenConfig?.contractAddress ?? undefined;
|
6303
6934
|
if (contractAddress === undefined) continue;
|
6304
6935
|
await (async () => {
|
@@ -6314,28 +6945,35 @@ const SubPsp22Module = hydrate => {
|
|
6314
6945
|
const decimalsData = decimalsResult.toJSON()?.result?.ok?.data;
|
6315
6946
|
decimals = typeof decimalsData === "string" && decimalsData.startsWith("0x") ? hexToNumber(decimalsData) : decimals;
|
6316
6947
|
})();
|
6317
|
-
const id = subPsp22TokenId(chainId,
|
6948
|
+
const id = subPsp22TokenId(chainId, symbol);
|
6318
6949
|
const token = {
|
6319
6950
|
id,
|
6320
6951
|
type: "substrate-psp22",
|
6321
|
-
|
6952
|
+
isTestnet,
|
6322
6953
|
isDefault: tokenConfig.isDefault ?? true,
|
6323
6954
|
symbol,
|
6324
6955
|
decimals,
|
6325
|
-
|
6326
|
-
|
6956
|
+
logo: tokenConfig?.logo || githubTokenLogoUrl(id),
|
6957
|
+
existentialDeposit,
|
6327
6958
|
contractAddress,
|
6328
|
-
|
6959
|
+
chain: {
|
6960
|
+
id: chainId
|
6961
|
+
}
|
6329
6962
|
};
|
6963
|
+
if (tokenConfig?.symbol) {
|
6964
|
+
token.symbol = tokenConfig?.symbol;
|
6965
|
+
token.id = subPsp22TokenId(chainId, token.symbol);
|
6966
|
+
}
|
6330
6967
|
if (tokenConfig?.coingeckoId) token.coingeckoId = tokenConfig?.coingeckoId;
|
6968
|
+
if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName;
|
6331
6969
|
if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf;
|
6332
|
-
|
6970
|
+
tokens[token.id] = token;
|
6333
6971
|
} catch (error) {
|
6334
6972
|
log.error(`Failed to build substrate-psp22 token ${tokenConfig.contractAddress} (${tokenConfig.symbol}) on ${chainId}`, error?.message ?? error);
|
6335
6973
|
continue;
|
6336
6974
|
}
|
6337
6975
|
}
|
6338
|
-
return
|
6976
|
+
return tokens;
|
6339
6977
|
},
|
6340
6978
|
// TODO: Don't create empty subscriptions
|
6341
6979
|
async subscribeBalances({
|
@@ -6345,7 +6983,7 @@ const SubPsp22Module = hydrate => {
|
|
6345
6983
|
const subscriptionInterval = 12_000; // 12_000ms == 12 seconds
|
6346
6984
|
const initDelay = 3_000; // 3000ms == 3 seconds
|
6347
6985
|
const cache = new Map();
|
6348
|
-
const tokens = await chaindataProvider.
|
6986
|
+
const tokens = await chaindataProvider.tokensById();
|
6349
6987
|
const poll = async () => {
|
6350
6988
|
if (!subscriptionActive) return;
|
6351
6989
|
try {
|
@@ -6372,7 +7010,7 @@ const SubPsp22Module = hydrate => {
|
|
6372
7010
|
},
|
6373
7011
|
async fetchBalances(addressesByToken) {
|
6374
7012
|
assert(chainConnectors.substrate, "This module requires a substrate chain connector");
|
6375
|
-
const tokens = await chaindataProvider.
|
7013
|
+
const tokens = await chaindataProvider.tokensById();
|
6376
7014
|
return fetchBalances(chainConnectors.substrate, tokens, addressesByToken);
|
6377
7015
|
},
|
6378
7016
|
async transferToken({
|
@@ -6390,11 +7028,11 @@ const SubPsp22Module = hydrate => {
|
|
6390
7028
|
tip,
|
6391
7029
|
userExtensions
|
6392
7030
|
}) {
|
6393
|
-
const token = await chaindataProvider.
|
7031
|
+
const token = await chaindataProvider.tokenById(tokenId);
|
6394
7032
|
assert(token, `Token ${tokenId} not found in store`);
|
6395
7033
|
if (token.type !== "substrate-psp22") throw new Error(`This module doesn't handle tokens of type ${token.type}`);
|
6396
|
-
const chainId = token.
|
6397
|
-
const chain = await chaindataProvider.
|
7034
|
+
const chainId = token.chain.id;
|
7035
|
+
const chain = await chaindataProvider.chainById(chainId);
|
6398
7036
|
assert(chain?.genesisHash, `Chain ${chainId} not found in store`);
|
6399
7037
|
const {
|
6400
7038
|
genesisHash
|
@@ -6470,7 +7108,7 @@ const fetchBalances = async (chainConnector, tokens, addressesByToken) => {
|
|
6470
7108
|
// TODO: Use `decodeOutput` from `./util/decodeOutput`
|
6471
7109
|
const contractCall = makeContractCaller({
|
6472
7110
|
chainConnector,
|
6473
|
-
chainId: token.
|
7111
|
+
chainId: token.chain.id,
|
6474
7112
|
registry
|
6475
7113
|
});
|
6476
7114
|
if (token.contractAddress === undefined) {
|
@@ -6487,7 +7125,10 @@ const fetchBalances = async (chainConnector, tokens, addressesByToken) => {
|
|
6487
7125
|
source: "substrate-psp22",
|
6488
7126
|
status: "live",
|
6489
7127
|
address,
|
6490
|
-
|
7128
|
+
multiChainId: {
|
7129
|
+
subChainId: token.chain.id
|
7130
|
+
},
|
7131
|
+
chainId: token.chain.id,
|
6491
7132
|
tokenId,
|
6492
7133
|
value: balance
|
6493
7134
|
};
|
@@ -6510,16 +7151,8 @@ const fetchBalances = async (chainConnector, tokens, addressesByToken) => {
|
|
6510
7151
|
};
|
6511
7152
|
|
6512
7153
|
const moduleType = "substrate-tokens";
|
6513
|
-
const SubTokensTokenConfigSchema = z.strictObject({
|
6514
|
-
onChainId: SubTokensTokenSchema.shape.onChainId,
|
6515
|
-
...TokenConfigBaseSchema.shape,
|
6516
|
-
existentialDeposit: SubTokensTokenSchema.shape.existentialDeposit.optional()
|
6517
|
-
});
|
6518
7154
|
const defaultPalletId = "Tokens";
|
6519
|
-
const
|
6520
|
-
miniMetadata: null,
|
6521
|
-
extra: {}
|
6522
|
-
};
|
7155
|
+
const subTokensTokenId = (chainId, onChainId) => `${chainId}-substrate-tokens-${compressToEncodedURIComponent(String(onChainId))}`;
|
6523
7156
|
const SubTokensModule = hydrate => {
|
6524
7157
|
const {
|
6525
7158
|
chainConnectors,
|
@@ -6530,7 +7163,14 @@ const SubTokensModule = hydrate => {
|
|
6530
7163
|
return {
|
6531
7164
|
...DefaultBalanceModule(moduleType),
|
6532
7165
|
async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) {
|
6533
|
-
|
7166
|
+
const isTestnet = (await chaindataProvider.chainById(chainId))?.isTestnet || false;
|
7167
|
+
if (metadataRpc === undefined) return {
|
7168
|
+
isTestnet
|
7169
|
+
};
|
7170
|
+
if ((moduleConfig?.tokens ?? []).length < 1) return {
|
7171
|
+
isTestnet
|
7172
|
+
};
|
7173
|
+
const metadataVersion = getMetadataVersion(metadataRpc);
|
6534
7174
|
const metadata = decAnyMetadata(metadataRpc);
|
6535
7175
|
const palletId = moduleConfig?.palletId ?? defaultPalletId;
|
6536
7176
|
compactMetadata(metadata, [{
|
@@ -6538,83 +7178,74 @@ const SubTokensModule = hydrate => {
|
|
6538
7178
|
items: ["Accounts"]
|
6539
7179
|
}]);
|
6540
7180
|
const miniMetadata = encodeMetadata(metadata);
|
6541
|
-
return {
|
7181
|
+
return palletId === defaultPalletId ? {
|
7182
|
+
isTestnet,
|
6542
7183
|
miniMetadata,
|
6543
|
-
|
6544
|
-
|
6545
|
-
|
7184
|
+
metadataVersion
|
7185
|
+
} : {
|
7186
|
+
isTestnet,
|
7187
|
+
palletId,
|
7188
|
+
miniMetadata,
|
7189
|
+
metadataVersion
|
6546
7190
|
};
|
6547
7191
|
},
|
6548
|
-
async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig
|
6549
|
-
const
|
6550
|
-
|
7192
|
+
async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) {
|
7193
|
+
const {
|
7194
|
+
isTestnet
|
7195
|
+
} = chainMeta;
|
7196
|
+
const tokens = {};
|
7197
|
+
for (const tokenConfig of moduleConfig?.tokens ?? []) {
|
6551
7198
|
try {
|
6552
|
-
// TODO fetch metadata from chain, like we do for assets
|
6553
7199
|
const symbol = tokenConfig?.symbol ?? "Unit";
|
6554
7200
|
const decimals = tokenConfig?.decimals ?? 0;
|
6555
|
-
const existentialDeposit = tokenConfig?.
|
7201
|
+
const existentialDeposit = tokenConfig?.ed ?? "0";
|
6556
7202
|
const onChainId = tokenConfig?.onChainId ?? undefined;
|
6557
7203
|
if (onChainId === undefined) continue;
|
6558
7204
|
const id = subTokensTokenId(chainId, onChainId);
|
6559
7205
|
const token = {
|
6560
7206
|
id,
|
6561
7207
|
type: "substrate-tokens",
|
6562
|
-
|
7208
|
+
isTestnet,
|
6563
7209
|
isDefault: tokenConfig.isDefault ?? true,
|
6564
7210
|
symbol,
|
6565
7211
|
decimals,
|
6566
|
-
|
6567
|
-
logo: tokenConfig?.logo,
|
7212
|
+
logo: tokenConfig?.logo || githubTokenLogoUrl(id),
|
6568
7213
|
existentialDeposit,
|
6569
7214
|
onChainId,
|
6570
|
-
|
7215
|
+
chain: {
|
7216
|
+
id: chainId
|
7217
|
+
}
|
6571
7218
|
};
|
7219
|
+
if (tokenConfig?.symbol) {
|
7220
|
+
token.symbol = tokenConfig?.symbol;
|
7221
|
+
token.id = subTokensTokenId(chainId, token.onChainId);
|
7222
|
+
}
|
6572
7223
|
if (tokenConfig?.coingeckoId) token.coingeckoId = tokenConfig?.coingeckoId;
|
7224
|
+
if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName;
|
6573
7225
|
if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf;
|
6574
|
-
|
7226
|
+
tokens[token.id] = token;
|
6575
7227
|
} catch (error) {
|
6576
7228
|
log.error(`Failed to build substrate-tokens token ${tokenConfig.onChainId} (${tokenConfig.symbol}) on ${chainId}`, error?.message ?? error);
|
6577
7229
|
continue;
|
6578
7230
|
}
|
6579
7231
|
}
|
6580
|
-
return
|
7232
|
+
return tokens;
|
6581
7233
|
},
|
6582
7234
|
// TODO: Don't create empty subscriptions
|
6583
7235
|
async subscribeBalances({
|
6584
7236
|
addressesByToken
|
6585
7237
|
}, callback) {
|
6586
|
-
const
|
6587
|
-
|
6588
|
-
if (
|
6589
|
-
|
6590
|
-
|
6591
|
-
}
|
6592
|
-
|
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
|
-
};
|
7238
|
+
const queries = await buildQueries(chaindataProvider, addressesByToken);
|
7239
|
+
const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe((error, result) => {
|
7240
|
+
if (error) return callback(error);
|
7241
|
+
const balances = result?.filter(b => b !== null) ?? [];
|
7242
|
+
if (balances.length > 0) callback(null, new Balances(balances));
|
7243
|
+
});
|
7244
|
+
return unsubscribe;
|
6614
7245
|
},
|
6615
7246
|
async fetchBalances(addressesByToken) {
|
6616
7247
|
assert(chainConnectors.substrate, "This module requires a substrate chain connector");
|
6617
|
-
const queries = await buildQueries(
|
7248
|
+
const queries = await buildQueries(chaindataProvider, addressesByToken);
|
6618
7249
|
const result = await new RpcStateQueryHelper(chainConnectors.substrate, queries).fetch();
|
6619
7250
|
const balances = result?.filter(b => b !== null) ?? [];
|
6620
7251
|
return new Balances(balances);
|
@@ -6626,13 +7257,15 @@ const SubTokensModule = hydrate => {
|
|
6626
7257
|
transferMethod,
|
6627
7258
|
metadataRpc
|
6628
7259
|
}) {
|
6629
|
-
const token = await chaindataProvider.
|
7260
|
+
const token = await chaindataProvider.tokenById(tokenId);
|
6630
7261
|
assert(token, `Token ${tokenId} not found in store`);
|
6631
|
-
|
6632
|
-
const
|
7262
|
+
if (token.type !== "substrate-tokens") throw new Error(`This module doesn't handle tokens of type ${token.type}`);
|
7263
|
+
const chainId = token.chain.id;
|
7264
|
+
const chain = await chaindataProvider.chainById(chainId);
|
6633
7265
|
assert(chain?.genesisHash, `Chain ${chainId} not found in store`);
|
6634
|
-
const
|
6635
|
-
const
|
7266
|
+
const miniMetadatas = new Map((await db.miniMetadatas.toArray()).map(miniMetadata => [miniMetadata.id, miniMetadata]));
|
7267
|
+
const [chainMeta] = findChainMeta(miniMetadatas, moduleType, chain);
|
7268
|
+
const tokensPallet = chainMeta?.palletId ?? defaultPalletId;
|
6636
7269
|
const onChainId = (() => {
|
6637
7270
|
try {
|
6638
7271
|
return papiParse(token.onChainId);
|
@@ -6731,15 +7364,23 @@ const SubTokensModule = hydrate => {
|
|
6731
7364
|
}
|
6732
7365
|
};
|
6733
7366
|
};
|
6734
|
-
async function
|
6735
|
-
const
|
6736
|
-
const
|
6737
|
-
const
|
6738
|
-
|
6739
|
-
|
6740
|
-
const
|
6741
|
-
const
|
6742
|
-
|
7367
|
+
async function buildQueries(chaindataProvider, addressesByToken) {
|
7368
|
+
const allChains = await chaindataProvider.chainsById();
|
7369
|
+
const tokens = await chaindataProvider.tokensById();
|
7370
|
+
const miniMetadatas = new Map((await db.miniMetadatas.toArray()).map(miniMetadata => [miniMetadata.id, miniMetadata]));
|
7371
|
+
const tokensPalletByChain = new Map(Object.values(allChains).map(chain => [chain.id, findChainMeta(miniMetadatas, moduleType, chain)[0]?.palletId]));
|
7372
|
+
const uniqueChainIds = getUniqueChainIds(addressesByToken, tokens);
|
7373
|
+
const chains = Object.fromEntries(uniqueChainIds.map(chainId => [chainId, allChains[chainId]]));
|
7374
|
+
const chainStorageCoders = buildStorageCoders({
|
7375
|
+
chainIds: uniqueChainIds,
|
7376
|
+
chains,
|
7377
|
+
miniMetadatas,
|
7378
|
+
moduleType: "substrate-tokens",
|
7379
|
+
coders: {
|
7380
|
+
storage: ({
|
7381
|
+
chainId
|
7382
|
+
}) => [tokensPalletByChain.get(chainId) ?? defaultPalletId, "Accounts"]
|
7383
|
+
}
|
6743
7384
|
});
|
6744
7385
|
return Object.entries(addressesByToken).flatMap(([tokenId, addresses]) => {
|
6745
7386
|
const token = tokens[tokenId];
|
@@ -6751,8 +7392,18 @@ async function buildNetworkQueries(networkId, chainConnector, chaindataProvider,
|
|
6751
7392
|
log.debug(`This module doesn't handle tokens of type ${token.type}`);
|
6752
7393
|
return [];
|
6753
7394
|
}
|
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
|
+
}
|
6754
7405
|
return addresses.flatMap(address => {
|
6755
|
-
const scaleCoder =
|
7406
|
+
const scaleCoder = chainStorageCoders.get(chainId)?.storage;
|
6756
7407
|
const onChainId = (() => {
|
6757
7408
|
try {
|
6758
7409
|
return papiParse(token.onChainId);
|
@@ -6760,12 +7411,12 @@ async function buildNetworkQueries(networkId, chainConnector, chaindataProvider,
|
|
6760
7411
|
return token.onChainId;
|
6761
7412
|
}
|
6762
7413
|
})();
|
6763
|
-
const stateKey = encodeStateKey(scaleCoder, `Invalid address / token onChainId in ${
|
7414
|
+
const stateKey = encodeStateKey(scaleCoder, `Invalid address / token onChainId in ${chainId} storage query ${address} / ${token.onChainId}`, address, onChainId);
|
6764
7415
|
if (!stateKey) return [];
|
6765
7416
|
const decodeResult = change => {
|
6766
7417
|
/** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
|
6767
7418
|
|
6768
|
-
const decoded = decodeScale(scaleCoder, change, `Failed to decode substrate-tokens balance on chain ${
|
7419
|
+
const decoded = decodeScale(scaleCoder, change, `Failed to decode substrate-tokens balance on chain ${chainId}`) ?? {
|
6769
7420
|
free: 0n,
|
6770
7421
|
reserved: 0n,
|
6771
7422
|
frozen: 0n
|
@@ -6790,31 +7441,49 @@ async function buildNetworkQueries(networkId, chainConnector, chaindataProvider,
|
|
6790
7441
|
source: "substrate-tokens",
|
6791
7442
|
status: "live",
|
6792
7443
|
address,
|
6793
|
-
|
7444
|
+
multiChainId: {
|
7445
|
+
subChainId: chainId
|
7446
|
+
},
|
7447
|
+
chainId,
|
6794
7448
|
tokenId: token.id,
|
6795
7449
|
values: balanceValues
|
6796
7450
|
};
|
6797
7451
|
};
|
6798
7452
|
return {
|
6799
|
-
chainId
|
7453
|
+
chainId,
|
6800
7454
|
stateKey,
|
6801
7455
|
decodeResult
|
6802
7456
|
};
|
6803
7457
|
});
|
6804
7458
|
});
|
6805
7459
|
}
|
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
|
-
}
|
6817
7460
|
|
6818
|
-
const defaultBalanceModules = [EvmErc20Module, EvmNativeModule, EvmUniswapV2Module, SubAssetsModule, SubForeignAssetsModule, SubNativeModule, SubPsp22Module, SubTokensModule];
|
7461
|
+
const defaultBalanceModules = [EvmErc20Module, EvmNativeModule, EvmUniswapV2Module, SubAssetsModule, SubEquilibriumModule, SubForeignAssetsModule, SubNativeModule, SubPsp22Module, SubTokensModule];
|
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
|
+
};
|
6819
7488
|
|
6820
|
-
export { Balance, BalanceFormatter, BalanceValueGetter, Balances, Change24hCurrencyFormatter, DefaultBalanceModule, EvmErc20Module,
|
7489
|
+
export { Balance, BalanceFormatter, BalanceValueGetter, Balances, Change24hCurrencyFormatter, DefaultBalanceModule, EvmErc20Module, EvmNativeModule, EvmTokenFetcher, EvmUniswapV2Module, FiatSumBalancesFormatter, MiniMetadataUpdater, ONE_ALPHA_TOKEN, PlanckSumBalancesFormatter, RpcStateQueryHelper, SCALE_FACTOR, SUBTENSOR_MIN_STAKE_AMOUNT_PLANK, SUBTENSOR_ROOT_NETUID, SubAssetsModule, SubEquilibriumModule, SubForeignAssetsModule, SubNativeModule, SubPsp22Module, SubTokensModule, SumBalancesFormatter, TalismanBalancesDatabase, abiMulticall, balances, buildStorageCoders, calculateAlphaPrice, calculateTaoAmountFromAlpha, calculateTaoFromDynamicInfo, compress, configureStore, db, decodeOutput, decompress, defaultBalanceModules, deriveMiniMetadataId, detectTransferMethod, erc20Abi, erc20BalancesAggregatorAbi, evmErc20TokenId, evmNativeTokenId, evmUniswapV2TokenId, excludeFromFeePayableLocks, excludeFromTransferableAmount, filterBaseLocks, filterMirrorTokens, findChainMeta, getBalanceId, getLockTitle, getUniqueChainIds, getValueId, hydrateChaindataAndMiniMetadata, includeInTotalExtraAmount, makeContractCaller, subAssetTokenId, subEquilibriumTokenId, subForeignAssetTokenId, subNativeTokenId, subPsp22TokenId, subTokensTokenId, uniswapV2PairAbi, updateCustomMiniMetadata, updateEvmTokens };
|