@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.
Files changed (46) hide show
  1. package/dist/declarations/src/BalanceModule.d.ts +12 -23
  2. package/dist/declarations/src/EvmTokenFetcher.d.ts +11 -0
  3. package/dist/declarations/src/MiniMetadataUpdater.d.ts +43 -0
  4. package/dist/declarations/src/index.d.ts +3 -0
  5. package/dist/declarations/src/modules/EvmErc20Module.d.ts +23 -20
  6. package/dist/declarations/src/modules/EvmNativeModule.d.ts +18 -19
  7. package/dist/declarations/src/modules/EvmUniswapV2Module.d.ts +30 -28
  8. package/dist/declarations/src/modules/SubstrateAssetsModule.d.ts +18 -20
  9. package/dist/declarations/src/modules/SubstrateEquilibriumModule.d.ts +39 -0
  10. package/dist/declarations/src/modules/SubstrateForeignAssetsModule.d.ts +18 -20
  11. package/dist/declarations/src/modules/SubstrateNativeModule/index.d.ts +4 -17
  12. package/dist/declarations/src/modules/SubstrateNativeModule/subscribeCrowdloans.d.ts +5 -0
  13. package/dist/declarations/src/modules/SubstrateNativeModule/subscribeNompoolStaking.d.ts +3 -3
  14. package/dist/declarations/src/modules/SubstrateNativeModule/subscribeSubtensorStaking.d.ts +3 -3
  15. package/dist/declarations/src/modules/SubstrateNativeModule/types.d.ts +23 -6
  16. package/dist/declarations/src/modules/SubstrateNativeModule/util/QueryCache.d.ts +6 -6
  17. package/dist/declarations/src/modules/SubstrateNativeModule/util/buildQueries.d.ts +4 -5
  18. package/dist/declarations/src/modules/SubstrateNativeModule/util/crowdloanFundContributionsChildKey.d.ts +4 -0
  19. package/dist/declarations/src/modules/SubstrateNativeModule/util/detectMiniMetadataChanges.d.ts +2 -0
  20. package/dist/declarations/src/modules/SubstrateNativeModule/util/sortChains.d.ts +1 -1
  21. package/dist/declarations/src/modules/SubstratePsp22Module.d.ts +19 -20
  22. package/dist/declarations/src/modules/SubstrateTokensModule.d.ts +22 -22
  23. package/dist/declarations/src/modules/index.d.ts +38 -213
  24. package/dist/declarations/src/modules/util/InferBalanceModuleTypes.d.ts +3 -5
  25. package/dist/declarations/src/modules/util/buildStorageCoders.d.ts +7 -16
  26. package/dist/declarations/src/modules/util/findChainMeta.d.ts +8 -0
  27. package/dist/declarations/src/modules/util/getUniqueChainIds.d.ts +2 -2
  28. package/dist/declarations/src/modules/util/index.d.ts +1 -0
  29. package/dist/declarations/src/types/balances.d.ts +72 -203
  30. package/dist/declarations/src/types/balancetypes.d.ts +18 -8
  31. package/dist/declarations/src/types/minimetadatas.d.ts +24 -6
  32. package/dist/declarations/src/util/hydrateChaindata.d.ts +8 -0
  33. package/dist/declarations/src/util/index.d.ts +1 -0
  34. package/dist/talismn-balances.cjs.dev.js +2099 -1424
  35. package/dist/talismn-balances.cjs.prod.js +2099 -1424
  36. package/dist/talismn-balances.esm.js +2083 -1414
  37. package/package.json +8 -9
  38. package/dist/declarations/src/getMiniMetadata/getMetadataRpc.d.ts +0 -3
  39. package/dist/declarations/src/getMiniMetadata/getMiniMetadatas.d.ts +0 -4
  40. package/dist/declarations/src/getMiniMetadata/getSpecVersion.d.ts +0 -6
  41. package/dist/declarations/src/getMiniMetadata/getUpdatedMiniMetadatas.d.ts +0 -4
  42. package/dist/declarations/src/getMiniMetadata/index.d.ts +0 -5
  43. package/dist/declarations/src/libVersion.d.ts +0 -1
  44. package/dist/declarations/src/modules/SubstrateNativeModule/util/systemProperties.d.ts +0 -5
  45. package/dist/declarations/src/modules/util/getAddresssesByTokenByNetwork.d.ts +0 -3
  46. package/dist/declarations/src/types/tokens.d.ts +0 -11
@@ -1,30 +1,29 @@
1
- import { Dexie } from 'dexie';
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, isArrayOf, isTruthy, isEthereumAddress, hasOwnProperty, isAbortError, isNotNil, decodeAnyAddress, blake2Concat, Deferred } from '@talismn/util';
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, papiParse, getMetadataVersion, encodeStateKey } from '@talismn/scale';
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 { Observable, scan, share, map, switchAll, combineLatest, from, mergeMap, toArray, interval, startWith, exhaustMap, BehaviorSubject, debounceTime, takeUntil, distinctUntilChanged, switchMap, withLatestFrom, concatMap } from 'rxjs';
25
- import { u32, Struct, u128 } from 'scale-ts';
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
- var pkg = {
63
- name: "@talismn/balances",
64
- version: "0.0.0-pr2043-20250703063959"};
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(pkg.name);
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
- return [source, address, tokenId].join("::");
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 networkId() {
345
- return this.#storage.networkId;
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 network() {
348
- return this.#db?.networks?.[this.networkId] || null;
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.networkId, this.token.tokenAddress0);
369
- const tokenId1 = evmErc20TokenId(this.networkId, this.token.tokenAddress1);
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.subtensor.map(({
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
- libVersion
852
- }) => u8aToHex(xxhashAsU8a(new TextEncoder().encode(`${source}${chainId}${specVersion}${libVersion}`), 64), undefined, false);
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 TokenConfigBaseSchema = TokenBaseSchema.partial().omit({
948
- id: true
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$7 = "evm-erc20";
1182
- const EvmErc20TokenConfigSchema = z.strictObject({
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.getTokensMapById(moduleType$7);
1482
+ return await chaindataProvider.tokensByIdForType(moduleType$8);
1210
1483
  };
1211
1484
  const getErc20Aggregators = async () => {
1212
- const evmNetworks = await chaindataProvider.getNetworks("ethereum");
1213
- return Object.fromEntries(evmNetworks.filter(n => n.contracts?.Erc20Aggregator).map(n => [n.id, n.contracts.Erc20Aggregator]));
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$7),
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(_chainId) {
1494
+ async fetchEvmChainMeta(chainId) {
1495
+ const isTestnet = (await chaindataProvider.evmNetworkById(chainId))?.isTestnet || false;
1222
1496
  return {
1223
- miniMetadata: null,
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, _chainMeta, moduleConfig, tokens) {
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
- platform: "ethereum",
1527
+ isTestnet,
1253
1528
  isDefault: tokenConfig.isDefault ?? true,
1254
1529
  symbol,
1255
1530
  decimals,
1256
- name: contractName ?? symbol,
1257
- logo: tokenConfig?.logo,
1531
+ logo: tokenConfig?.logo || githubTokenLogoUrl(id),
1258
1532
  contractAddress,
1259
- networkId: chainId
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
- const validation = EvmErc20TokenSchema.safeParse(token);
1266
- if (validation.success) {
1267
- chainTokens[token.id] = token;
1268
- } else {
1269
- log.warn("Ignoring invalid token", token.id, validation.error.message, validation.error.issues);
1270
- }
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.networkId));
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.getNetworksMapById("ethereum");
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.networkId;
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 ([networkId, networkParams]) => {
1420
- const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(networkId);
1421
- if (!publicClient) throw new EvmErc20NetworkError(`Could not get rpc provider for evm network ${networkId}`, networkId);
1422
- const balances = await getEvmTokenBalances(publicClient, networkParams, result.errors, erc20Aggregators[networkId]);
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
- networkId,
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.networkId;
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$6 = "evm-native";
1514
- const EvmNativeTokenConfigSchema = TokenConfigBaseSchema;
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.getTokensMapById(moduleType$6);
1801
+ return await chaindataProvider.tokensByIdForType(moduleType$7);
1522
1802
  };
1523
1803
  return {
1524
- ...DefaultBalanceModule(moduleType$6),
1804
+ ...DefaultBalanceModule(moduleType$7),
1525
1805
  get tokens() {
1526
- return chaindataProvider.getTokensMapById(moduleType$6);
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(_chainId) {
1812
+ async fetchEvmChainMeta(chainId) {
1813
+ const isTestnet = (await chaindataProvider.evmNetworkById(chainId))?.isTestnet || false;
1533
1814
  return {
1534
- miniMetadata: null,
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
- // token is currently generated in chaindata from the EthNetworkConfig["nativeCurrency"] field
1544
- return {};
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 = fromPairs(toPairs(addressesByToken).map(([tokenId, addresses]) => {
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.networkId;
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(networkIdFromTokenId);
1873
+ const activeEvmNetworkIds = Object.keys(ethAddressesByToken).map(getEvmNetworkIdFromTokenId);
1570
1874
  const initialisingBalances = new Set(activeEvmNetworkIds);
1571
- const positiveBalanceNetworks = new Set(initialBalances?.map(b => b.networkId));
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 = networkIdFromTokenId(tokenId);
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.networkId) {
1599
- initialisingBalances.delete(balance.networkId);
1902
+ if (balance.evmNetworkId) {
1903
+ initialisingBalances.delete(balance.evmNetworkId);
1600
1904
  if (BigInt(balance.value) > 0n) {
1601
- positiveBalanceNetworks.add(balance.networkId);
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 networkId = token.networkId;
1652
- if (!networkId) throw new Error(`Token ${token.id} has no evm network`);
1653
- const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(networkId);
1654
- if (!publicClient) throw new Error(`Could not get rpc provider for evm network ${networkId}`);
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 ", networkId);
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
- networkId,
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.id} for ${ethAddresses.length} addresses: ${errorMessage}`);
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$5 = "evm-uniswapv2";
2286
- const EvmUniswapV2TokenConfigSchema = z.strictObject({
2287
- contractAddress: EvmUniswapV2TokenSchema.shape.contractAddress,
2288
- ...TokenConfigBaseSchema.shape,
2289
- // on chaindata side these are fetched by a dedicated task
2290
- symbol0: EvmUniswapV2TokenSchema.shape.symbol0.optional(),
2291
- symbol1: EvmUniswapV2TokenSchema.shape.symbol1.optional(),
2292
- decimals0: EvmUniswapV2TokenSchema.shape.decimals0.optional(),
2293
- decimals1: EvmUniswapV2TokenSchema.shape.decimals1.optional(),
2294
- tokenAddress0: EvmUniswapV2TokenSchema.shape.tokenAddress0.optional(),
2295
- tokenAddress1: EvmUniswapV2TokenSchema.shape.tokenAddress1.optional(),
2296
- coingeckoId0: EvmUniswapV2TokenSchema.shape.coingeckoId0.optional(),
2297
- coingeckoId1: EvmUniswapV2TokenSchema.shape.coingeckoId1.optional()
2298
- });
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$5),
2308
- async fetchEvmChainMeta(_chainId) {
2602
+ ...DefaultBalanceModule(moduleType$6),
2603
+ async fetchEvmChainMeta(chainId) {
2604
+ const isTestnet = (await chaindataProvider.evmNetworkById(chainId))?.isTestnet || false;
2309
2605
  return {
2310
- miniMetadata: null,
2311
- extra: null
2606
+ isTestnet
2312
2607
  };
2313
2608
  },
2314
- async fetchEvmChainTokens(chainId, _chainMeta, moduleConfig, pools) {
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
- platform: "ethereum",
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 || getGithubTokenLogoUrl("uniswap"),
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
- networkId: chainId
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.networkId) return result;
2373
- if (!result[b.networkId]) result[b.networkId] = {};
2374
- result[b.networkId][getBalanceId(b)] = 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.getNetworksMapById("ethereum");
2383
- const tokens = await chaindataProvider.getTokensMapById();
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.getNetworksMapById("ethereum");
2427
- const tokens = await chaindataProvider.getTokensMapById();
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 ([networkId, addressesByToken]) => {
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[networkId];
2437
- if (!evmNetwork) throw new Error(`Evm network ${networkId} not found`);
2438
- const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(networkId);
2439
- if (!publicClient) throw new Error(`Could not get rpc provider for evm network ${networkId}`);
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
- networkId,
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.networkId;
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
- // TODO remove this one in favor of the network specific one below
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 = miniMetadatas.get(chainId); // findMiniMetadata<TBalanceModule>(miniMetadatas, moduleType, chain)
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]?.networkId).flatMap(chainId => chainId ? [chainId] : []))];
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$4 = "substrate-assets";
2996
- const SubAssetsTokenConfigSchema = z.strictObject({
2997
- assetId: SubAssetsTokenSchema.shape.assetId,
2998
- ...TokenConfigBaseSchema.shape
2999
- });
3000
- const UNSUPPORTED_CHAIN_META$2 = {
3001
- miniMetadata: null,
3002
- extra: null
3003
- };
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$4),
3013
- // TODO make synchronous at the module definition level ?
3149
+ ...DefaultBalanceModule(moduleType$5),
3014
3150
  async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) {
3015
- if (!metadataRpc) return UNSUPPORTED_CHAIN_META$2;
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
- extra: null
3168
+ metadataVersion
3025
3169
  };
3026
3170
  },
3027
- async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig, tokens) {
3028
- if (!tokens?.length) return {};
3171
+ async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) {
3172
+ if ((moduleConfig?.tokens ?? []).length < 1) return {};
3029
3173
  const {
3030
- miniMetadata
3174
+ isTestnet,
3175
+ miniMetadata,
3176
+ metadataVersion
3031
3177
  } = chainMeta;
3032
- if (!miniMetadata) return {};
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 tokenList = {};
3038
- for (const tokenConfig of tokens ?? []) {
3184
+ const tokens = {};
3185
+ for (const tokenConfig of moduleConfig?.tokens ?? []) {
3039
3186
  try {
3040
- const assetId = String(tokenConfig.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
- platform: "polkadot",
3200
+ isTestnet,
3055
3201
  isDefault: tokenConfig?.isDefault ?? true,
3056
- symbol: tokenConfig?.symbol ?? 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
- networkId: chainId
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
- tokenList[token.id] = token;
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 tokenList;
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 byNetwork = keys(addressesByToken).reduce((acc, tokenId) => {
3084
- const networkId = parseSubAssetTokenId(tokenId).networkId;
3085
- if (!acc[networkId]) acc[networkId] = {};
3086
- acc[networkId][tokenId] = addressesByToken[tokenId];
3087
- return acc;
3088
- }, {});
3089
- const controller = new AbortController();
3090
- const pUnsubs = Promise.all(toPairs(byNetwork).map(async ([networkId, addressesByToken]) => {
3091
- try {
3092
- const queries = await buildNetworkQueries$2(networkId, chainConnector, chaindataProvider, addressesByToken, controller.signal);
3093
- if (controller.signal.aborted) return () => {};
3094
- const stateHelper = new RpcStateQueryHelper(chainConnector, queries);
3095
- return await stateHelper.subscribe((error, result) => {
3096
- if (error) return callback(error);
3097
- const balances = result?.filter(b => b !== null) ?? [];
3098
- if (balances.length > 0) callback(null, new Balances(balances));
3099
- });
3100
- } catch (err) {
3101
- if (!isAbortError(err)) log.error(`Failed to subscribe balances for network ${networkId}`, err);
3102
- return () => {};
3103
- }
3104
- }));
3105
- return () => {
3106
- pUnsubs.then(unsubs => {
3107
- unsubs.forEach(unsubscribe => unsubscribe());
3108
- });
3109
- controller.abort();
3110
- };
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$3(chainConnector, chaindataProvider, addressesByToken);
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.getTokenById(tokenId, "substrate-assets");
3262
+ const token = await chaindataProvider.tokenById(tokenId);
3136
3263
  assert(token, `Token ${tokenId} not found in store`);
3137
- const chainId = token.networkId;
3138
- const chain = await chaindataProvider.getNetworkById(chainId, "polkadot");
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 buildNetworkQueries$2(networkId, chainConnector, chaindataProvider, addressesByToken, signal) {
3184
- const miniMetadata = await getMiniMetadata(chaindataProvider, chainConnector, networkId, moduleType$4, signal);
3185
- const chain = await chaindataProvider.getNetworkById(networkId, "polkadot");
3186
- const tokensById = await chaindataProvider.getTokensMapById();
3187
- signal?.throwIfAborted();
3188
- const networkStorageCoders = buildNetworkStorageCoders(networkId, miniMetadata, {
3189
- storage: ["Assets", "Account"]
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 = tokensById[tokenId];
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 ${networkId} for token ${tokenId} not found`);
3343
+ log.warn(`Chain ${chainId} for token ${tokenId} not found`);
3204
3344
  return [];
3205
3345
  }
3206
3346
  return addresses.flatMap(address => {
3207
- const scaleCoder = networkStorageCoders?.storage;
3208
- const stateKey = tryEncode(scaleCoder, Number(token.assetId), address) ?? tryEncode(scaleCoder, BigInt(token.assetId), address);
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 ${networkId} storage query ${token.assetId} / ${address}`);
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 ${networkId}`) ?? {
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
- networkId,
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: networkId,
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$3 = "substrate-foreignassets";
3287
- const UNSUPPORTED_CHAIN_META$1 = {
3288
- miniMetadata: null,
3289
- extra: null
3290
- };
3291
- const SubForeignAssetsTokenConfigSchema = z.strictObject({
3292
- onChainId: SubForeignAssetsTokenSchema.shape.onChainId,
3293
- ...TokenConfigBaseSchema.shape
3294
- });
3295
- const SubForeignAssetsModule = hydrate => {
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$3),
3428
+ ...DefaultBalanceModule(moduleType$4),
3304
3429
  async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) {
3305
- if (metadataRpc === undefined) return UNSUPPORTED_CHAIN_META$1;
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: "ForeignAssets",
3311
- items: ["Account", "Asset", "Metadata"]
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
- extra: null
3451
+ metadataVersion
3317
3452
  };
3318
3453
  },
3319
- async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig, tokens) {
3320
- if (!tokens?.length) return {};
3454
+ async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) {
3455
+ // default to disabled
3456
+ if (moduleConfig?.disable !== false) return {};
3321
3457
  const {
3322
- miniMetadata
3458
+ isTestnet,
3459
+ miniMetadata,
3460
+ metadataVersion
3323
3461
  } = chainMeta;
3324
- if (!miniMetadata) return {};
3325
- const metadata = decAnyMetadata(miniMetadata);
3326
- const unifiedMetadata = unifyMetadata(metadata);
3327
- const scaleBuilder = getDynamicBuilder(getLookupFn(unifiedMetadata));
3328
- const assetCoder = scaleBuilder.buildStorage("ForeignAssets", "Asset");
3329
- const metadataCoder = scaleBuilder.buildStorage("ForeignAssets", "Metadata");
3330
- const tokenList = {};
3331
- for (const tokenConfig of tokens ?? []) {
3332
- try {
3333
- const onChainId = (() => {
3334
- try {
3335
- return papiParse(tokenConfig.onChainId);
3336
- } catch (error) {
3337
- return tokenConfig.onChainId;
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 (onChainId === undefined) continue;
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, tokenConfig.onChainId);
3750
+ const id = subForeignAssetTokenId(chainId, symbol);
3350
3751
  const token = {
3351
3752
  id,
3352
3753
  type: "substrate-foreignassets",
3353
- platform: "polkadot",
3754
+ isTestnet,
3354
3755
  isDefault: tokenConfig?.isDefault ?? true,
3355
3756
  symbol,
3356
3757
  decimals,
3357
- name: tokenConfig?.name ?? name,
3358
- logo: tokenConfig?.logo,
3758
+ logo: tokenConfig?.logo || githubTokenLogoUrl(id),
3359
3759
  existentialDeposit,
3360
3760
  onChainId: tokenConfig.onChainId,
3361
3761
  isFrozen,
3362
- networkId: chainId
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
- tokenList[token.id] = token;
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 tokenList;
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 byNetwork = keys(addressesByToken).reduce((acc, tokenId) => {
3379
- const networkId = parseSubForeignAssetTokenId(tokenId).networkId;
3380
- if (!acc[networkId]) acc[networkId] = {};
3381
- acc[networkId][tokenId] = addressesByToken[tokenId];
3382
- return acc;
3383
- }, {});
3384
- const controller = new AbortController();
3385
- const pUnsubs = Promise.all(toPairs(byNetwork).map(async ([networkId, addressesByToken]) => {
3386
- try {
3387
- const queries = await buildNetworkQueries$1(networkId, chainConnector, chaindataProvider, addressesByToken, controller.signal);
3388
- if (controller.signal.aborted) return () => {};
3389
- const stateHelper = new RpcStateQueryHelper(chainConnector, queries);
3390
- return await stateHelper.subscribe((error, result) => {
3391
- if (error) return callback(error);
3392
- const balances = result?.filter(b => b !== null) ?? [];
3393
- if (balances.length > 0) callback(null, new Balances(balances));
3394
- });
3395
- } catch (err) {
3396
- if (!isAbortError(err)) log.error(`Failed to subscribe ${moduleType$3} balances for network ${networkId}`, err);
3397
- return () => {};
3398
- }
3399
- }));
3400
- return () => {
3401
- pUnsubs.then(unsubs => {
3402
- unsubs.forEach(unsubscribe => unsubscribe());
3403
- });
3404
- controller.abort();
3405
- };
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(chainConnector, chaindataProvider, addressesByToken);
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.getTokenById(tokenId, "substrate-foreignassets");
3807
+ const token = await chaindataProvider.tokenById(tokenId);
3422
3808
  assert(token, `Token ${tokenId} not found in store`);
3423
- const chainId = token.networkId;
3424
- const chain = await chaindataProvider.getNetworkById(chainId, "polkadot");
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 buildNetworkQueries$1(networkId, chainConnector, chaindataProvider, addressesByToken, signal) {
3465
- const miniMetadata = await getMiniMetadata(chaindataProvider, chainConnector, networkId, moduleType$3, signal);
3466
- const chain = await chaindataProvider.getNetworkById(networkId, "polkadot");
3467
- const tokensById = await chaindataProvider.getTokensMapById();
3468
- signal?.throwIfAborted();
3469
- const networkStorageCoders = buildNetworkStorageCoders(networkId, miniMetadata, {
3470
- storage: ["ForeignAssets", "Account"]
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 = tokensById[tokenId];
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 ${networkId} for token ${tokenId} not found`);
3883
+ log.warn(`Chain ${chainId} for token ${tokenId} not found`);
3484
3884
  return [];
3485
3885
  }
3486
3886
  return addresses.flatMap(address => {
3487
- const scaleCoder = networkStorageCoders?.storage;
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 ${networkId} storage query ${address} / ${token.onChainId}`, onChainId, address);
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 ${networkId}`) ?? {
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
- networkId,
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: networkId,
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
- try {
3571
- const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe((error, result) => {
3572
- if (error) callback(error);
3573
- if (result && result.length > 0) callback(null, result);
3574
- });
3575
- return unsubscribe;
3576
- } catch (err) {
3577
- if (!isAbortError(err)) log.error("Error subscribing to base queries", {
3578
- err
3579
- });
3580
- return () => {};
3581
- }
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
- // TODO make this method chain-specific
3617
- async function subscribeNompoolStaking(chaindataProvider, chainConnector, addressesByToken, callback, signal) {
3618
- try {
3619
- const allChains = await chaindataProvider.getNetworksMapById("polkadot");
3620
- const tokens = await chaindataProvider.getTokensMapById();
3621
-
3622
- // there should be only one network here when subscribing to balances, we've split it up by network at the top level
3623
- const networkIds = keys(addressesByToken).map(tokenId => parseTokenId(tokenId).networkId);
3624
- const miniMetadatas = new Map();
3625
- for (const networkId of networkIds) {
3626
- const miniMetadata = await getMiniMetadata(chaindataProvider, chainConnector, networkId, "substrate-native");
3627
- miniMetadatas.set(networkId, miniMetadata);
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
- signal?.throwIfAborted();
3630
- const nomPoolTokenIds = Object.entries(tokens).filter(([, token]) => {
3631
- // ignore non-native tokens
3632
- if (token.type !== "substrate-native") return false;
3633
-
3634
- // ignore tokens on chains with no nompools pallet
3635
- const miniMetadata = miniMetadatas.get(token.networkId);
3636
- return typeof miniMetadata?.extra?.nominationPoolsPalletId === "string";
3637
- }).map(([tokenId]) => tokenId);
3638
-
3639
- // staking can only be done by the native token on chains with the staking pallet
3640
- const addressesByNomPoolToken = Object.fromEntries(Object.entries(addressesByToken)
3641
- // remove ethereum addresses
3642
- .map(([tokenId, addresses]) => [tokenId, addresses.filter(address => !isEthereumAddress(address))])
3643
- // remove tokens which aren't nom pool tokens
3644
- .filter(([tokenId]) => nomPoolTokenIds.includes(tokenId)));
3645
- const uniqueChainIds = getUniqueChainIds(addressesByNomPoolToken, tokens);
3646
- const chains = Object.fromEntries(Object.entries(allChains).filter(([chainId]) => uniqueChainIds.includes(chainId)));
3647
- const chainStorageCoders = buildStorageCoders({
3648
- chainIds: uniqueChainIds,
3649
- chains,
3650
- miniMetadatas,
3651
- coders: {
3652
- poolMembers: ["NominationPools", "PoolMembers"],
3653
- bondedPools: ["NominationPools", "BondedPools"],
3654
- ledger: ["Staking", "Ledger"],
3655
- metadata: ["NominationPools", "Metadata"]
3656
- }
3657
- });
3658
- const resultUnsubscribes = [];
3659
- for (const [tokenId, addresses] of Object.entries(addressesByNomPoolToken)) {
3660
- const token = tokens[tokenId];
3661
- if (!token) {
3662
- log.warn(`Token ${tokenId} not found`);
3663
- continue;
3664
- }
3665
- if (token.type !== "substrate-native") {
3666
- log.debug(`This module doesn't handle tokens of type ${token.type}`);
3667
- continue;
3668
- }
3669
- const chainId = token.networkId;
3670
- if (!chainId) {
3671
- log.warn(`Token ${tokenId} has no chain`);
3672
- continue;
3673
- }
3674
- const chain = chains[chainId];
3675
- if (!chain) {
3676
- log.warn(`Chain ${chainId} for token ${tokenId} not found`);
3677
- continue;
3678
- }
3679
- const miniMetadata = miniMetadatas.get(chainId);
3680
- const {
3681
- nominationPoolsPalletId
3682
- } = miniMetadata?.extra ?? {};
3683
- const subscribePoolMembers = (addresses, callback) => {
3684
- const scaleCoder = chainStorageCoders.get(chainId)?.poolMembers;
3685
- const queries = addresses.flatMap(address => {
3686
- const stateKey = encodeStateKey(scaleCoder, `Invalid address in ${chainId} poolMembers query ${address}`, address);
3687
- if (!stateKey) return [];
3688
- const decodeResult = change => {
3689
- /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
3690
-
3691
- const decoded = decodeScale(scaleCoder, change, `Failed to decode poolMembers on chain ${chainId}`);
3692
- const poolId = decoded?.pool_id?.toString?.();
3693
- const points = decoded?.points?.toString?.();
3694
- const unbondingEras = Array.from(decoded?.unbonding_eras ?? []).flatMap(entry => {
3695
- if (entry === undefined) return [];
3696
- const [key, value] = Array.from(entry);
3697
- const era = key?.toString?.();
3698
- const amount = value?.toString?.();
3699
- if (typeof era !== "string" || typeof amount !== "string") return [];
3700
- return {
3701
- era,
3702
- amount
3703
- };
3704
- });
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
- poolId,
3788
- metadata
4322
+ era,
4323
+ amount
3789
4324
  };
3790
- };
4325
+ });
3791
4326
  return {
3792
- chainId,
3793
- stateKey,
3794
- decodeResult
3795
- };
3796
- });
3797
- const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
3798
- return () => subscription.then(unsubscribe => unsubscribe());
3799
- };
3800
- const poolMembersByAddress$ = asObservable(subscribePoolMembers)(addresses).pipe(scan((state, next) => {
3801
- for (const poolMembers of next) {
3802
- const {
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
- });else state.set(poolMembers.address, null);
3812
- }
3813
- return state;
3814
- }, new Map()), share());
3815
- const poolIdByAddress$ = poolMembersByAddress$.pipe(map(pm => new Map(Array.from(pm).map(([address, pm]) => [address, pm?.poolId ?? null]))));
3816
- const pointsByAddress$ = poolMembersByAddress$.pipe(map(pm => new Map(Array.from(pm).map(([address, pm]) => [address, pm?.points ?? null]))));
3817
- const unbondingErasByAddress$ = poolMembersByAddress$.pipe(map(pm => new Map(Array.from(pm).map(([address, pm]) => [address, pm?.unbondingEras ?? null]))));
3818
- const poolIds$ = poolIdByAddress$.pipe(map(byAddress => [...new Set(Array.from(byAddress.values()).flatMap(poolId => poolId ?? []))]));
3819
- const pointsByPool$ = poolIds$.pipe(map(poolIds => asObservable(subscribePoolPoints)(poolIds)), switchAll(), scan((state, next) => {
3820
- for (const poolPoints of next) {
3821
- const {
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
- } = poolPoints;
3825
- if (typeof points === "string") state.set(poolId, points);else state.delete(poolId);
3826
- }
3827
- return state;
3828
- }, new Map()));
3829
- const stakeByPool$ = poolIds$.pipe(map(poolIds => asObservable(subscribePoolStake)(poolIds)), switchAll(), scan((state, next) => {
3830
- for (const poolStake of next) {
3831
- const {
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
- } = poolStake;
3835
- if (typeof activeStake === "string") state.set(poolId, activeStake);else state.delete(poolId);
3836
- }
3837
- return state;
3838
- }, new Map()));
3839
- const metadataByPool$ = poolIds$.pipe(map(poolIds => asObservable(subscribePoolMetadata)(poolIds)), switchAll(), scan((state, next) => {
3840
- for (const poolMetadata of next) {
3841
- const {
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
- } = poolMetadata;
3845
- if (typeof metadata === "string") state.set(poolId, metadata);else state.delete(poolId);
3846
- }
3847
- return state;
3848
- }, new Map()));
3849
- const subscription = combineLatest([poolIdByAddress$, pointsByAddress$, unbondingErasByAddress$, pointsByPool$, stakeByPool$, metadataByPool$]).subscribe({
3850
- next: ([poolIdByAddress, pointsByAddress, unbondingErasByAddress, pointsByPool, stakeByPool, metadataByPool]) => {
3851
- const balances = Array.from(poolIdByAddress).map(([address, poolId]) => {
3852
- const parsedPoolId = poolId === null ? undefined : parseInt(poolId);
3853
- const points = pointsByAddress.get(address) ?? "0";
3854
- const poolPoints = pointsByPool.get(poolId ?? "") ?? "0";
3855
- const poolStake = stakeByPool.get(poolId ?? "") ?? "0";
3856
- const poolMetadata = poolId ? metadataByPool.get(poolId) ?? `Pool ${poolId}` : undefined;
3857
- const amount = points === "0" || poolPoints === "0" || poolStake === "0" ? 0n : BigInt(poolStake) * BigInt(points) / BigInt(poolPoints);
3858
- const unbondingAmount = (unbondingErasByAddress.get(address) ?? []).reduce((total, {
3859
- amount
3860
- }) => total + BigInt(amount ?? "0"), 0n);
3861
- return {
3862
- source: "substrate-native",
3863
- status: "live",
3864
- address,
3865
- networkId: chainId,
3866
- tokenId,
3867
- values: [{
3868
- source: "nompools-staking",
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
- resultUnsubscribes.push(() => subscription.unsubscribe());
3895
- }
3896
- return () => resultUnsubscribes.forEach(unsub => unsub());
3897
- } catch (err) {
3898
- if (!isAbortError(err)) log.error("Error subscribing to nom pool staking", {
3899
- err
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
- return () => {};
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
- // TODO make this method chain-specific
3945
- async function subscribeSubtensorStaking(chaindataProvider, chainConnector, addressesByToken, callback, signal) {
3946
- try {
3947
- const allChains = await chaindataProvider.getNetworksMapById("polkadot");
3948
- const tokens = await chaindataProvider.getTokensMapById();
3949
-
3950
- // there should be only one network here when subscribing to balances, we've split it up by network at the top level
3951
- const networkIds = keys(addressesByToken).map(tokenId => parseTokenId(tokenId).networkId);
3952
- const miniMetadatas = new Map();
3953
- for (const networkId of networkIds) {
3954
- const miniMetadata = await getMiniMetadata(chaindataProvider, chainConnector, networkId, "substrate-native", signal);
3955
- miniMetadatas.set(networkId, miniMetadata);
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
- signal?.throwIfAborted();
3958
- const subtensorTokenIds = Object.entries(tokens).filter(([, token]) => {
3959
- // ignore non-native tokens
3960
- if (token.type !== "substrate-native") return false;
3961
- // ignore tokens on chains with no subtensor pallet
3962
- const miniMetadata = miniMetadatas.get(token.networkId);
3963
- return miniMetadata?.extra?.hasSubtensorPallet === true;
3964
- }).map(([tokenId]) => tokenId);
3965
-
3966
- // staking can only be done by the native token on chains with the subtensor pallet
3967
- const addressesBySubtensorToken = Object.fromEntries(Object.entries(addressesByToken)
3968
- // remove ethereum addresses
3969
- .map(([tokenId, addresses]) => [tokenId, addresses.filter(address => !isEthereumAddress(address))])
3970
- // remove tokens which aren't subtensor staking tokens
3971
- .filter(([tokenId]) => subtensorTokenIds.includes(tokenId)));
3972
- const uniqueChainIds = getUniqueChainIds(addressesBySubtensorToken, tokens);
3973
- const chains = Object.fromEntries(Object.entries(allChains).filter(([chainId]) => uniqueChainIds.includes(chainId)));
3974
- const abortController = new AbortController();
3975
- for (const [tokenId, addresses] of Object.entries(addressesBySubtensorToken)) {
3976
- const token = tokens[tokenId];
3977
- if (!token) {
3978
- log.warn(`Token ${tokenId} not found`);
3979
- continue;
3980
- }
3981
- if (token.type !== "substrate-native") {
3982
- log.debug(`This module doesn't handle tokens of type ${token.type}`);
3983
- continue;
3984
- }
3985
- const chainId = token.networkId;
3986
- const chain = chains[chainId];
3987
- if (!chain) {
3988
- log.warn(`Chain ${chainId} for token ${tokenId} not found`);
3989
- continue;
3990
- }
3991
- const miniMetadata = miniMetadatas.get(token.networkId);
3992
- if (!miniMetadata?.data) {
3993
- log.warn(`MiniMetadata for chain ${chainId} not found`);
3994
- continue;
3995
- }
3996
- const scaleApi = getScaleApi({
3997
- chainId,
3998
- send: (...args) => chainConnector.send(chainId, ...args, {
3999
- expectErrors: true
4000
- } // don't pollute the wallet logs when this request fails
4001
- )
4002
- }, miniMetadata.data, token, chain.hasCheckMetadataHash, chain.signedExtensions, chain.registryTypes);
4003
-
4004
- // sets the number of addresses to query in parallel (per chain, since each chain runs in parallel to the others)
4005
- const concurrency = 4;
4006
- // In-memory cache for successful dynamic info results
4007
- const dynamicInfoCache = new Map();
4008
- const fetchDynamicInfoForNetuids = async uniqueNetuids => {
4009
- const MAX_RETRIES = 3;
4010
- const RETRY_DELAY_MS = 500;
4011
- const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
4012
- const fetchInfo = async netuid => {
4013
- if (netuid === 0) return null;
4014
- for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
4015
- try {
4016
- const params = [netuid];
4017
- const result = await scaleApi.getRuntimeCallValue("SubnetInfoRuntimeApi", "get_dynamic_info", params);
4018
- dynamicInfoCache.set(netuid, result); // Cache successful response
4019
- return result;
4020
- } catch (error) {
4021
- log.trace(`Attempt ${attempt} failed for netuid ${netuid}:`, error);
4022
- if (attempt < MAX_RETRIES) {
4023
- const backoffTime = RETRY_DELAY_MS * 2 ** (attempt - 1);
4024
- log.trace(`Retrying in ${backoffTime}ms...`);
4025
- await delay(backoffTime);
4026
- }
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
- if (dynamicInfoCache.has(netuid)) {
4030
- return dynamicInfoCache.get(netuid); // Use cached value on failure
4031
- }
4032
- log.trace(`Failed to fetch dynamic info for netuid ${netuid} after ${MAX_RETRIES} attempts.`);
4033
- return null;
4034
- };
4035
- return Promise.all(uniqueNetuids.map(fetchInfo));
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
- const subtensorQueries = from(addresses).pipe(
4038
- // mergeMap lets us run N concurrent queries, where N is the value of `concurrency`
4039
- mergeMap(async address => {
4040
- const queryMethods = [async () => {
4041
- if (chain.isTestnet) return [];
4042
- const params = [address];
4043
- const result = await scaleApi.getRuntimeCallValue("StakeInfoRuntimeApi", "get_stake_info_for_coldkey", params);
4044
- if (!Array.isArray(result)) return [];
4045
- const uniqueNetuids = Array.from(new Set(result.map(item => Number(item.netuid)).filter(netuid => netuid !== SUBTENSOR_ROOT_NETUID)));
4046
- await fetchDynamicInfoForNetuids(uniqueNetuids);
4047
- const stakes = result?.map(({
4048
- coldkey,
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
- return {
4054
- address: coldkey,
4055
- hotkey,
4056
- netuid: Number(netuid),
4057
- stake: BigInt(stake),
4058
- dynamicInfo: dynamicInfoCache.get(Number(netuid))
4059
- };
4060
- }).filter(({
4061
- stake
4062
- }) => stake >= SUBTENSOR_MIN_STAKE_AMOUNT_PLANK);
4063
- return stakes;
4064
- }];
4065
- const errors = [];
4066
- for (const queryMethod of queryMethods) {
4067
- try {
4068
- // try each query method
4069
- return await queryMethod();
4070
- } catch (cause) {
4071
- // if it fails, keep track of the error and try the next one
4072
- errors.push(cause);
4073
- }
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
- // if we get to here, that means that all query methods failed
4077
- // let's throw the errors back to the native balance module
4078
- throw new Error([`Failed to fetch ${tokenId} subtensor staked balance for ${address}:`, ...errors.map(error => String(error))].join("\n\t"));
4079
- }, concurrency),
4080
- // instead of emitting each balance as it's fetched, toArray waits for them all to fetch and then it collects them into an array
4081
- toArray(),
4082
- // this mergeMap flattens our Array<Array<Stakes>> into just an Array<Stakes>
4083
- mergeMap(stakes => stakes),
4084
- // convert our Array<Stakes> into Array<Balances>, which we can then return to the native balance module
4085
- map(stakes => stakes.map(({
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
- hotkey,
4088
- stake,
4089
- netuid,
4090
- dynamicInfo
4091
- }) => {
4092
- const {
4093
- token_symbol,
4094
- subnet_name,
4095
- subnet_identity
4096
- } = dynamicInfo ?? {};
4097
- const tokenSymbol = new TextDecoder().decode(Uint8Array.from(token_symbol ?? []));
4098
- const subnetName = new TextDecoder().decode(Uint8Array.from(subnet_name ?? []));
4099
-
4100
- /** Map from Record<string, Binary> to Record<string, string> */
4101
- const binaryToText = input => Object.entries(input).reduce((acc, [key, value]) => {
4102
- acc[key] = value.asText();
4103
- return acc;
4104
- }, {});
4105
- const subnetIdentity = subnet_identity ? binaryToText(subnet_identity) : undefined;
4106
-
4107
- // Add 1n balance if failed to fetch dynamic info, so the position is not ignored by Balance lib and is displayed in the UI.
4108
- const alphaStakedInTao = dynamicInfo ? calculateTaoFromDynamicInfo({
4109
- dynamicInfo,
4110
- alphaStaked: stake
4111
- }) : 1n;
4112
- const alphaToTaoRate = calculateTaoFromDynamicInfo({
4113
- dynamicInfo: dynamicInfo ?? null,
4114
- alphaStaked: ONE_ALPHA_TOKEN
4115
- }).toString();
4116
- const stakeByNetuid = Number(netuid) === SUBTENSOR_ROOT_NETUID ? stake : alphaStakedInTao;
4117
- return {
4118
- source: "substrate-native",
4119
- status: "live",
4120
- address,
4121
- networkId: chainId,
4122
- tokenId,
4123
- values: [{
4124
- source: "subtensor-staking",
4125
- type: "subtensor",
4126
- label: "subtensor-staking",
4127
- amount: stakeByNetuid.toString(),
4128
- meta: {
4129
- type: "subtensor-staking",
4130
- hotkey,
4131
- netuid,
4132
- amountStaked: stake.toString(),
4133
- alphaToTaoRate,
4134
- dynamicInfo: {
4135
- tokenSymbol,
4136
- subnetName,
4137
- subnetIdentity: {
4138
- ...subnetIdentity,
4139
- subnetName: subnetIdentity?.subnet_name || subnetName
4140
- }
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
- // subscribe to the balances
4157
- const subscription = subtensorQueriesInterval.subscribe({
4158
- next: balances => callback(null, balances),
4159
- error: error => callback(error)
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
- // use the abortController to tear the subscription down when we don't need it anymore
4163
- abortController.signal.addEventListener("abort", () => {
4164
- subscription.unsubscribe();
4165
- });
4166
- }
4167
- return () => abortController.abort();
4168
- } catch (err) {
4169
- if (!isAbortError(err)) log.error("Error subscribing to subtensor staking", {
4170
- err
4771
+ // subscribe to the balances
4772
+ const subscription = subtensorQueriesInterval.subscribe({
4773
+ next: balances => callback(null, balances),
4774
+ error: error => callback(error)
4171
4775
  });
4172
- return () => {};
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") return "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.networkId;
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 miniMetadata = miniMetadatas.get(chainId);
4947
+ const [chainMeta] = findChainMeta(miniMetadatas, "substrate-native", chain);
4336
4948
  const {
4337
4949
  useLegacyTransferableCalculation
4338
- } = miniMetadata?.extra ?? {};
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
- networkId: chainId,
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
- constructor(chaindataProvider, chainConnector) {
5269
+ metadataSub = null;
5270
+ constructor(chaindataProvider) {
4624
5271
  this.chaindataProvider = chaindataProvider;
4625
- this.#chaindataProvider = chaindataProvider;
4626
- this.#chainConnector = chainConnector;
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
- const chains = await this.chaindataProvider.getNetworksMapById("polkadot");
4630
- const tokens = await this.chaindataProvider.getTokensMapById();
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 uniqueChainIds = keys(byNetwork); // getUniqueChainIds(queryResults.newAddressesByToken, tokens)
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: this.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, this.miniMetadatas, queryResults.newAddressesByToken);
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 IMPORTANT_TOKENS = [subNativeTokenId("polkadot"), subNativeTokenId("kusama"), subNativeTokenId("polkadot-asset-hub"), subNativeTokenId("kusama-asset-hub"), subNativeTokenId("bittensor")];
4678
- const sortChainsNativeTokensByPriority = (a, b) => {
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 (IMPORTANT_TOKENS.includes(a)) return -1;
4681
- if (IMPORTANT_TOKENS.includes(b)) return 1;
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 DotNetworkPropertiesSimple = z.object({
4698
- tokenDecimals: z.number().optional().default(0),
4699
- tokenSymbol: z.string().optional().default("Unit")
4700
- });
4701
- const DotNetworkPropertiesArray = z.object({
4702
- tokenDecimals: z.array(z.number()).nonempty(),
4703
- tokenSymbol: z.array(z.string()).nonempty()
4704
- });
4705
- const DotNetworkPropertiesSchema = z.union([DotNetworkPropertiesSimple, DotNetworkPropertiesArray]).transform(val => ({
4706
- tokenDecimals: Array.isArray(val.tokenDecimals) ? val.tokenDecimals[0] : val.tokenDecimals,
4707
- tokenSymbol: Array.isArray(val.tokenSymbol) ? val.tokenSymbol[0] : val.tokenSymbol
4708
- }));
4709
- const getChainProperties = async (chainConnector, networkId) => {
4710
- const properties = await chainConnector.send(networkId, "system_properties", [], true);
4711
- return DotNetworkPropertiesSchema.parse(properties);
4712
- };
4713
-
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, chainConnector);
5378
+ const queryCache = new QueryCache(chaindataProvider);
4729
5379
  const getModuleTokens = async () => {
4730
- return await chaindataProvider.getTokensMapById(moduleType$2);
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
- if (moduleConfig?.disable) return EMPTY_CHAIN_META;
4913
- if (!metadataRpc) return EMPTY_CHAIN_META;
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
- extra: {
4982
- useLegacyTransferableCalculation,
4983
- existentialDeposit,
4984
- nominationPoolsPalletId,
4985
- hasSubtensorPallet
4986
- }
5480
+ metadataVersion
4987
5481
  };
4988
- if (!useLegacyTransferableCalculation) delete chainMeta.extra?.useLegacyTransferableCalculation;
4989
- if (!hasSubtensorPallet) delete chainMeta.extra?.hasSubtensorPallet;
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
- tokenSymbol: symbol,
4996
- tokenDecimals: decimals
4997
- } = await getChainProperties(chainConnector, chainId);
4998
- const {
5489
+ isTestnet,
5490
+ symbol,
5491
+ decimals,
4999
5492
  existentialDeposit
5000
- } = chainMeta.extra ?? {};
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
- platform: "polkadot",
5007
- isDefault: true,
5008
- symbol: symbol,
5009
- name: symbol,
5010
- decimals: decimals,
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
- networkId: chainId
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
- const addressesByTokenByNetwork = getAddresssesByTokenByNetwork(addressesByToken);
5024
- const initialBalancesByNetwork = groupBy$1(initialBalances ?? [], "networkId");
5025
- const controller = new AbortController();
5026
- const safeCallback = (error, result) => {
5027
- if (controller.signal.aborted) return;
5028
- if (isAbortError(error)) return;
5029
- // typescript isnt happy with fowarding parameters as is
5030
- return error ? callback(error, undefined) : callback(error, result);
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
- const unsubsribeFns = Promise.all(keys(addressesByTokenByNetwork).map(async networkId => {
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
- // this is what we want to be done separately for each network
5035
- // this will update the DB so minimetadata will be available when it's used, everywhere else down the tree of subscribeChainBalances
5036
- await getMiniMetadata(chaindataProvider, chainConnector, networkId, moduleType$2, controller.signal);
5037
- } catch (err) {
5038
- if (!isAbortError(err)) log.warn("Failed to get native token miniMetadata for network", networkId, err);
5039
- return () => {};
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
- if (controller.signal.aborted) return () => {};
5042
- return subscribeChainBalances(networkId, {
5043
- addressesByToken: addressesByTokenByNetwork[networkId] ?? {},
5044
- initialBalances: initialBalancesByNetwork[networkId] ?? []
5045
- }, safeCallback, controller.signal);
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
- unsubsribeFns.then(fns => fns.forEach(unsubscribe => unsubscribe()));
5049
- controller.abort();
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.getTokenById(tokenId, "substrate-native");
5700
+ const token = await chaindataProvider.tokenById(tokenId);
5070
5701
  assert(token, `Token ${tokenId} not found in store`);
5071
- const chainId = token.networkId;
5072
- const chain = await chaindataProvider.getNetworkById(chainId, "polkadot");
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 SubPsp22TokenConfigSchema = z.strictObject({
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(_chainId) {
6280
- // we dont need anything
6908
+ async fetchSubstrateChainMeta(chainId) {
6909
+ const isTestnet = (await chaindataProvider.chainById(chainId))?.isTestnet || false;
6281
6910
  return {
6282
- miniMetadata: null,
6283
- extra: null
6911
+ isTestnet
6284
6912
  };
6285
6913
  },
6286
- async fetchSubstrateChainTokens(chainId, _chainMeta, moduleConfig, tokens) {
6287
- if (!tokens?.length) return {};
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 tokenList = {};
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, contractAddress);
6948
+ const id = subPsp22TokenId(chainId, symbol);
6318
6949
  const token = {
6319
6950
  id,
6320
6951
  type: "substrate-psp22",
6321
- platform: "polkadot",
6952
+ isTestnet,
6322
6953
  isDefault: tokenConfig.isDefault ?? true,
6323
6954
  symbol,
6324
6955
  decimals,
6325
- name: tokenConfig?.name || symbol,
6326
- logo: tokenConfig?.logo,
6956
+ logo: tokenConfig?.logo || githubTokenLogoUrl(id),
6957
+ existentialDeposit,
6327
6958
  contractAddress,
6328
- networkId: chainId
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
- tokenList[token.id] = token;
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 tokenList;
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.getTokensMapById();
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.getTokensMapById();
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.getTokenById(tokenId, "substrate-psp22");
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.networkId;
6397
- const chain = await chaindataProvider.getNetworkById(chainId, "polkadot");
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.networkId,
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
- networkId: token.networkId,
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 UNSUPPORTED_CHAIN_META = {
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
- if (metadataRpc === undefined) return UNSUPPORTED_CHAIN_META;
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
- extra: {
6544
- palletId
6545
- }
7184
+ metadataVersion
7185
+ } : {
7186
+ isTestnet,
7187
+ palletId,
7188
+ miniMetadata,
7189
+ metadataVersion
6546
7190
  };
6547
7191
  },
6548
- async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig, tokens) {
6549
- const tokenList = {};
6550
- for (const tokenConfig of tokens ?? []) {
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?.existentialDeposit ?? "0";
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
- platform: "polkadot",
7208
+ isTestnet,
6563
7209
  isDefault: tokenConfig.isDefault ?? true,
6564
7210
  symbol,
6565
7211
  decimals,
6566
- name: tokenConfig?.name ?? symbol,
6567
- logo: tokenConfig?.logo,
7212
+ logo: tokenConfig?.logo || githubTokenLogoUrl(id),
6568
7213
  existentialDeposit,
6569
7214
  onChainId,
6570
- networkId: chainId
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
- tokenList[token.id] = token;
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 tokenList;
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 byNetwork = keys(addressesByToken).reduce((acc, tokenId) => {
6587
- const networkId = parseSubTokensTokenId(tokenId).networkId;
6588
- if (!acc[networkId]) acc[networkId] = {};
6589
- acc[networkId][tokenId] = addressesByToken[tokenId];
6590
- return acc;
6591
- }, {});
6592
- const controller = new AbortController();
6593
- const pUnsubs = Promise.all(toPairs(byNetwork).map(async ([networkId, addressesByToken]) => {
6594
- try {
6595
- const queries = await buildNetworkQueries(networkId, chainConnector, chaindataProvider, addressesByToken, controller.signal);
6596
- if (controller.signal.aborted) return () => {};
6597
- const stateHelper = new RpcStateQueryHelper(chainConnector, queries);
6598
- return await stateHelper.subscribe((error, result) => {
6599
- if (error) return callback(error);
6600
- const balances = result?.filter(b => b !== null) ?? [];
6601
- if (balances.length > 0) callback(null, new Balances(balances));
6602
- });
6603
- } catch (err) {
6604
- if (!isAbortError(err)) log.error(`Failed to subscribe balances for network ${networkId}`, err);
6605
- return () => {};
6606
- }
6607
- }));
6608
- return () => {
6609
- pUnsubs.then(unsubs => {
6610
- unsubs.forEach(unsubscribe => unsubscribe());
6611
- });
6612
- controller.abort();
6613
- };
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(chainConnector, chaindataProvider, addressesByToken);
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.getTokenById(tokenId, "substrate-tokens");
7260
+ const token = await chaindataProvider.tokenById(tokenId);
6630
7261
  assert(token, `Token ${tokenId} not found in store`);
6631
- const chainId = token.networkId;
6632
- const chain = await chaindataProvider.getNetworkById(chainId, "polkadot");
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 miniMetadata = await getMiniMetadata(chaindataProvider, chainConnector, chainId, moduleType);
6635
- const tokensPallet = miniMetadata?.extra?.palletId ?? defaultPalletId;
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 buildNetworkQueries(networkId, chainConnector, chaindataProvider, addressesByToken, signal) {
6735
- const miniMetadata = await getMiniMetadata(chaindataProvider, chainConnector, networkId, moduleType, signal);
6736
- const chain = await chaindataProvider.getNetworkById(networkId, "polkadot");
6737
- const tokens = await chaindataProvider.getTokensMapById();
6738
- if (!chain) return [];
6739
- signal?.throwIfAborted();
6740
- const palletId = miniMetadata.extra.palletId ?? defaultPalletId;
6741
- const networkStorageCoders = buildNetworkStorageCoders(networkId, miniMetadata, {
6742
- storage: [palletId, "Accounts"]
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 = networkStorageCoders?.storage;
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 ${networkId} storage query ${address} / ${token.onChainId}`, address, onChainId);
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 ${networkId}`) ?? {
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
- networkId,
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: networkId,
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, EvmErc20TokenConfigSchema, EvmNativeModule, EvmNativeTokenConfigSchema, EvmUniswapV2Module, EvmUniswapV2TokenConfigSchema, FiatSumBalancesFormatter, ONE_ALPHA_TOKEN, PlanckSumBalancesFormatter, RpcStateQueryHelper, SCALE_FACTOR, SUBTENSOR_MIN_STAKE_AMOUNT_PLANK, SUBTENSOR_ROOT_NETUID, SubAssetsModule, SubAssetsTokenConfigSchema, SubForeignAssetsModule, SubForeignAssetsTokenConfigSchema, SubNativeModule, SubNativeTokenConfigSchema, SubPsp22Module, SubPsp22TokenConfigSchema, SubTokensModule, SubTokensTokenConfigSchema, SumBalancesFormatter, TalismanBalancesDatabase, abiMulticall, balances, buildNetworkStorageCoders, buildStorageCoders, calculateAlphaPrice, calculateTaoAmountFromAlpha, calculateTaoFromDynamicInfo, compress, configureStore, db, decodeOutput, decompress, defaultBalanceModules, deriveMiniMetadataId, detectTransferMethod, erc20Abi, erc20BalancesAggregatorAbi, excludeFromFeePayableLocks, excludeFromTransferableAmount, filterBaseLocks, filterMirrorTokens, getBalanceId, getLockTitle, getUniqueChainIds, getValueId, includeInTotalExtraAmount, makeContractCaller, uniswapV2PairAbi };
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 };