@talismn/balances 0.0.0-pr2043-20250703063531 → 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 -1423
  35. package/dist/talismn-balances.cjs.prod.js +2099 -1423
  36. package/dist/talismn-balances.esm.js +2083 -1413
  37. package/package.json +10 -11
  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,42 +1,40 @@
1
1
  'use strict';
2
2
 
3
+ var PromisePool = require('@supercharge/promise-pool');
4
+ var chaindataProvider = require('@talismn/chaindata-provider');
5
+ var sapi = require('@talismn/sapi');
3
6
  var dexie = require('dexie');
7
+ var isEqual = require('lodash/isEqual');
8
+ var rxjs = require('rxjs');
4
9
  var anylogger = require('anylogger');
5
- var chaindataProvider = require('@talismn/chaindata-provider');
6
10
  var tokenRates = require('@talismn/token-rates');
7
11
  var util = require('@talismn/util');
8
12
  var BigNumber = require('bignumber.js');
9
13
  var util$1 = require('@polkadot/util');
10
14
  var utilCrypto = require('@polkadot/util-crypto');
11
15
  var pako = require('pako');
12
- var z = require('zod/v4');
13
16
  var viem = require('viem');
14
- var lodash = require('lodash');
15
- var isEqual = require('lodash/isEqual');
16
17
  var txwrapperCore = require('@substrate/txwrapper-core');
17
18
  var scale = require('@talismn/scale');
18
19
  var camelCase = require('lodash/camelCase');
19
- var PQueue = require('p-queue');
20
- var sapi = require('@talismn/sapi');
21
20
  var types = require('@polkadot/types');
22
21
  var groupBy = require('lodash/groupBy');
23
22
  var utils = require('@polkadot-api/utils');
24
23
  var polkadotApi = require('polkadot-api');
25
24
  var chainConnector = require('@talismn/chain-connector');
26
- var rxjs = require('rxjs');
27
25
  var scaleTs = require('scale-ts');
28
26
  var upperFirst = require('lodash/upperFirst');
29
27
  var apiContract = require('@polkadot/api-contract');
28
+ var lzString = require('lz-string');
30
29
 
31
30
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
32
31
 
32
+ var PromisePool__default = /*#__PURE__*/_interopDefault(PromisePool);
33
+ var isEqual__default = /*#__PURE__*/_interopDefault(isEqual);
33
34
  var anylogger__default = /*#__PURE__*/_interopDefault(anylogger);
34
35
  var BigNumber__default = /*#__PURE__*/_interopDefault(BigNumber);
35
36
  var pako__default = /*#__PURE__*/_interopDefault(pako);
36
- var z__default = /*#__PURE__*/_interopDefault(z);
37
- var isEqual__default = /*#__PURE__*/_interopDefault(isEqual);
38
37
  var camelCase__default = /*#__PURE__*/_interopDefault(camelCase);
39
- var PQueue__default = /*#__PURE__*/_interopDefault(PQueue);
40
38
  var groupBy__default = /*#__PURE__*/_interopDefault(groupBy);
41
39
  var upperFirst__default = /*#__PURE__*/_interopDefault(upperFirst);
42
40
 
@@ -73,11 +71,48 @@ const DefaultBalanceModule = type => ({
73
71
  // internal
74
72
  //
75
73
 
76
- var pkg = {
77
- name: "@talismn/balances",
78
- version: "0.0.0-pr2043-20250703063531"};
74
+ /**
75
+ * Fetches tokens for EVM networks.
76
+ */
77
+ class EvmTokenFetcher {
78
+ #chaindataProvider;
79
+ #balanceModules;
80
+ constructor(chaindataProvider, balanceModules) {
81
+ this.#chaindataProvider = chaindataProvider;
82
+ this.#balanceModules = balanceModules;
83
+ }
84
+ async update(evmNetworkIds) {
85
+ await this.updateEvmNetworks(evmNetworkIds);
86
+ }
87
+ async updateEvmNetworks(evmNetworkIds) {
88
+ const evmNetworks = new Map((await this.#chaindataProvider.evmNetworks()).map(evmNetwork => [evmNetwork.id, evmNetwork]));
89
+ const allEvmTokens = {};
90
+ const evmNetworkConcurrency = 10;
91
+ await PromisePool.PromisePool.withConcurrency(evmNetworkConcurrency).for(evmNetworkIds).process(async evmNetworkId => {
92
+ const evmNetwork = evmNetworks.get(evmNetworkId);
93
+ if (!evmNetwork) return;
94
+ for (const mod of this.#balanceModules.filter(m => m.type.startsWith("evm-"))) {
95
+ const balancesConfig = (evmNetwork.balancesConfig ?? []).find(({
96
+ moduleType
97
+ }) => moduleType === mod.type);
98
+ const moduleConfig = balancesConfig?.moduleConfig ?? {};
99
+
100
+ // chainMeta arg only needs the isTestnet property, let's save a db roundtrip for now
101
+ const isTestnet = evmNetwork.isTestnet ?? false;
102
+ const tokens = await mod.fetchEvmChainTokens(evmNetworkId, {
103
+ isTestnet
104
+ }, moduleConfig);
105
+ for (const [tokenId, token] of Object.entries(tokens)) allEvmTokens[tokenId] = token;
106
+ }
107
+ });
108
+ await this.#chaindataProvider.updateEvmNetworkTokens(Object.values(allEvmTokens));
109
+ }
110
+ }
111
+
112
+ var packageJson = {
113
+ name: "@talismn/balances"};
79
114
 
80
- var log = anylogger__default.default(pkg.name);
115
+ var log = anylogger__default.default(packageJson.name);
81
116
 
82
117
  function excludeFromTransferableAmount(locks) {
83
118
  if (typeof locks === "string") return BigInt(locks);
@@ -287,13 +322,15 @@ class Balances {
287
322
  return new SumBalancesFormatter(this);
288
323
  }
289
324
  }
325
+ const isBalanceEvm = balance => "evmNetworkId" in balance;
290
326
  const getBalanceId = balance => {
291
327
  const {
292
328
  source,
293
329
  address,
294
330
  tokenId
295
331
  } = balance;
296
- return [source, address, tokenId].join("::");
332
+ const locationId = isBalanceEvm(balance) ? balance.evmNetworkId : balance.chainId;
333
+ return [source, address, locationId, tokenId].filter(util.isTruthy).join("::");
297
334
  };
298
335
 
299
336
  /**
@@ -355,17 +392,23 @@ class Balance {
355
392
  get address() {
356
393
  return this.#storage.address;
357
394
  }
358
- get networkId() {
359
- return this.#storage.networkId;
395
+ get chainId() {
396
+ return isBalanceEvm(this.#storage) ? undefined : this.#storage.chainId;
397
+ }
398
+ get chain() {
399
+ return this.#db?.chains && this.chainId && this.#db?.chains[this.chainId] || null;
360
400
  }
361
- get network() {
362
- return this.#db?.networks?.[this.networkId] || null;
401
+ get evmNetworkId() {
402
+ return isBalanceEvm(this.#storage) ? this.#storage.evmNetworkId : undefined;
403
+ }
404
+ get evmNetwork() {
405
+ return this.#db?.evmNetworks && this.evmNetworkId && this.#db?.evmNetworks[this.evmNetworkId] || null;
363
406
  }
364
407
  get tokenId() {
365
408
  return this.#storage.tokenId;
366
409
  }
367
410
  get token() {
368
- return this.#db?.tokens?.[this.tokenId] || null;
411
+ return this.#db?.tokens && this.#db?.tokens[this.tokenId] || null;
369
412
  }
370
413
  get decimals() {
371
414
  return this.token?.decimals || null;
@@ -378,9 +421,9 @@ class Balance {
378
421
  //
379
422
  // This means that those rates are always available for calculating the uniswapv2 rates,
380
423
  // regardless of whether or not the underlying erc20s are actually in chaindata and enabled.
381
- if (this.isSource("evm-uniswapv2") && this.token?.type === "evm-uniswapv2") {
382
- const tokenId0 = chaindataProvider.evmErc20TokenId(this.networkId, this.token.tokenAddress0);
383
- const tokenId1 = chaindataProvider.evmErc20TokenId(this.networkId, this.token.tokenAddress1);
424
+ if (this.isSource("evm-uniswapv2") && this.token?.type === "evm-uniswapv2" && this.evmNetworkId) {
425
+ const tokenId0 = evmErc20TokenId$1(this.evmNetworkId, this.token.tokenAddress0);
426
+ const tokenId1 = evmErc20TokenId$1(this.evmNetworkId, this.token.tokenAddress1);
384
427
  const decimals = this.token.decimals;
385
428
  const decimals0 = this.token.decimals0;
386
429
  const decimals1 = this.token.decimals1;
@@ -459,7 +502,9 @@ class Balance {
459
502
  const nomPoolStakedPlancks = this.locks.some(lock => lock.source === "substrate-native-holds" && lock.label === "DelegatedStaking") ? 0n : this.nompools.map(({
460
503
  amount
461
504
  }) => amount.planck).reduce((a, b) => a + b, 0n);
462
- return this.#format(this.free.planck + this.reserved.planck + nomPoolStakedPlancks + this.subtensor.map(({
505
+ return this.#format(this.free.planck + this.reserved.planck + nomPoolStakedPlancks + this.crowdloans.map(({
506
+ amount
507
+ }) => amount.planck).reduce((a, b) => a + b, 0n) + this.subtensor.map(({
463
508
  amount
464
509
  }) => amount.planck).reduce((a, b) => a + b, 0n) + includeInTotalExtraAmount(extra));
465
510
  }
@@ -495,6 +540,9 @@ class Balance {
495
540
  get locks() {
496
541
  return this.getValue("locked");
497
542
  }
543
+ get crowdloans() {
544
+ return this.getValue("crowdloan");
545
+ }
498
546
  get nompools() {
499
547
  return this.getValue("nompool");
500
548
  }
@@ -573,7 +621,7 @@ class Balance {
573
621
  const nomPoolStakedPlancks = this.locks.some(lock => lock.source === "substrate-native-holds" && lock.label === "DelegatedStaking") ? 0n : this.nompools.map(({
574
622
  amount
575
623
  }) => amount.planck).reduce((a, b) => a + b, 0n);
576
- const otherUnavailable = nomPoolStakedPlancks + this.subtensor.reduce((total, each) => total + each.amount.planck, 0n);
624
+ const otherUnavailable = nomPoolStakedPlancks + this.crowdloans.reduce((total, each) => total + each.amount.planck, 0n) + this.subtensor.reduce((total, each) => total + each.amount.planck, 0n);
577
625
  return this.#format(baseUnavailable + otherUnavailable);
578
626
  }
579
627
 
@@ -808,6 +856,10 @@ const filterMirrorTokens = (balance, i, balances) => {
808
856
  return !mirrorOf || !balances.find(b => b.tokenId === mirrorOf);
809
857
  };
810
858
 
859
+ // TODO: Move this into a common module which can then be imported both here and into EvmErc20Module
860
+ // We can't import this directly from EvmErc20Module because then we'd have a circular dependency
861
+ const evmErc20TokenId$1 = (chainId, tokenContractAddress) => `${chainId}-evm-erc20-${tokenContractAddress}`.toLowerCase();
862
+
811
863
  /**
812
864
  * `BalanceTypes` is an automatically determined sub-selection of `PluginBalanceTypes`.
813
865
  *
@@ -838,6 +890,7 @@ const getValueId = amount => {
838
890
  const getMetaId = () => {
839
891
  const meta = amount.meta;
840
892
  if (!meta) return "";
893
+ if (amount.type === "crowdloan") return meta.paraId?.toString() ?? "";
841
894
  if (amount.type === "nompool") return meta.poolId?.toString() ?? "";
842
895
  if (amount.type === "subtensor") {
843
896
  const {
@@ -861,9 +914,10 @@ const getValueId = amount => {
861
914
  const deriveMiniMetadataId = ({
862
915
  source,
863
916
  chainId,
917
+ specName,
864
918
  specVersion,
865
- libVersion
866
- }) => util$1.u8aToHex(utilCrypto.xxhashAsU8a(new TextEncoder().encode(`${source}${chainId}${specVersion}${libVersion}`), 64), undefined, false);
919
+ balancesConfig
920
+ }) => util$1.u8aToHex(utilCrypto.xxhashAsU8a(new TextEncoder().encode(`${source}${chainId}${specName}${specVersion}${balancesConfig}`), 64), undefined, false);
867
921
 
868
922
  // for DB version 3, Wallet version 1.21.0
869
923
  const upgradeRemoveSymbolFromNativeTokenId = async tx => {
@@ -958,9 +1012,230 @@ class TalismanBalancesDatabase extends dexie.Dexie {
958
1012
  }
959
1013
  const db = new TalismanBalancesDatabase();
960
1014
 
961
- const TokenConfigBaseSchema = chaindataProvider.TokenBaseSchema.partial().omit({
962
- id: true
963
- });
1015
+ const minimumHydrationInterval = 300_000; // 300_000ms = 300s = 5 minutes
1016
+
1017
+ /**
1018
+ * A substrate dapp needs access to a set of types when it wants to communicate with a blockchain node.
1019
+ *
1020
+ * These types are used to encode requests & decode responses via the SCALE codec.
1021
+ * Each chain generally has its own set of types.
1022
+ *
1023
+ * Substrate provides a construct to retrieve these types from a blockchain node.
1024
+ * The chain metadata.
1025
+ *
1026
+ * The metadata includes the types required for any communication with the chain,
1027
+ * including lots of methods which are not relevant to balance fetching.
1028
+ *
1029
+ * As such, the metadata can clock in at around 1-2MB per chain, which is a lot of storage
1030
+ * for browser-based dapps which want to connect to lots of chains.
1031
+ *
1032
+ * By utilizing the wonderful [scale-ts](https://github.com/unstoppablejs/unstoppablejs/tree/main/packages/scale-ts#readme) library,
1033
+ * we can trim the chain metadata down so that it only includes the types we need for balance fetching.
1034
+ *
1035
+ * Each balance module has a function to do just that, `BalanceModule::fetchSubstrateChainMeta`.
1036
+ *
1037
+ * But, we only want to run this operation when necessary.
1038
+ *
1039
+ * The purpose of this class, `MiniMetadataUpdater`, is to maintain a local cache of
1040
+ * trimmed-down metadatas, which we'll refer to as `MiniMetadatas`.
1041
+ */
1042
+ class MiniMetadataUpdater {
1043
+ #lastHydratedMiniMetadatasAt = 0;
1044
+ #lastHydratedCustomChainsAt = 0;
1045
+ #chainConnectors;
1046
+ #chaindataProvider;
1047
+ #balanceModules;
1048
+ constructor(chainConnectors, chaindataProvider, balanceModules) {
1049
+ this.#chainConnectors = chainConnectors;
1050
+ this.#chaindataProvider = chaindataProvider;
1051
+ this.#balanceModules = balanceModules;
1052
+ }
1053
+
1054
+ /** Subscribe to the metadata for a chain */
1055
+ subscribe(chainId) {
1056
+ return rxjs.from(dexie.liveQuery(() => db.miniMetadatas.filter(m => m.chainId === chainId).toArray().then(array => array[0])));
1057
+ }
1058
+ async update(chainIds) {
1059
+ await this.updateSubstrateChains(chainIds);
1060
+ }
1061
+ async statuses(chains) {
1062
+ const ids = await db.miniMetadatas.orderBy("id").primaryKeys();
1063
+ const wantedIdsByChain = new Map(chains.flatMap(({
1064
+ id: chainId,
1065
+ specName,
1066
+ specVersion,
1067
+ balancesConfig
1068
+ }) => {
1069
+ if (specName === null) return [];
1070
+ if (specVersion === null) return [];
1071
+ return [[chainId, this.#balanceModules.filter(m => m.type.startsWith("substrate-")).map(({
1072
+ type: source
1073
+ }) => deriveMiniMetadataId({
1074
+ source,
1075
+ chainId: chainId,
1076
+ specName: specName,
1077
+ specVersion: specVersion,
1078
+ balancesConfig: JSON.stringify((balancesConfig ?? []).find(({
1079
+ moduleType
1080
+ }) => moduleType === source)?.moduleConfig ?? {})
1081
+ }))]];
1082
+ }));
1083
+ const statusesByChain = new Map(Array.from(wantedIdsByChain.entries()).map(([chainId, wantedIds]) => [chainId, wantedIds.every(wantedId => ids.includes(wantedId)) ? "good" : "none"]));
1084
+ return {
1085
+ wantedIdsByChain,
1086
+ statusesByChain
1087
+ };
1088
+ }
1089
+ async hydrateFromChaindata() {
1090
+ const now = Date.now();
1091
+ if (now - this.#lastHydratedMiniMetadatasAt < minimumHydrationInterval) return false;
1092
+ const dbHasMiniMetadatas = (await db.miniMetadatas.count()) > 0;
1093
+ try {
1094
+ try {
1095
+ // TODO: Move `fetchMiniMetadatas` into this package,
1096
+ // so that we don't have a circular import between `@talismn/balances` and `@talismn/chaindata-provider`.
1097
+ var miniMetadatas = await chaindataProvider.fetchMiniMetadatas(); // eslint-disable-line no-var
1098
+ if (miniMetadatas.length <= 0) throw new Error("Ignoring empty chaindata miniMetadatas response");
1099
+ } catch (error) {
1100
+ if (dbHasMiniMetadatas) throw error;
1101
+ // On first start-up (db is empty), if we fail to fetch miniMetadatas then we should
1102
+ // initialize the DB with the list of miniMetadatas inside our init/mini-metadatas.json file.
1103
+ // This data will represent a relatively recent copy of what's in chaindata,
1104
+ // which will be better for our users than to have nothing at all.
1105
+ var miniMetadatas = await chaindataProvider.fetchInitMiniMetadatas(); // eslint-disable-line no-var
1106
+ }
1107
+ await db.miniMetadatas.bulkPut(miniMetadatas);
1108
+ this.#lastHydratedMiniMetadatasAt = now;
1109
+ return true;
1110
+ } catch (error) {
1111
+ log.warn(`Failed to hydrate miniMetadatas from chaindata`, error);
1112
+ return false;
1113
+ }
1114
+ }
1115
+ async hydrateCustomChains() {
1116
+ const now = Date.now();
1117
+ if (now - this.#lastHydratedCustomChainsAt < minimumHydrationInterval) return false;
1118
+ const chains = await this.#chaindataProvider.chains();
1119
+ const customChains = chains.filter(chain => "isCustom" in chain && chain.isCustom);
1120
+ const updatedCustomChains = [];
1121
+ const concurrency = 4;
1122
+ (await PromisePool.PromisePool.withConcurrency(concurrency).for(customChains).process(async customChain => {
1123
+ const send = (method, params) => this.#chainConnectors.substrate?.send(customChain.id, method, params);
1124
+ const [genesisHash, runtimeVersion, chainName, chainType] = await Promise.all([send("chain_getBlockHash", [0]), send("state_getRuntimeVersion", []), send("system_chain", []), send("system_chainType", [])]);
1125
+
1126
+ // deconstruct rpc data
1127
+ const {
1128
+ specName,
1129
+ implName
1130
+ } = runtimeVersion;
1131
+ const specVersion = String(runtimeVersion.specVersion);
1132
+ const changed = customChain.genesisHash !== genesisHash || customChain.chainName !== chainName || !isEqual__default.default(customChain.chainType, chainType) || customChain.implName !== implName || customChain.specName !== specName || customChain.specVersion !== specVersion;
1133
+ if (!changed) return;
1134
+ customChain.genesisHash = genesisHash;
1135
+ customChain.chainName = chainName;
1136
+ customChain.chainType = chainType;
1137
+ customChain.implName = implName;
1138
+ customChain.specName = specName;
1139
+ customChain.specVersion = specVersion;
1140
+ updatedCustomChains.push(customChain);
1141
+ })).errors.forEach(error => log.error("Error hydrating custom chains", error));
1142
+ if (updatedCustomChains.length > 0) {
1143
+ await this.#chaindataProvider.transaction("rw", ["chains"], async () => {
1144
+ for (const updatedCustomChain of updatedCustomChains) {
1145
+ await this.#chaindataProvider.removeCustomChain(updatedCustomChain.id);
1146
+ await this.#chaindataProvider.addCustomChain(updatedCustomChain);
1147
+ }
1148
+ });
1149
+ }
1150
+ if (updatedCustomChains.length > 0) this.#lastHydratedCustomChainsAt = now;
1151
+ return true;
1152
+ }
1153
+ async updateSubstrateChains(chainIds) {
1154
+ const chains = new Map((await this.#chaindataProvider.chains()).map(chain => [chain.id, chain]));
1155
+ const filteredChains = chainIds.flatMap(chainId => chains.get(chainId) ?? []);
1156
+ const ids = await db.miniMetadatas.orderBy("id").primaryKeys();
1157
+ const {
1158
+ wantedIdsByChain,
1159
+ statusesByChain
1160
+ } = await this.statuses(filteredChains);
1161
+
1162
+ // clean up store
1163
+ const wantedIds = Array.from(wantedIdsByChain.values()).flatMap(ids => ids);
1164
+ const unwantedIds = ids.filter(id => !wantedIds.includes(id));
1165
+ if (unwantedIds.length > 0) {
1166
+ const chainIds = Array.from(new Set((await db.miniMetadatas.bulkGet(unwantedIds)).map(m => m?.chainId)));
1167
+ log.info(`Pruning ${unwantedIds.length} miniMetadatas on chains ${chainIds.join(", ")}`);
1168
+ await db.miniMetadatas.bulkDelete(unwantedIds);
1169
+ }
1170
+ const needUpdates = Array.from(statusesByChain.entries()).filter(([, status]) => status !== "good").map(([chainId]) => chainId);
1171
+ if (needUpdates.length > 0) log.info(`${needUpdates.length} miniMetadatas need updates (${needUpdates.join(", ")})`);
1172
+ const availableTokenLogos = await chaindataProvider.availableTokenLogoFilenames().catch(error => {
1173
+ log.error("Failed to fetch available token logos", error);
1174
+ return [];
1175
+ });
1176
+ const concurrency = 12;
1177
+ (await PromisePool.PromisePool.withConcurrency(concurrency).for(needUpdates).process(async chainId => {
1178
+ log.info(`Updating metadata for chain ${chainId}`);
1179
+ const chain = chains.get(chainId);
1180
+ if (!chain) return;
1181
+ const {
1182
+ specName,
1183
+ specVersion
1184
+ } = chain;
1185
+ if (specName === null) return;
1186
+ if (specVersion === null) return;
1187
+ const fetchMetadata = async () => {
1188
+ try {
1189
+ return await sapi.fetchBestMetadata((method, params, isCacheable) => {
1190
+ if (!this.#chainConnectors.substrate) throw new Error("Substrate connector is not available");
1191
+ return this.#chainConnectors.substrate.send(chainId, method, params, isCacheable);
1192
+ }, true // allow v14 fallback
1193
+ );
1194
+ } catch (err) {
1195
+ log.warn(`Failed to fetch metadata for chain ${chainId}`);
1196
+ return undefined;
1197
+ }
1198
+ };
1199
+ const [metadataRpc, systemProperties] = await Promise.all([fetchMetadata(), this.#chainConnectors.substrate?.send(chainId, "system_properties", [])]);
1200
+ for (const mod of this.#balanceModules.filter(m => m.type.startsWith("substrate-"))) {
1201
+ const balancesConfig = (chain.balancesConfig ?? []).find(({
1202
+ moduleType
1203
+ }) => moduleType === mod.type);
1204
+ const moduleConfig = balancesConfig?.moduleConfig ?? {};
1205
+ const chainMeta = await mod.fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc, systemProperties);
1206
+ const tokens = await mod.fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig);
1207
+
1208
+ // update tokens in chaindata
1209
+ await this.#chaindataProvider.updateChainTokens(chainId, mod.type, Object.values(tokens), availableTokenLogos);
1210
+
1211
+ // update miniMetadatas
1212
+ const {
1213
+ miniMetadata: data,
1214
+ metadataVersion: version,
1215
+ ...extra
1216
+ } = chainMeta ?? {};
1217
+ await db.miniMetadatas.put({
1218
+ id: deriveMiniMetadataId({
1219
+ source: mod.type,
1220
+ chainId,
1221
+ specName,
1222
+ specVersion,
1223
+ balancesConfig: JSON.stringify(moduleConfig)
1224
+ }),
1225
+ source: mod.type,
1226
+ chainId,
1227
+ specName,
1228
+ specVersion,
1229
+ balancesConfig: JSON.stringify(moduleConfig),
1230
+ // TODO: Standardise return value from `fetchSubstrateChainMeta`
1231
+ version,
1232
+ data,
1233
+ extra: JSON.stringify(extra)
1234
+ });
1235
+ }
1236
+ })).errors.forEach(error => log.error("Error updating chain metadata", error));
1237
+ }
1238
+ }
964
1239
 
965
1240
  const erc20Abi = [{
966
1241
  constant: true,
@@ -1192,11 +1467,8 @@ const erc20Abi = [{
1192
1467
 
1193
1468
  const erc20BalancesAggregatorAbi = viem.parseAbi(["struct AccountToken {address account; address token;}", "function balances(AccountToken[] memory accountTokens) public view returns (uint256[] memory)"]);
1194
1469
 
1195
- const moduleType$7 = "evm-erc20";
1196
- const EvmErc20TokenConfigSchema = z__default.default.strictObject({
1197
- contractAddress: chaindataProvider.EvmErc20TokenSchema.shape.contractAddress,
1198
- ...TokenConfigBaseSchema.shape
1199
- });
1470
+ const moduleType$8 = "evm-erc20";
1471
+ const evmErc20TokenId = (chainId, tokenContractAddress) => `${chainId}-evm-erc20-${tokenContractAddress}`.toLowerCase();
1200
1472
  const EvmErc20Module = hydrate => {
1201
1473
  const {
1202
1474
  chainConnectors,
@@ -1220,36 +1492,38 @@ const EvmErc20Module = hydrate => {
1220
1492
  }, {});
1221
1493
  };
1222
1494
  const getModuleTokens = async () => {
1223
- return await chaindataProvider$1.getTokensMapById(moduleType$7);
1495
+ return await chaindataProvider$1.tokensByIdForType(moduleType$8);
1224
1496
  };
1225
1497
  const getErc20Aggregators = async () => {
1226
- const evmNetworks = await chaindataProvider$1.getNetworks("ethereum");
1227
- return Object.fromEntries(evmNetworks.filter(n => n.contracts?.Erc20Aggregator).map(n => [n.id, n.contracts.Erc20Aggregator]));
1498
+ const evmNetworks = await chaindataProvider$1.evmNetworks();
1499
+ return Object.fromEntries(evmNetworks.filter(n => n.erc20aggregator).map(n => [n.id, n.erc20aggregator]));
1228
1500
  };
1229
1501
  return {
1230
- ...DefaultBalanceModule(moduleType$7),
1502
+ ...DefaultBalanceModule(moduleType$8),
1231
1503
  /**
1232
1504
  * This method is currently executed on [a squid](https://github.com/TalismanSociety/chaindata-squid/blob/0ee02818bf5caa7362e3f3664e55ef05ec8df078/src/steps/updateEvmNetworksFromGithub.ts#L280-L284).
1233
1505
  * 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.
1234
1506
  */
1235
- async fetchEvmChainMeta(_chainId) {
1507
+ async fetchEvmChainMeta(chainId) {
1508
+ const isTestnet = (await chaindataProvider$1.evmNetworkById(chainId))?.isTestnet || false;
1236
1509
  return {
1237
- miniMetadata: null,
1238
- extra: null
1510
+ isTestnet
1239
1511
  };
1240
1512
  },
1241
1513
  /**
1242
1514
  * This method is currently executed on [a squid](https://github.com/TalismanSociety/chaindata-squid/blob/0ee02818bf5caa7362e3f3664e55ef05ec8df078/src/steps/updateEvmNetworksFromGithub.ts#L338-L343).
1243
1515
  * 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.
1244
1516
  */
1245
- async fetchEvmChainTokens(chainId, _chainMeta, moduleConfig, tokens) {
1517
+ async fetchEvmChainTokens(chainId, chainMeta, moduleConfig) {
1518
+ const {
1519
+ isTestnet
1520
+ } = chainMeta;
1246
1521
  const chainTokens = {};
1247
- for (const tokenConfig of tokens ?? []) {
1522
+ for (const tokenConfig of moduleConfig?.tokens ?? []) {
1248
1523
  const {
1249
1524
  contractAddress,
1250
1525
  symbol: contractSymbol,
1251
- decimals: contractDecimals,
1252
- name: contractName
1526
+ decimals: contractDecimals
1253
1527
  } = tokenConfig;
1254
1528
  // TODO : in chaindata's build, filter out all tokens that don't have any of these
1255
1529
  if (!contractAddress || !contractSymbol || contractDecimals === undefined) {
@@ -1259,29 +1533,26 @@ const EvmErc20Module = hydrate => {
1259
1533
  const symbol = tokenConfig?.symbol ?? contractSymbol ?? "ETH";
1260
1534
  const decimals = typeof tokenConfig?.decimals === "number" ? tokenConfig.decimals : typeof contractDecimals === "number" ? contractDecimals : 18;
1261
1535
  if (!symbol || typeof decimals !== "number") continue;
1262
- const id = chaindataProvider.evmErc20TokenId(chainId, contractAddress);
1536
+ const id = evmErc20TokenId(chainId, contractAddress);
1263
1537
  const token = {
1264
1538
  id,
1265
1539
  type: "evm-erc20",
1266
- platform: "ethereum",
1540
+ isTestnet,
1267
1541
  isDefault: tokenConfig.isDefault ?? true,
1268
1542
  symbol,
1269
1543
  decimals,
1270
- name: contractName ?? symbol,
1271
- logo: tokenConfig?.logo,
1544
+ logo: tokenConfig?.logo || chaindataProvider.githubTokenLogoUrl(id),
1272
1545
  contractAddress,
1273
- networkId: chainId
1546
+ evmNetwork: {
1547
+ id: chainId
1548
+ }
1274
1549
  };
1275
1550
  if (tokenConfig?.symbol) token.symbol = tokenConfig?.symbol;
1276
1551
  if (tokenConfig?.coingeckoId) token.coingeckoId = tokenConfig?.coingeckoId;
1552
+ if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName;
1277
1553
  if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf;
1278
1554
  if (tokenConfig?.noDiscovery) token.noDiscovery = tokenConfig?.noDiscovery;
1279
- const validation = chaindataProvider.EvmErc20TokenSchema.safeParse(token);
1280
- if (validation.success) {
1281
- chainTokens[token.id] = token;
1282
- } else {
1283
- log.warn("Ignoring invalid token", token.id, validation.error.message, validation.error.issues);
1284
- }
1555
+ chainTokens[token.id] = token;
1285
1556
  }
1286
1557
  return chainTokens;
1287
1558
  },
@@ -1293,18 +1564,18 @@ const EvmErc20Module = hydrate => {
1293
1564
  const subscriptionInterval = 6_000; // 6_000ms == 6 seconds
1294
1565
  const initDelay = 1_500; // 1_500ms == 1.5 seconds
1295
1566
  const initialisingBalances = new Set();
1296
- const positiveBalanceNetworks = new Set(initialBalances?.map(b => b.networkId));
1567
+ const positiveBalanceNetworks = new Set(initialBalances?.map(b => b.evmNetworkId));
1297
1568
  const tokens = await getModuleTokens();
1298
1569
 
1299
1570
  // for chains with a zero balance we only call fetchBalances once every 5 subscriptionIntervals
1300
1571
  // if subscriptionInterval is 6 seconds, this means we only poll chains with a zero balance every 30 seconds
1301
1572
  let zeroBalanceSubscriptionIntervalCounter = 0;
1302
- const evmNetworks = await chaindataProvider$1.getNetworksMapById("ethereum");
1573
+ const evmNetworks = await chaindataProvider$1.evmNetworksById();
1303
1574
  const ethAddressesByToken = Object.fromEntries(Object.entries(addressesByToken).map(([tokenId, addresses]) => {
1304
1575
  const ethAddresses = addresses.filter(util.isEthereumAddress);
1305
1576
  if (ethAddresses.length === 0) return null;
1306
1577
  const token = tokens[tokenId];
1307
- const evmNetworkId = token.networkId;
1578
+ const evmNetworkId = token.evmNetwork?.id;
1308
1579
  if (!evmNetworkId) return null;
1309
1580
  return [tokenId, ethAddresses];
1310
1581
  }).filter(x => Boolean(x)));
@@ -1430,17 +1701,20 @@ const fetchBalances$3 = async (evmChainConnector, tokenAddressesByNetwork, erc20
1430
1701
  results: [],
1431
1702
  errors: []
1432
1703
  };
1433
- await Promise.all(Object.entries(tokenAddressesByNetwork).map(async ([networkId, networkParams]) => {
1434
- const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(networkId);
1435
- if (!publicClient) throw new EvmErc20NetworkError(`Could not get rpc provider for evm network ${networkId}`, networkId);
1436
- const balances = await getEvmTokenBalances(publicClient, networkParams, result.errors, erc20Aggregators[networkId]);
1704
+ await Promise.all(Object.entries(tokenAddressesByNetwork).map(async ([evmNetworkId, networkParams]) => {
1705
+ const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(evmNetworkId);
1706
+ if (!publicClient) throw new EvmErc20NetworkError(`Could not get rpc provider for evm network ${evmNetworkId}`, evmNetworkId);
1707
+ const balances = await getEvmTokenBalances(publicClient, networkParams, result.errors, erc20Aggregators[evmNetworkId]);
1437
1708
 
1438
1709
  // consider only non null balances in the results
1439
1710
  result.results.push(...balances.filter(util.isTruthy).map((free, i) => ({
1440
1711
  source: "evm-erc20",
1441
1712
  status: "live",
1442
1713
  address: networkParams[i].address,
1443
- networkId,
1714
+ multiChainId: {
1715
+ evmChainId: evmNetworkId
1716
+ },
1717
+ evmNetworkId,
1444
1718
  tokenId: networkParams[i].token.id,
1445
1719
  value: free
1446
1720
  })));
@@ -1511,7 +1785,7 @@ function groupAddressesByTokenByEvmNetwork$1(addressesByToken, tokens) {
1511
1785
  log.error(`Token ${tokenId} not found`);
1512
1786
  return byChain;
1513
1787
  }
1514
- const chainId = token.networkId;
1788
+ const chainId = token.evmNetwork?.id;
1515
1789
  if (!chainId) {
1516
1790
  log.error(`Token ${tokenId} has no evm network`);
1517
1791
  return byChain;
@@ -1524,38 +1798,67 @@ function groupAddressesByTokenByEvmNetwork$1(addressesByToken, tokens) {
1524
1798
 
1525
1799
  const abiMulticall = viem.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)"]);
1526
1800
 
1527
- const moduleType$6 = "evm-native";
1528
- const EvmNativeTokenConfigSchema = TokenConfigBaseSchema;
1801
+ const moduleType$7 = "evm-native";
1802
+ const evmNativeTokenId = chainId => `${chainId}-evm-native`.toLowerCase().replace(/ /g, "-");
1803
+ const getEvmNetworkIdFromTokenId = tokenId => {
1804
+ const evmNetworkId = tokenId.split("-")[0];
1805
+ if (!evmNetworkId) throw new Error(`Can't detect chainId for token ${tokenId}`);
1806
+ return evmNetworkId;
1807
+ };
1529
1808
  const EvmNativeModule = hydrate => {
1530
1809
  const {
1531
1810
  chainConnectors,
1532
1811
  chaindataProvider: chaindataProvider$1
1533
1812
  } = hydrate;
1534
1813
  const getModuleTokens = async () => {
1535
- return await chaindataProvider$1.getTokensMapById(moduleType$6);
1814
+ return await chaindataProvider$1.tokensByIdForType(moduleType$7);
1536
1815
  };
1537
1816
  return {
1538
- ...DefaultBalanceModule(moduleType$6),
1817
+ ...DefaultBalanceModule(moduleType$7),
1539
1818
  get tokens() {
1540
- return chaindataProvider$1.getTokensMapById(moduleType$6);
1819
+ return chaindataProvider$1.tokensByIdForType(moduleType$7);
1541
1820
  },
1542
1821
  /**
1543
1822
  * This method is currently executed on [a squid](https://github.com/TalismanSociety/chaindata-squid/blob/0ee02818bf5caa7362e3f3664e55ef05ec8df078/src/steps/updateEvmNetworksFromGithub.ts#L280-L284).
1544
1823
  * 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.
1545
1824
  */
1546
- async fetchEvmChainMeta(_chainId) {
1825
+ async fetchEvmChainMeta(chainId) {
1826
+ const isTestnet = (await chaindataProvider$1.evmNetworkById(chainId))?.isTestnet || false;
1547
1827
  return {
1548
- miniMetadata: null,
1549
- extra: null
1828
+ isTestnet
1550
1829
  };
1551
1830
  },
1552
1831
  /**
1553
1832
  * This method is currently executed on [a squid](https://github.com/TalismanSociety/chaindata-squid/blob/0ee02818bf5caa7362e3f3664e55ef05ec8df078/src/steps/updateEvmNetworksFromGithub.ts#L338-L343).
1554
1833
  * 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.
1555
1834
  */
1556
- async fetchEvmChainTokens() {
1557
- // token is currently generated in chaindata from the EthNetworkConfig["nativeCurrency"] field
1558
- return {};
1835
+ async fetchEvmChainTokens(chainId, chainMeta, moduleConfig) {
1836
+ const {
1837
+ isTestnet
1838
+ } = chainMeta;
1839
+ const symbol = moduleConfig?.symbol ?? "ETH";
1840
+ const decimals = typeof moduleConfig?.decimals === "number" ? moduleConfig.decimals : 18;
1841
+ const id = evmNativeTokenId(chainId);
1842
+ const nativeToken = {
1843
+ id,
1844
+ type: "evm-native",
1845
+ isTestnet,
1846
+ isDefault: true,
1847
+ symbol,
1848
+ decimals,
1849
+ logo: moduleConfig?.logo || chaindataProvider.githubTokenLogoUrl(id),
1850
+ evmNetwork: {
1851
+ id: chainId
1852
+ }
1853
+ };
1854
+ if (moduleConfig?.symbol) nativeToken.symbol = moduleConfig?.symbol;
1855
+ if (moduleConfig?.coingeckoId) nativeToken.coingeckoId = moduleConfig?.coingeckoId;
1856
+ if (moduleConfig?.dcentName) nativeToken.dcentName = moduleConfig?.dcentName;
1857
+ if (moduleConfig?.mirrorOf) nativeToken.mirrorOf = moduleConfig?.mirrorOf;
1858
+ if (moduleConfig?.noDiscovery) nativeToken.noDiscovery = moduleConfig?.noDiscovery;
1859
+ return {
1860
+ [nativeToken.id]: nativeToken
1861
+ };
1559
1862
  },
1560
1863
  async subscribeBalances({
1561
1864
  addressesByToken,
@@ -1566,11 +1869,11 @@ const EvmNativeModule = hydrate => {
1566
1869
  const initDelay = 500; // 500ms == 0.5 seconds
1567
1870
 
1568
1871
  const tokens = await getModuleTokens();
1569
- const ethAddressesByToken = lodash.fromPairs(lodash.toPairs(addressesByToken).map(([tokenId, addresses]) => {
1872
+ const ethAddressesByToken = Object.fromEntries(Object.entries(addressesByToken).map(([tokenId, addresses]) => {
1570
1873
  const ethAddresses = addresses.filter(util.isEthereumAddress);
1571
1874
  if (ethAddresses.length === 0) return null;
1572
1875
  const token = tokens[tokenId];
1573
- const evmNetworkId = token.networkId;
1876
+ const evmNetworkId = token.evmNetwork?.id;
1574
1877
  if (!evmNetworkId) return null;
1575
1878
  return [tokenId, ethAddresses];
1576
1879
  }).filter(x => Boolean(x)));
@@ -1580,16 +1883,16 @@ const EvmNativeModule = hydrate => {
1580
1883
  let zeroBalanceSubscriptionIntervalCounter = 0;
1581
1884
 
1582
1885
  // setup initialising balances for all active evm networks
1583
- const activeEvmNetworkIds = lodash.keys(ethAddressesByToken).map(chaindataProvider.networkIdFromTokenId);
1886
+ const activeEvmNetworkIds = Object.keys(ethAddressesByToken).map(getEvmNetworkIdFromTokenId);
1584
1887
  const initialisingBalances = new Set(activeEvmNetworkIds);
1585
- const positiveBalanceNetworks = new Set(initialBalances?.map(b => b.networkId));
1888
+ const positiveBalanceNetworks = new Set(initialBalances?.map(b => b.evmNetworkId));
1586
1889
  const poll = async () => {
1587
1890
  if (!subscriptionActive) return;
1588
1891
  zeroBalanceSubscriptionIntervalCounter = (zeroBalanceSubscriptionIntervalCounter + 1) % 5;
1589
1892
  try {
1590
1893
  // fetch balance for each network sequentially to prevent creating a big queue of http requests (browser can only handle 2 at a time)
1591
1894
  for (const [tokenId, addresses] of Object.entries(ethAddressesByToken)) {
1592
- const evmNetworkId = chaindataProvider.networkIdFromTokenId(tokenId);
1895
+ const evmNetworkId = getEvmNetworkIdFromTokenId(tokenId);
1593
1896
 
1594
1897
  // a zero balance network is one that has initialised and does not have a positive balance
1595
1898
  const isZeroBalanceNetwork = !initialisingBalances.has(evmNetworkId) && !positiveBalanceNetworks.has(evmNetworkId);
@@ -1609,10 +1912,10 @@ const EvmNativeModule = hydrate => {
1609
1912
  log.error(balance.message, balance.networkId);
1610
1913
  initialisingBalances.delete(balance.networkId);
1611
1914
  } else {
1612
- if (balance.networkId) {
1613
- initialisingBalances.delete(balance.networkId);
1915
+ if (balance.evmNetworkId) {
1916
+ initialisingBalances.delete(balance.evmNetworkId);
1614
1917
  if (BigInt(balance.value) > 0n) {
1615
- positiveBalanceNetworks.add(balance.networkId);
1918
+ positiveBalanceNetworks.add(balance.evmNetworkId);
1616
1919
  }
1617
1920
  resultBalances.push(balance);
1618
1921
  }
@@ -1662,20 +1965,23 @@ const fetchBalances$2 = async (evmChainConnector, addressesByToken, tokens) => {
1662
1965
  if (!evmChainConnector) throw new Error(`This module requires an evm chain connector`);
1663
1966
  return Promise.all(Object.entries(addressesByToken).map(async ([tokenId, addresses]) => {
1664
1967
  const token = tokens[tokenId];
1665
- const networkId = token.networkId;
1666
- if (!networkId) throw new Error(`Token ${token.id} has no evm network`);
1667
- const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(networkId);
1668
- if (!publicClient) throw new Error(`Could not get rpc provider for evm network ${networkId}`);
1968
+ const evmNetworkId = token.evmNetwork?.id;
1969
+ if (!evmNetworkId) throw new Error(`Token ${token.id} has no evm network`);
1970
+ const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(evmNetworkId);
1971
+ if (!publicClient) throw new Error(`Could not get rpc provider for evm network ${evmNetworkId}`);
1669
1972
 
1670
1973
  // fetch all balances
1671
1974
  const freeBalances = await getFreeBalances(publicClient, addresses);
1672
1975
  const balanceResults = addresses.map((address, i) => {
1673
- if (freeBalances[i] === "error") return new EvmNativeBalanceError("Could not fetch balance ", networkId);
1976
+ if (freeBalances[i] === "error") return new EvmNativeBalanceError("Could not fetch balance ", evmNetworkId);
1674
1977
  return {
1675
1978
  source: "evm-native",
1676
1979
  status: "live",
1677
1980
  address: address,
1678
- networkId,
1981
+ multiChainId: {
1982
+ evmChainId: evmNetworkId
1983
+ },
1984
+ evmNetworkId,
1679
1985
  tokenId,
1680
1986
  value: freeBalances[i].toString()
1681
1987
  };
@@ -1728,7 +2034,7 @@ async function getFreeBalances(publicClient, addresses) {
1728
2034
  });
1729
2035
  } catch (err) {
1730
2036
  const errorMessage = util.hasOwnProperty(err, "shortMessage") ? err.shortMessage : util.hasOwnProperty(err, "message") ? err.message : err;
1731
- log.warn(`Failed to get balance from chain ${publicClient.chain.id} for ${ethAddresses.length} addresses: ${errorMessage}`);
2037
+ log.warn(`Failed to get balance from chain ${publicClient.chain?.id} for ${ethAddresses.length} addresses: ${errorMessage}`);
1732
2038
  return ethAddresses.map(() => "error");
1733
2039
  }
1734
2040
  }
@@ -2296,20 +2602,8 @@ const uniswapV2PairAbi = [{
2296
2602
  type: "function"
2297
2603
  }];
2298
2604
 
2299
- const moduleType$5 = "evm-uniswapv2";
2300
- const EvmUniswapV2TokenConfigSchema = z__default.default.strictObject({
2301
- contractAddress: chaindataProvider.EvmUniswapV2TokenSchema.shape.contractAddress,
2302
- ...TokenConfigBaseSchema.shape,
2303
- // on chaindata side these are fetched by a dedicated task
2304
- symbol0: chaindataProvider.EvmUniswapV2TokenSchema.shape.symbol0.optional(),
2305
- symbol1: chaindataProvider.EvmUniswapV2TokenSchema.shape.symbol1.optional(),
2306
- decimals0: chaindataProvider.EvmUniswapV2TokenSchema.shape.decimals0.optional(),
2307
- decimals1: chaindataProvider.EvmUniswapV2TokenSchema.shape.decimals1.optional(),
2308
- tokenAddress0: chaindataProvider.EvmUniswapV2TokenSchema.shape.tokenAddress0.optional(),
2309
- tokenAddress1: chaindataProvider.EvmUniswapV2TokenSchema.shape.tokenAddress1.optional(),
2310
- coingeckoId0: chaindataProvider.EvmUniswapV2TokenSchema.shape.coingeckoId0.optional(),
2311
- coingeckoId1: chaindataProvider.EvmUniswapV2TokenSchema.shape.coingeckoId1.optional()
2312
- });
2605
+ const moduleType$6 = "evm-uniswapv2";
2606
+ const evmUniswapV2TokenId = (chainId, contractAddress) => `${chainId}-evm-uniswapv2-${contractAddress}`.toLowerCase();
2313
2607
  const EvmUniswapV2Module = hydrate => {
2314
2608
  const {
2315
2609
  chainConnectors,
@@ -2318,16 +2612,19 @@ const EvmUniswapV2Module = hydrate => {
2318
2612
  const chainConnector = chainConnectors.evm;
2319
2613
  util$1.assert(chainConnector, "This module requires an evm chain connector");
2320
2614
  return {
2321
- ...DefaultBalanceModule(moduleType$5),
2322
- async fetchEvmChainMeta(_chainId) {
2615
+ ...DefaultBalanceModule(moduleType$6),
2616
+ async fetchEvmChainMeta(chainId) {
2617
+ const isTestnet = (await chaindataProvider$1.evmNetworkById(chainId))?.isTestnet || false;
2323
2618
  return {
2324
- miniMetadata: null,
2325
- extra: null
2619
+ isTestnet
2326
2620
  };
2327
2621
  },
2328
- async fetchEvmChainTokens(chainId, _chainMeta, moduleConfig, pools) {
2622
+ async fetchEvmChainTokens(chainId, chainMeta, moduleConfig) {
2623
+ const {
2624
+ isTestnet
2625
+ } = chainMeta;
2329
2626
  const tokens = {};
2330
- for (const tokenConfig of pools ?? []) {
2627
+ for (const tokenConfig of moduleConfig?.pools ?? []) {
2331
2628
  const {
2332
2629
  contractAddress,
2333
2630
  decimals,
@@ -2338,23 +2635,21 @@ const EvmUniswapV2Module = hydrate => {
2338
2635
  tokenAddress0,
2339
2636
  tokenAddress1,
2340
2637
  coingeckoId0,
2341
- coingeckoId1,
2342
- name
2638
+ coingeckoId1
2343
2639
  } = tokenConfig;
2344
2640
  if (!contractAddress || decimals === undefined || symbol0 === undefined || decimals0 === undefined || symbol1 === undefined || decimals1 === undefined || tokenAddress0 === undefined || tokenAddress1 === undefined) {
2345
2641
  log.warn("ignoring token on chain %s", chainId, tokenConfig);
2346
2642
  continue;
2347
2643
  }
2348
- const id = chaindataProvider.evmUniswapV2TokenId(chainId, contractAddress);
2644
+ const id = evmUniswapV2TokenId(chainId, contractAddress);
2349
2645
  const token = {
2350
2646
  id,
2351
2647
  type: "evm-uniswapv2",
2352
- platform: "ethereum",
2648
+ isTestnet,
2353
2649
  isDefault: tokenConfig.isDefault ?? false,
2354
2650
  symbol: `${symbol0 ?? "UNKNOWN"}/${symbol1 ?? "UNKNOWN"}`,
2355
- name: name ?? `${symbol0 ?? "UNKNOWN"}/${symbol1 ?? "UNKNOWN"}`,
2356
2651
  decimals,
2357
- logo: tokenConfig?.logo || chaindataProvider.getGithubTokenLogoUrl("uniswap"),
2652
+ logo: tokenConfig?.logo || chaindataProvider.githubTokenLogoUrl("uniswap"),
2358
2653
  symbol0,
2359
2654
  decimals0,
2360
2655
  symbol1,
@@ -2364,10 +2659,13 @@ const EvmUniswapV2Module = hydrate => {
2364
2659
  tokenAddress1,
2365
2660
  coingeckoId0,
2366
2661
  coingeckoId1,
2367
- networkId: chainId
2662
+ evmNetwork: {
2663
+ id: chainId
2664
+ }
2368
2665
  };
2369
2666
  if (tokenConfig?.symbol) token.symbol = tokenConfig?.symbol;
2370
2667
  if (tokenConfig?.coingeckoId) token.coingeckoId = tokenConfig?.coingeckoId;
2668
+ if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName;
2371
2669
  if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf;
2372
2670
  if (tokenConfig?.noDiscovery) token.noDiscovery = tokenConfig?.noDiscovery;
2373
2671
  tokens[token.id] = token;
@@ -2383,9 +2681,9 @@ const EvmUniswapV2Module = hydrate => {
2383
2681
  const initDelay = 1_500; // 1_500ms == 1.5 seconds
2384
2682
 
2385
2683
  const initialBalancesByNetwork = initialBalances?.reduce((result, b) => {
2386
- if (!b.networkId) return result;
2387
- if (!result[b.networkId]) result[b.networkId] = {};
2388
- result[b.networkId][getBalanceId(b)] = b;
2684
+ if (!b.evmNetworkId) return result;
2685
+ if (!result[b.evmNetworkId]) result[b.evmNetworkId] = {};
2686
+ result[b.evmNetworkId][getBalanceId(b)] = b;
2389
2687
  return result;
2390
2688
  }, {});
2391
2689
  const cache = new Map(Object.entries(initialBalancesByNetwork ?? {}));
@@ -2393,8 +2691,8 @@ const EvmUniswapV2Module = hydrate => {
2393
2691
  // for chains with a zero balance we only call fetchBalances once every 5 subscriptionIntervals
2394
2692
  // if subscriptionInterval is 6 seconds, this means we only poll chains with a zero balance every 30 seconds
2395
2693
  let zeroBalanceSubscriptionIntervalCounter = 0;
2396
- const evmNetworks = await chaindataProvider$1.getNetworksMapById("ethereum");
2397
- const tokens = await chaindataProvider$1.getTokensMapById();
2694
+ const evmNetworks = await chaindataProvider$1.evmNetworksById();
2695
+ const tokens = await chaindataProvider$1.tokensById();
2398
2696
  const poll = async () => {
2399
2697
  if (!subscriptionActive) return;
2400
2698
  zeroBalanceSubscriptionIntervalCounter = (zeroBalanceSubscriptionIntervalCounter + 1) % 5;
@@ -2437,20 +2735,20 @@ const EvmUniswapV2Module = hydrate => {
2437
2735
  },
2438
2736
  async fetchBalances(addressesByToken) {
2439
2737
  if (!chainConnectors.evm) throw new Error(`This module requires an evm chain connector`);
2440
- const evmNetworks = await chaindataProvider$1.getNetworksMapById("ethereum");
2441
- const tokens = await chaindataProvider$1.getTokensMapById();
2738
+ const evmNetworks = await chaindataProvider$1.evmNetworksById();
2739
+ const tokens = await chaindataProvider$1.tokensById();
2442
2740
  return fetchBalances$1(chainConnectors.evm, evmNetworks, tokens, addressesByToken);
2443
2741
  }
2444
2742
  };
2445
2743
  };
2446
2744
  const fetchBalances$1 = async (evmChainConnector, evmNetworks, tokens, addressesByToken) => {
2447
2745
  const addressesByTokenGroupedByEvmNetwork = groupAddressesByTokenByEvmNetwork(addressesByToken, tokens);
2448
- const balances = (await Promise.allSettled(Object.entries(addressesByTokenGroupedByEvmNetwork).map(async ([networkId, addressesByToken]) => {
2746
+ const balances = (await Promise.allSettled(Object.entries(addressesByTokenGroupedByEvmNetwork).map(async ([evmNetworkId, addressesByToken]) => {
2449
2747
  if (!evmChainConnector) throw new Error(`This module requires an evm chain connector`);
2450
- const evmNetwork = evmNetworks[networkId];
2451
- if (!evmNetwork) throw new Error(`Evm network ${networkId} not found`);
2452
- const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(networkId);
2453
- if (!publicClient) throw new Error(`Could not get rpc provider for evm network ${networkId}`);
2748
+ const evmNetwork = evmNetworks[evmNetworkId];
2749
+ if (!evmNetwork) throw new Error(`Evm network ${evmNetworkId} not found`);
2750
+ const publicClient = await evmChainConnector.getPublicClientForEvmNetwork(evmNetworkId);
2751
+ if (!publicClient) throw new Error(`Could not get rpc provider for evm network ${evmNetworkId}`);
2454
2752
  const tokensAndAddresses = Object.entries(addressesByToken).reduce((tokensAndAddresses, [tokenId, addresses]) => {
2455
2753
  const token = tokens[tokenId];
2456
2754
  if (!token) {
@@ -2472,7 +2770,10 @@ const fetchBalances$1 = async (evmChainConnector, evmNetworks, tokens, addresses
2472
2770
  source: "evm-uniswapv2",
2473
2771
  status: "live",
2474
2772
  address: address,
2475
- networkId,
2773
+ multiChainId: {
2774
+ evmChainId: evmNetwork.id
2775
+ },
2776
+ evmNetworkId,
2476
2777
  tokenId: token.id,
2477
2778
  values: await getPoolBalance(publicClient, token.contractAddress, address)
2478
2779
  }));
@@ -2508,7 +2809,7 @@ function groupAddressesByTokenByEvmNetwork(addressesByToken, tokens) {
2508
2809
  log.error(`Token ${tokenId} not found`);
2509
2810
  return byChain;
2510
2811
  }
2511
- const chainId = token.networkId;
2812
+ const chainId = token.evmNetwork?.id;
2512
2813
  if (!chainId) {
2513
2814
  log.error(`Token ${tokenId} has no evm network`);
2514
2815
  return byChain;
@@ -2583,170 +2884,6 @@ async function getPoolBalance(publicClient, contractAddress, accountAddress) {
2583
2884
  }
2584
2885
  }
2585
2886
 
2586
- const libVersion = pkg.version;
2587
-
2588
- // cache the promise so it can be shared across multiple calls
2589
- const CACHE_GET_SPEC_VERSION = new Map();
2590
- const fetchSpecVersion = async (chainConnector, networkId) => {
2591
- const {
2592
- specVersion
2593
- } = await chainConnector.send(networkId, "state_getRuntimeVersion", [], true);
2594
- return specVersion;
2595
- };
2596
-
2597
- /**
2598
- * fetches the spec version of a network. current request is cached in case of multiple calls (all balance modules will kick in at once)
2599
- */
2600
- const getSpecVersion = async (chainConnector, networkId) => {
2601
- if (CACHE_GET_SPEC_VERSION.has(networkId)) return CACHE_GET_SPEC_VERSION.get(networkId);
2602
- const pResult = fetchSpecVersion(chainConnector, networkId);
2603
- CACHE_GET_SPEC_VERSION.set(networkId, pResult);
2604
- try {
2605
- return await pResult;
2606
- } catch (cause) {
2607
- throw new Error(`Failed to fetch specVersion for network ${networkId}`, {
2608
- cause
2609
- });
2610
- } finally {
2611
- CACHE_GET_SPEC_VERSION.delete(networkId);
2612
- }
2613
- };
2614
-
2615
- // share requests as all modules will call this at once
2616
- const CACHE$1 = new Map();
2617
- const getMetadataRpc = async (chainConnector, networkId) => {
2618
- if (CACHE$1.has(networkId)) return CACHE$1.get(networkId);
2619
- const pResult = sapi.fetchBestMetadata((method, params, isCacheable) => chainConnector.send(networkId, method, params, isCacheable, {
2620
- expectErrors: true
2621
- }), true // allow fallback to 14 as modules dont use any v15 or v16 specifics yet
2622
- );
2623
- CACHE$1.set(networkId, pResult);
2624
- try {
2625
- return await pResult;
2626
- } catch (cause) {
2627
- if (util.isAbortError(cause)) throw cause;
2628
- throw new Error(`Failed to fetch metadataRpc for network ${networkId}`, {
2629
- cause
2630
- });
2631
- } finally {
2632
- CACHE$1.delete(networkId);
2633
- }
2634
- };
2635
-
2636
- // share requests as all modules will call this at once
2637
- const CACHE = new Map();
2638
-
2639
- // ensures we dont fetch miniMetadatas on more than 4 chains at once
2640
- const POOL = new PQueue__default.default({
2641
- concurrency: 4
2642
- });
2643
- const getMiniMetadatas = async (chainConnector, chaindataProvider, networkId, specVersion, signal) => {
2644
- if (CACHE.has(networkId)) return CACHE.get(networkId);
2645
- 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
2646
- );
2647
- const pResult = POOL.add(() => fetchMiniMetadatas(chainConnector, chaindataProvider, networkId, specVersion), {
2648
- signal
2649
- });
2650
- CACHE.set(networkId, pResult);
2651
- try {
2652
- return await pResult;
2653
- } catch (cause) {
2654
- if (util.isAbortError(cause)) throw cause;
2655
- throw new Error(`Failed to fetch metadataRpc for network ${networkId}`, {
2656
- cause
2657
- });
2658
- } finally {
2659
- CACHE.delete(networkId);
2660
- }
2661
- };
2662
- const DotBalanceModuleTypeSchema = z__default.default.keyof(chaindataProvider.DotNetworkBalancesConfigSchema);
2663
- const fetchMiniMetadatas = async (chainConnector, chaindataProvider, chainId, specVersion, signal) => {
2664
- const start = performance.now();
2665
- log.info("[miniMetadata] fetching minimetadatas for %s", chainId);
2666
- try {
2667
- const metadataRpc = await getMetadataRpc(chainConnector, chainId);
2668
- signal?.throwIfAborted();
2669
- const chainConnectors = {
2670
- substrate: chainConnector,
2671
- evm: {} // wont be used but workarounds error for module creation
2672
- };
2673
- const modules = defaultBalanceModules.map(mod => mod({
2674
- chainConnectors,
2675
- chaindataProvider
2676
- })).filter(mod => DotBalanceModuleTypeSchema.safeParse(mod.type).success);
2677
- return Promise.all(modules.map(async mod => {
2678
- const source = mod.type;
2679
- const chain = await chaindataProvider.getNetworkById(chainId, "polkadot");
2680
- const balancesConfig = chain?.balancesConfig?.[mod.type];
2681
- const chainMeta = await mod.fetchSubstrateChainMeta(chainId, balancesConfig,
2682
- // TODO better typing
2683
- metadataRpc);
2684
- return {
2685
- id: deriveMiniMetadataId({
2686
- source,
2687
- chainId,
2688
- specVersion,
2689
- libVersion
2690
- }),
2691
- source,
2692
- chainId,
2693
- specVersion,
2694
- libVersion,
2695
- data: chainMeta?.miniMetadata ?? null,
2696
- extra: chainMeta?.extra ?? null
2697
- };
2698
- }));
2699
- } finally {
2700
- log.debug("[miniMetadata] updated miniMetadatas for %s in %sms", chainId, (performance.now() - start).toFixed(2));
2701
- }
2702
- };
2703
-
2704
- const getUpdatedMiniMetadatas = async (chainConnector, chaindataProvider, chainId, specVersion, signal) => {
2705
- const miniMetadatas = await getMiniMetadatas(chainConnector, chaindataProvider, chainId, specVersion, signal);
2706
- signal?.throwIfAborted();
2707
- await db.transaction("readwrite", "miniMetadatas", async tx => {
2708
- await tx.miniMetadatas.where({
2709
- chainId
2710
- }).delete();
2711
- await tx.miniMetadatas.bulkPut(miniMetadatas);
2712
- });
2713
- return miniMetadatas;
2714
- };
2715
-
2716
- const getMiniMetadata = async (chaindataProvider, chainConnector, chainId, source, signal) => {
2717
- const specVersion = await getSpecVersion(chainConnector, chainId);
2718
- signal?.throwIfAborted();
2719
- const miniMetadataId = deriveMiniMetadataId({
2720
- source,
2721
- chainId,
2722
- specVersion,
2723
- libVersion
2724
- });
2725
-
2726
- // lookup local ones
2727
- const [dbMiniMetadata, ghMiniMetadata] = await Promise.all([db.miniMetadatas.get(miniMetadataId), chaindataProvider.miniMetadataById(miniMetadataId)]);
2728
- signal?.throwIfAborted();
2729
- const miniMetadata = dbMiniMetadata ?? ghMiniMetadata;
2730
- if (miniMetadata) return miniMetadata;
2731
-
2732
- // update from live chain metadata and persist locally
2733
- const miniMetadatas = await getUpdatedMiniMetadatas(chainConnector, chaindataProvider, chainId, specVersion, signal);
2734
- signal?.throwIfAborted();
2735
- const found = miniMetadatas.find(m => m.id === miniMetadataId);
2736
- if (!found) {
2737
- log.warn("MiniMetadata not found in updated miniMetadatas", {
2738
- source,
2739
- chainId,
2740
- specVersion,
2741
- libVersion,
2742
- miniMetadataId,
2743
- miniMetadatas
2744
- });
2745
- throw new Error(`MiniMetadata not found for ${source} on ${chainId}`);
2746
- }
2747
- return found;
2748
- };
2749
-
2750
2887
  /**
2751
2888
  * Wraps a BalanceModule's fetch/subscribe methods with a single `balances` method.
2752
2889
  * This `balances` method will subscribe if a callback parameter is provided, or otherwise fetch.
@@ -2762,16 +2899,44 @@ async function balances(balanceModule, addressesByToken, callback) {
2762
2899
  return await balanceModule.fetchBalances(addressesByToken);
2763
2900
  }
2764
2901
 
2765
- // TODO remove this one in favor of the network specific one below
2902
+ /**
2903
+ * Given a `moduleType` and a `chain` from a chaindataProvider, this function will find the chainMeta
2904
+ * associated with the given balanceModule for the given chain.
2905
+ */
2906
+ const findChainMeta = (miniMetadatas, moduleType, chain) => {
2907
+ if (!chain) return [undefined, undefined];
2908
+ if (!chain.specName) return [undefined, undefined];
2909
+ if (!chain.specVersion) return [undefined, undefined];
2910
+
2911
+ // TODO: This is spaghetti to import this here, it should be injected into each balance module or something.
2912
+ const metadataId = deriveMiniMetadataId({
2913
+ source: moduleType,
2914
+ chainId: chain.id,
2915
+ specName: chain.specName,
2916
+ specVersion: chain.specVersion,
2917
+ balancesConfig: JSON.stringify(chain.balancesConfig?.find(config => config.moduleType === moduleType)?.moduleConfig ?? {})
2918
+ });
2919
+
2920
+ // TODO: Fix this (needs to fetch miniMetadata without being async)
2921
+ const miniMetadata = miniMetadatas.get(metadataId);
2922
+ const chainMeta = miniMetadata ? {
2923
+ miniMetadata: miniMetadata.data,
2924
+ metadataVersion: miniMetadata.version,
2925
+ ...JSON.parse(miniMetadata.extra)
2926
+ } : undefined;
2927
+ return [chainMeta, miniMetadata];
2928
+ };
2929
+
2766
2930
  const buildStorageCoders = ({
2767
2931
  chainIds,
2768
2932
  chains,
2769
2933
  miniMetadatas,
2934
+ moduleType,
2770
2935
  coders
2771
2936
  }) => new Map([...chainIds].flatMap(chainId => {
2772
2937
  const chain = chains[chainId];
2773
2938
  if (!chain) return [];
2774
- const miniMetadata = miniMetadatas.get(chainId); // findMiniMetadata<TBalanceModule>(miniMetadatas, moduleType, chain)
2939
+ const [, miniMetadata] = findChainMeta(miniMetadatas, moduleType, chain);
2775
2940
  if (!miniMetadata) return [];
2776
2941
  if (!miniMetadata.data) return [];
2777
2942
  const metadata = scale.unifyMetadata(scale.decAnyMetadata(miniMetadata.data));
@@ -2794,28 +2959,6 @@ const buildStorageCoders = ({
2794
2959
  return [];
2795
2960
  }
2796
2961
  }));
2797
- const buildNetworkStorageCoders = (chainId, miniMetadata, coders) => {
2798
- if (!miniMetadata.data) return null;
2799
- const metadata = scale.unifyMetadata(scale.decAnyMetadata(miniMetadata.data));
2800
- try {
2801
- const scaleBuilder = scale.getDynamicBuilder(scale.getLookupFn(metadata));
2802
- const builtCoders = Object.fromEntries(Object.entries(coders).flatMap(([key, moduleMethodOrFn]) => {
2803
- const [module, method] = typeof moduleMethodOrFn === "function" ? moduleMethodOrFn({
2804
- chainId
2805
- }) : moduleMethodOrFn;
2806
- try {
2807
- return [[key, scaleBuilder.buildStorage(module, method)]];
2808
- } catch (cause) {
2809
- log.trace(`Failed to build SCALE coder for chain ${chainId} (${module}::${method})`, cause);
2810
- return [];
2811
- }
2812
- }));
2813
- return builtCoders;
2814
- } catch (cause) {
2815
- log.error(`Failed to build SCALE coders for chain ${chainId} (${JSON.stringify(coders)})`, cause);
2816
- }
2817
- return null;
2818
- };
2819
2962
 
2820
2963
  /**
2821
2964
  * Decodes & unwraps outputs and errors of a given result, contract, and method.
@@ -2897,7 +3040,7 @@ const detectTransferMethod = metadataRpc => {
2897
3040
  return hasDeprecatedTransferCall ? "transfer" : "transfer_allow_death";
2898
3041
  };
2899
3042
 
2900
- const getUniqueChainIds = (addressesByToken, tokens) => [...new Set(Object.keys(addressesByToken).map(tokenId => tokens[tokenId]?.networkId).flatMap(chainId => chainId ? [chainId] : []))];
3043
+ const getUniqueChainIds = (addressesByToken, tokens) => [...new Set(Object.keys(addressesByToken).map(tokenId => tokens[tokenId]?.chain?.id).flatMap(chainId => chainId ? [chainId] : []))];
2901
3044
 
2902
3045
  const makeContractCaller = ({
2903
3046
  chainConnector,
@@ -3006,15 +3149,8 @@ const decompress = data => {
3006
3149
  return JSON.parse(decompressed);
3007
3150
  };
3008
3151
 
3009
- const moduleType$4 = "substrate-assets";
3010
- const SubAssetsTokenConfigSchema = z__default.default.strictObject({
3011
- assetId: chaindataProvider.SubAssetsTokenSchema.shape.assetId,
3012
- ...TokenConfigBaseSchema.shape
3013
- });
3014
- const UNSUPPORTED_CHAIN_META$3 = {
3015
- miniMetadata: null,
3016
- extra: null
3017
- };
3152
+ const moduleType$5 = "substrate-assets";
3153
+ const subAssetTokenId = (chainId, assetId, tokenSymbol) => `${chainId}-substrate-assets-${assetId}-${tokenSymbol}`.toLowerCase().replace(/ /g, "-");
3018
3154
  const SubAssetsModule = hydrate => {
3019
3155
  const {
3020
3156
  chainConnectors,
@@ -3023,10 +3159,16 @@ const SubAssetsModule = hydrate => {
3023
3159
  const chainConnector = chainConnectors.substrate;
3024
3160
  util$1.assert(chainConnector, "This module requires a substrate chain connector");
3025
3161
  return {
3026
- ...DefaultBalanceModule(moduleType$4),
3027
- // TODO make synchronous at the module definition level ?
3162
+ ...DefaultBalanceModule(moduleType$5),
3028
3163
  async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) {
3029
- if (!metadataRpc) return UNSUPPORTED_CHAIN_META$3;
3164
+ const isTestnet = (await chaindataProvider$1.chainById(chainId))?.isTestnet || false;
3165
+ if (metadataRpc === undefined) return {
3166
+ isTestnet
3167
+ };
3168
+ if ((moduleConfig?.tokens ?? []).length < 1) return {
3169
+ isTestnet
3170
+ };
3171
+ const metadataVersion = scale.getMetadataVersion(metadataRpc);
3030
3172
  const metadata = scale.decAnyMetadata(metadataRpc);
3031
3173
  scale.compactMetadata(metadata, [{
3032
3174
  pallet: "Assets",
@@ -3034,98 +3176,82 @@ const SubAssetsModule = hydrate => {
3034
3176
  }]);
3035
3177
  const miniMetadata = scale.encodeMetadata(metadata);
3036
3178
  return {
3179
+ isTestnet,
3037
3180
  miniMetadata,
3038
- extra: null
3181
+ metadataVersion
3039
3182
  };
3040
3183
  },
3041
- async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig, tokens) {
3042
- if (!tokens?.length) return {};
3184
+ async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) {
3185
+ if ((moduleConfig?.tokens ?? []).length < 1) return {};
3043
3186
  const {
3044
- miniMetadata
3187
+ isTestnet,
3188
+ miniMetadata,
3189
+ metadataVersion
3045
3190
  } = chainMeta;
3046
- if (!miniMetadata) return {};
3191
+ if (miniMetadata === undefined || metadataVersion === undefined) return {};
3192
+ if (metadataVersion < 14) return {};
3047
3193
  const metadata = scale.unifyMetadata(scale.decAnyMetadata(miniMetadata));
3048
3194
  const scaleBuilder = scale.getDynamicBuilder(scale.getLookupFn(metadata));
3049
3195
  const assetCoder = scaleBuilder.buildStorage("Assets", "Asset");
3050
3196
  const metadataCoder = scaleBuilder.buildStorage("Assets", "Metadata");
3051
- const tokenList = {};
3052
- for (const tokenConfig of tokens ?? []) {
3197
+ const tokens = {};
3198
+ for (const tokenConfig of moduleConfig?.tokens ?? []) {
3053
3199
  try {
3054
- const assetId = String(tokenConfig.assetId);
3200
+ const assetId = typeof tokenConfig.assetId === "number" ? tokenConfig.assetId.toString() : tokenConfig.assetId;
3055
3201
  const assetStateKey = tryEncode(assetCoder, BigInt(assetId)) ?? tryEncode(assetCoder, assetId);
3056
3202
  const metadataStateKey = tryEncode(metadataCoder, BigInt(assetId)) ?? tryEncode(metadataCoder, assetId);
3057
3203
  if (assetStateKey === null || metadataStateKey === null) throw new Error(`Failed to encode stateKey for asset ${assetId} on chain ${chainId}`);
3058
3204
  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)]);
3059
3205
  const existentialDeposit = assetsAsset?.min_balance?.toString?.() ?? "0";
3060
3206
  const symbol = assetsMetadata?.symbol?.asText?.() ?? "Unit";
3061
- const name = assetsMetadata?.name?.asText?.() ?? symbol;
3062
3207
  const decimals = assetsMetadata?.decimals ?? 0;
3063
3208
  const isFrozen = assetsMetadata?.is_frozen ?? false;
3064
- const id = chaindataProvider.subAssetTokenId(chainId, assetId);
3209
+ const id = subAssetTokenId(chainId, assetId, symbol);
3065
3210
  const token = {
3066
3211
  id,
3067
3212
  type: "substrate-assets",
3068
- platform: "polkadot",
3213
+ isTestnet,
3069
3214
  isDefault: tokenConfig?.isDefault ?? true,
3070
- symbol: tokenConfig?.symbol ?? symbol,
3071
- name: tokenConfig?.name ?? name,
3215
+ symbol,
3072
3216
  decimals,
3073
- logo: tokenConfig?.logo,
3217
+ logo: tokenConfig?.logo || chaindataProvider.githubTokenLogoUrl(id),
3074
3218
  existentialDeposit,
3075
3219
  assetId,
3076
3220
  isFrozen,
3077
- networkId: chainId
3221
+ chain: {
3222
+ id: chainId
3223
+ }
3078
3224
  };
3079
3225
  if (tokenConfig?.symbol) {
3080
3226
  token.symbol = tokenConfig?.symbol;
3081
- token.id = chaindataProvider.subAssetTokenId(chainId, assetId);
3227
+ token.id = subAssetTokenId(chainId, assetId, token.symbol);
3082
3228
  }
3083
3229
  if (tokenConfig?.coingeckoId) token.coingeckoId = tokenConfig?.coingeckoId;
3230
+ if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName;
3084
3231
  if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf;
3085
- tokenList[token.id] = token;
3232
+ tokens[token.id] = token;
3086
3233
  } catch (error) {
3087
3234
  log.error(`Failed to build substrate-assets token ${tokenConfig.assetId} (${tokenConfig.symbol}) on ${chainId}`, error);
3088
3235
  continue;
3089
3236
  }
3090
3237
  }
3091
- return tokenList;
3238
+ return tokens;
3092
3239
  },
3093
3240
  // TODO: Don't create empty subscriptions
3094
3241
  async subscribeBalances({
3095
3242
  addressesByToken
3096
3243
  }, callback) {
3097
- const byNetwork = lodash.keys(addressesByToken).reduce((acc, tokenId) => {
3098
- const networkId = chaindataProvider.parseSubAssetTokenId(tokenId).networkId;
3099
- if (!acc[networkId]) acc[networkId] = {};
3100
- acc[networkId][tokenId] = addressesByToken[tokenId];
3101
- return acc;
3102
- }, {});
3103
- const controller = new AbortController();
3104
- const pUnsubs = Promise.all(lodash.toPairs(byNetwork).map(async ([networkId, addressesByToken]) => {
3105
- try {
3106
- const queries = await buildNetworkQueries$2(networkId, chainConnector, chaindataProvider$1, addressesByToken, controller.signal);
3107
- if (controller.signal.aborted) return () => {};
3108
- const stateHelper = new RpcStateQueryHelper(chainConnector, queries);
3109
- return await stateHelper.subscribe((error, result) => {
3110
- if (error) return callback(error);
3111
- const balances = result?.filter(b => b !== null) ?? [];
3112
- if (balances.length > 0) callback(null, new Balances(balances));
3113
- });
3114
- } catch (err) {
3115
- if (!util.isAbortError(err)) log.error(`Failed to subscribe balances for network ${networkId}`, err);
3116
- return () => {};
3117
- }
3118
- }));
3119
- return () => {
3120
- pUnsubs.then(unsubs => {
3121
- unsubs.forEach(unsubscribe => unsubscribe());
3122
- });
3123
- controller.abort();
3124
- };
3244
+ const queries = await buildQueries$4(chaindataProvider$1, addressesByToken);
3245
+ const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe((error, result) => {
3246
+ if (error) return callback(error);
3247
+ const balances = result?.filter(b => b !== null) ?? [];
3248
+ if (balances.length > 0) callback(null, new Balances(balances));
3249
+ });
3250
+ return unsubscribe;
3125
3251
  },
3126
3252
  async fetchBalances(addressesByToken) {
3127
3253
  util$1.assert(chainConnectors.substrate, "This module requires a substrate chain connector");
3128
- const queries = await buildQueries$3(chainConnector, chaindataProvider$1, addressesByToken);
3254
+ const queries = await buildQueries$4(chaindataProvider$1, addressesByToken);
3129
3255
  const result = await new RpcStateQueryHelper(chainConnectors.substrate, queries).fetch();
3130
3256
  const balances = result?.filter(b => b !== null) ?? [];
3131
3257
  return new Balances(balances);
@@ -3146,10 +3272,11 @@ const SubAssetsModule = hydrate => {
3146
3272
  transferMethod,
3147
3273
  userExtensions
3148
3274
  }) {
3149
- const token = await chaindataProvider$1.getTokenById(tokenId, "substrate-assets");
3275
+ const token = await chaindataProvider$1.tokenById(tokenId);
3150
3276
  util$1.assert(token, `Token ${tokenId} not found in store`);
3151
- const chainId = token.networkId;
3152
- const chain = await chaindataProvider$1.getNetworkById(chainId, "polkadot");
3277
+ if (token.type !== "substrate-assets") throw new Error(`This module doesn't handle tokens of type ${token.type}`);
3278
+ const chainId = token.chain.id;
3279
+ const chain = await chaindataProvider$1.chainById(chainId);
3153
3280
  util$1.assert(chain?.genesisHash, `Chain ${chainId} not found in store`);
3154
3281
  const {
3155
3282
  genesisHash
@@ -3194,16 +3321,23 @@ const SubAssetsModule = hydrate => {
3194
3321
  }
3195
3322
  };
3196
3323
  };
3197
- async function buildNetworkQueries$2(networkId, chainConnector, chaindataProvider, addressesByToken, signal) {
3198
- const miniMetadata = await getMiniMetadata(chaindataProvider, chainConnector, networkId, moduleType$4, signal);
3199
- const chain = await chaindataProvider.getNetworkById(networkId, "polkadot");
3200
- const tokensById = await chaindataProvider.getTokensMapById();
3201
- signal?.throwIfAborted();
3202
- const networkStorageCoders = buildNetworkStorageCoders(networkId, miniMetadata, {
3203
- storage: ["Assets", "Account"]
3324
+ async function buildQueries$4(chaindataProvider, addressesByToken) {
3325
+ const allChains = await chaindataProvider.chainsById();
3326
+ const tokens = await chaindataProvider.tokensById();
3327
+ const miniMetadatas = new Map((await db.miniMetadatas.toArray()).map(miniMetadata => [miniMetadata.id, miniMetadata]));
3328
+ const uniqueChainIds = getUniqueChainIds(addressesByToken, tokens);
3329
+ const chains = Object.fromEntries(uniqueChainIds.map(chainId => [chainId, allChains[chainId]]));
3330
+ const chainStorageCoders = buildStorageCoders({
3331
+ chainIds: uniqueChainIds,
3332
+ chains,
3333
+ miniMetadatas,
3334
+ moduleType: "substrate-assets",
3335
+ coders: {
3336
+ storage: ["Assets", "Account"]
3337
+ }
3204
3338
  });
3205
3339
  return Object.entries(addressesByToken).flatMap(([tokenId, addresses]) => {
3206
- const token = tokensById[tokenId];
3340
+ const token = tokens[tokenId];
3207
3341
  if (!token) {
3208
3342
  log.warn(`Token ${tokenId} not found`);
3209
3343
  return [];
@@ -3212,22 +3346,27 @@ async function buildNetworkQueries$2(networkId, chainConnector, chaindataProvide
3212
3346
  log.debug(`This module doesn't handle tokens of type ${token.type}`);
3213
3347
  return [];
3214
3348
  }
3215
- //
3349
+ const chainId = token.chain?.id;
3350
+ if (!chainId) {
3351
+ log.warn(`Token ${tokenId} has no chain`);
3352
+ return [];
3353
+ }
3354
+ const chain = chains[chainId];
3216
3355
  if (!chain) {
3217
- log.warn(`Chain ${networkId} for token ${tokenId} not found`);
3356
+ log.warn(`Chain ${chainId} for token ${tokenId} not found`);
3218
3357
  return [];
3219
3358
  }
3220
3359
  return addresses.flatMap(address => {
3221
- const scaleCoder = networkStorageCoders?.storage;
3222
- const stateKey = tryEncode(scaleCoder, Number(token.assetId), address) ?? tryEncode(scaleCoder, BigInt(token.assetId), address);
3360
+ const scaleCoder = chainStorageCoders.get(chainId)?.storage;
3361
+ const stateKey = tryEncode(scaleCoder, BigInt(token.assetId), address) ?? tryEncode(scaleCoder, token.assetId, address);
3223
3362
  if (!stateKey) {
3224
- log.warn(`Invalid assetId / address in ${networkId} storage query ${token.assetId} / ${address}`);
3363
+ log.warn(`Invalid assetId / address in ${chainId} storage query ${token.assetId} / ${address}`);
3225
3364
  return [];
3226
3365
  }
3227
3366
  const decodeResult = change => {
3228
3367
  /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
3229
3368
 
3230
- const decoded = scale.decodeScale(scaleCoder, change, `Failed to decode substrate-assets balance on chain ${networkId}`) ?? {
3369
+ const decoded = scale.decodeScale(scaleCoder, change, `Failed to decode substrate-assets balance on chain ${chainId}`) ?? {
3231
3370
  balance: 0n,
3232
3371
  status: {
3233
3372
  type: "Liquid"
@@ -3261,30 +3400,22 @@ async function buildNetworkQueries$2(networkId, chainConnector, chaindataProvide
3261
3400
  source: "substrate-assets",
3262
3401
  status: "live",
3263
3402
  address,
3264
- networkId,
3403
+ multiChainId: {
3404
+ subChainId: chainId
3405
+ },
3406
+ chainId,
3265
3407
  tokenId: token.id,
3266
3408
  values: balanceValues
3267
3409
  };
3268
3410
  };
3269
3411
  return {
3270
- chainId: networkId,
3412
+ chainId,
3271
3413
  stateKey,
3272
3414
  decodeResult
3273
3415
  };
3274
3416
  });
3275
3417
  });
3276
3418
  }
3277
- async function buildQueries$3(chainConnector, chaindataProvider$1, addressesByToken, signal) {
3278
- const byNetwork = lodash.keys(addressesByToken).reduce((acc, tokenId) => {
3279
- const networkId = chaindataProvider.parseSubAssetTokenId(tokenId).networkId;
3280
- if (!acc[networkId]) acc[networkId] = {};
3281
- acc[networkId][tokenId] = addressesByToken[tokenId];
3282
- return acc;
3283
- }, {});
3284
- return (await Promise.all(lodash.toPairs(byNetwork).map(([networkId, addressesByToken]) => {
3285
- return buildNetworkQueries$2(networkId, chainConnector, chaindataProvider$1, addressesByToken, signal);
3286
- }))).flat();
3287
- }
3288
3419
  // NOTE: Different chains need different formats for assetId when encoding the stateKey
3289
3420
  // E.g. Polkadot Asset Hub needs it to be a string, Astar needs it to be a bigint
3290
3421
  //
@@ -3297,16 +3428,9 @@ const tryEncode = (scaleCoder, ...args) => {
3297
3428
  }
3298
3429
  };
3299
3430
 
3300
- const moduleType$3 = "substrate-foreignassets";
3301
- const UNSUPPORTED_CHAIN_META$2 = {
3302
- miniMetadata: null,
3303
- extra: null
3304
- };
3305
- const SubForeignAssetsTokenConfigSchema = z__default.default.strictObject({
3306
- onChainId: chaindataProvider.SubForeignAssetsTokenSchema.shape.onChainId,
3307
- ...TokenConfigBaseSchema.shape
3308
- });
3309
- const SubForeignAssetsModule = hydrate => {
3431
+ const moduleType$4 = "substrate-equilibrium";
3432
+ const subEquilibriumTokenId = (chainId, tokenSymbol) => `${chainId}-substrate-equilibrium-${tokenSymbol}`.toLowerCase().replace(/ /g, "-");
3433
+ const SubEquilibriumModule = hydrate => {
3310
3434
  const {
3311
3435
  chainConnectors,
3312
3436
  chaindataProvider: chaindataProvider$1
@@ -3314,113 +3438,374 @@ const SubForeignAssetsModule = hydrate => {
3314
3438
  const chainConnector = chainConnectors.substrate;
3315
3439
  util$1.assert(chainConnector, "This module requires a substrate chain connector");
3316
3440
  return {
3317
- ...DefaultBalanceModule(moduleType$3),
3441
+ ...DefaultBalanceModule(moduleType$4),
3318
3442
  async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) {
3319
- if (metadataRpc === undefined) return UNSUPPORTED_CHAIN_META$2;
3443
+ const isTestnet = (await chaindataProvider$1.chainById(chainId))?.isTestnet || false;
3444
+ if (metadataRpc === undefined) return {
3445
+ isTestnet
3446
+ };
3447
+ if (moduleConfig?.disable !== false) return {
3448
+ isTestnet
3449
+ }; // default to disabled
3450
+
3320
3451
  const metadataVersion = scale.getMetadataVersion(metadataRpc);
3321
- if (metadataVersion < 14) return UNSUPPORTED_CHAIN_META$2;
3322
3452
  const metadata = scale.decAnyMetadata(metadataRpc);
3323
3453
  scale.compactMetadata(metadata, [{
3324
- pallet: "ForeignAssets",
3325
- items: ["Account", "Asset", "Metadata"]
3454
+ pallet: "EqAssets",
3455
+ items: ["Assets"]
3456
+ }, {
3457
+ pallet: "System",
3458
+ items: ["Account"]
3326
3459
  }]);
3327
3460
  const miniMetadata = scale.encodeMetadata(metadata);
3328
3461
  return {
3462
+ isTestnet,
3329
3463
  miniMetadata,
3330
- extra: null
3464
+ metadataVersion
3331
3465
  };
3332
3466
  },
3333
- async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig, tokens) {
3334
- if (!tokens?.length) return {};
3467
+ async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) {
3468
+ // default to disabled
3469
+ if (moduleConfig?.disable !== false) return {};
3335
3470
  const {
3336
- miniMetadata
3471
+ isTestnet,
3472
+ miniMetadata,
3473
+ metadataVersion
3337
3474
  } = chainMeta;
3338
- if (!miniMetadata) return {};
3339
- const metadata = scale.decAnyMetadata(miniMetadata);
3340
- const unifiedMetadata = scale.unifyMetadata(metadata);
3341
- const scaleBuilder = scale.getDynamicBuilder(scale.getLookupFn(unifiedMetadata));
3342
- const assetCoder = scaleBuilder.buildStorage("ForeignAssets", "Asset");
3343
- const metadataCoder = scaleBuilder.buildStorage("ForeignAssets", "Metadata");
3344
- const tokenList = {};
3345
- for (const tokenConfig of tokens ?? []) {
3346
- try {
3347
- const onChainId = (() => {
3348
- try {
3349
- return scale.papiParse(tokenConfig.onChainId);
3350
- } catch (error) {
3351
- return tokenConfig.onChainId;
3352
- }
3353
- })();
3354
- if (onChainId === undefined) continue;
3475
+ if (miniMetadata === undefined || metadataVersion === undefined) return {};
3476
+ if (metadataVersion < 14) return {};
3477
+ try {
3478
+ const metadata = scale.unifyMetadata(scale.decAnyMetadata(miniMetadata));
3479
+ const scaleBuilder = scale.getDynamicBuilder(scale.getLookupFn(metadata));
3480
+ const assetsCoder = scaleBuilder.buildStorage("EqAssets", "Assets");
3481
+ const stateKey = assetsCoder.keys.enc();
3482
+
3483
+ /** NOTE: Just a guideline, the RPC can return whatever it wants */
3484
+
3485
+ const assetsResult = await chainConnector.send(chainId, "state_getStorage", [stateKey]).then(result => assetsCoder.value.dec(result) ?? null);
3486
+ const tokens = (Array.isArray(assetsResult) ? assetsResult : []).flatMap(asset => {
3487
+ if (!asset) return [];
3488
+ if (!asset?.id) return [];
3489
+ const assetId = asset.id.toString(10);
3490
+ const symbol = tokenSymbolFromU64Id(asset.id);
3491
+ const id = subEquilibriumTokenId(chainId, symbol);
3492
+ const decimals = DEFAULT_DECIMALS$1;
3493
+ const tokenConfig = (moduleConfig?.tokens ?? []).find(token => token.assetId === assetId);
3494
+ const token = {
3495
+ id,
3496
+ type: "substrate-equilibrium",
3497
+ isTestnet,
3498
+ isDefault: tokenConfig?.isDefault ?? true,
3499
+ symbol,
3500
+ decimals,
3501
+ logo: tokenConfig?.logo || chaindataProvider.githubTokenLogoUrl(id),
3502
+ // TODO: Fetch the ED
3503
+ existentialDeposit: "0",
3504
+ assetId,
3505
+ chain: {
3506
+ id: chainId
3507
+ }
3508
+ };
3509
+ if (tokenConfig?.symbol) {
3510
+ token.symbol = tokenConfig?.symbol;
3511
+ token.id = subEquilibriumTokenId(chainId, token.symbol);
3512
+ }
3513
+ if (tokenConfig?.coingeckoId) token.coingeckoId = tokenConfig?.coingeckoId;
3514
+ if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName;
3515
+ if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf;
3516
+ return [[token.id, token]];
3517
+ });
3518
+ return Object.fromEntries(tokens);
3519
+ } catch (error) {
3520
+ log.error(`Failed to build substrate-equilibrium tokens on ${chainId}`, error?.message ?? error);
3521
+ return {};
3522
+ }
3523
+ },
3524
+ // TODO: Don't create empty subscriptions
3525
+ async subscribeBalances({
3526
+ addressesByToken
3527
+ }, callback) {
3528
+ const queries = await buildQueries$3(chaindataProvider$1, addressesByToken);
3529
+ const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe((error, result) => {
3530
+ if (error) return callback(error);
3531
+ const balances = result?.flatMap(balances => balances) ?? [];
3532
+ if (balances.length > 0) callback(null, new Balances(balances));
3533
+ });
3534
+ return unsubscribe;
3535
+ },
3536
+ async fetchBalances(addressesByToken) {
3537
+ util$1.assert(chainConnectors.substrate, "This module requires a substrate chain connector");
3538
+ const queries = await buildQueries$3(chaindataProvider$1, addressesByToken);
3539
+ const result = await new RpcStateQueryHelper(chainConnectors.substrate, queries).fetch();
3540
+ const balances = result?.flatMap(balances => balances) ?? [];
3541
+ return new Balances(balances);
3542
+ },
3543
+ async transferToken({
3544
+ tokenId,
3545
+ from,
3546
+ to,
3547
+ amount,
3548
+ registry,
3549
+ metadataRpc,
3550
+ blockHash,
3551
+ blockNumber,
3552
+ nonce,
3553
+ specVersion,
3554
+ transactionVersion,
3555
+ tip,
3556
+ transferMethod,
3557
+ userExtensions
3558
+ }) {
3559
+ const token = await chaindataProvider$1.tokenById(tokenId);
3560
+ util$1.assert(token, `Token ${tokenId} not found in store`);
3561
+ if (token.type !== "substrate-equilibrium") throw new Error(`This module doesn't handle tokens of type ${token.type}`);
3562
+ const chainId = token.chain.id;
3563
+ const chain = await chaindataProvider$1.chainById(chainId);
3564
+ util$1.assert(chain?.genesisHash, `Chain ${chainId} not found in store`);
3565
+ const {
3566
+ genesisHash
3567
+ } = chain;
3568
+ const {
3569
+ assetId
3570
+ } = token;
3571
+ const pallet = "EqBalances";
3572
+ const method = transferMethod === "transfer_all" ?
3573
+ // the eqBalances pallet has no transfer_all method
3574
+ "transfer" : transferMethod === "transfer_keep_alive" ?
3575
+ // the eqBalances pallet has no transfer_keep_alive method
3576
+ "transfer" : "transfer";
3577
+ const args = {
3578
+ asset: assetId,
3579
+ to,
3580
+ value: amount
3581
+ };
3582
+ const unsigned = txwrapperCore.defineMethod({
3583
+ method: {
3584
+ pallet: camelCase__default.default(pallet),
3585
+ name: camelCase__default.default(method),
3586
+ args
3587
+ },
3588
+ address: from,
3589
+ blockHash,
3590
+ blockNumber,
3591
+ eraPeriod: 64,
3592
+ genesisHash,
3593
+ metadataRpc,
3594
+ nonce,
3595
+ specVersion,
3596
+ tip: tip ? Number(tip) : 0,
3597
+ transactionVersion
3598
+ }, {
3599
+ metadataRpc,
3600
+ registry,
3601
+ userExtensions
3602
+ });
3603
+ return {
3604
+ type: "substrate",
3605
+ callData: unsigned.method
3606
+ };
3607
+ }
3608
+ };
3609
+ };
3610
+ async function buildQueries$3(chaindataProvider, addressesByToken) {
3611
+ const allChains = await chaindataProvider.chainsById();
3612
+ const tokens = await chaindataProvider.tokensById();
3613
+ const miniMetadatas = new Map((await db.miniMetadatas.toArray()).map(miniMetadata => [miniMetadata.id, miniMetadata]));
3614
+ const uniqueChainIds = getUniqueChainIds(addressesByToken, tokens);
3615
+ const chains = Object.fromEntries(uniqueChainIds.map(chainId => [chainId, allChains[chainId]]));
3616
+ const chainStorageCoders = buildStorageCoders({
3617
+ chainIds: uniqueChainIds,
3618
+ chains,
3619
+ miniMetadatas,
3620
+ moduleType: "substrate-equilibrium",
3621
+ coders: {
3622
+ storage: ["System", "Account"]
3623
+ }
3624
+ });
3625
+
3626
+ // equilibrium returns all chain tokens for each address in the one query
3627
+ // so, we only need to make one query per address, rather than one query per token per address
3628
+ const addressesByChain = new Map();
3629
+ const tokensByAddress = new Map();
3630
+ Object.entries(addressesByToken).map(([tokenId, addresses]) => {
3631
+ const token = tokens[tokenId];
3632
+ if (!token) return log.warn(`Token ${tokenId} not found`);
3633
+ if (token.type !== "substrate-equilibrium") return log.debug(`This module doesn't handle tokens of type ${token.type}`);
3634
+ const chainId = token?.chain?.id;
3635
+ if (!chainId) return log.warn(`Token ${tokenId} has no chain`);
3636
+ const byChain = addressesByChain.get(chainId) ?? new Set();
3637
+ addresses.forEach(address => {
3638
+ byChain?.add(address);
3639
+ tokensByAddress.set(address, (tokensByAddress.get(address) ?? new Set()).add(token));
3640
+ });
3641
+ addressesByChain.set(chainId, byChain);
3642
+ });
3643
+ return Array.from(addressesByChain).flatMap(([chainId, addresses]) => {
3644
+ const chain = chains[chainId];
3645
+ if (!chain) {
3646
+ log.warn(`Chain ${chainId} not found`);
3647
+ return [];
3648
+ }
3649
+ return Array.from(addresses).flatMap(address => {
3650
+ const scaleCoder = chainStorageCoders.get(chainId)?.storage;
3651
+ const stateKey = scale.encodeStateKey(scaleCoder, `Invalid address in ${chainId} storage query ${address}`, address);
3652
+ if (!stateKey) return [];
3653
+ const decodeResult = change => {
3654
+ /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
3655
+
3656
+ const decoded = scale.decodeScale(scaleCoder, change, `Failed to decode eqBalances on chain ${chainId}`);
3657
+ const tokenBalances = Object.fromEntries((decoded?.data?.value?.balance ?? []).map(balance => ({
3658
+ id: (balance?.[0] ?? 0n)?.toString?.(),
3659
+ free: balance?.[1]?.type === "Positive" ? (balance?.[1]?.value ?? 0n).toString() : balance?.[1]?.type === "Negative" ? ((balance?.[1]?.value ?? 0n) * -1n).toString() : "0"
3660
+ })).map(({
3661
+ id,
3662
+ free
3663
+ }) => [id, free]).filter(([id, free]) => id !== undefined && free !== undefined));
3664
+ const result = Array.from(tokensByAddress.get(address) ?? []).filter(t => t.chain.id === chainId).map(token => {
3665
+ const value = tokenBalances[token.assetId];
3666
+ return {
3667
+ source: "substrate-equilibrium",
3668
+ status: "live",
3669
+ address,
3670
+ multiChainId: {
3671
+ subChainId: chainId
3672
+ },
3673
+ chainId,
3674
+ tokenId: token.id,
3675
+ value
3676
+ };
3677
+ }).filter(b => b !== undefined);
3678
+ return result;
3679
+ };
3680
+ return {
3681
+ chainId,
3682
+ stateKey,
3683
+ decodeResult
3684
+ };
3685
+ });
3686
+ });
3687
+ }
3688
+ const DEFAULT_DECIMALS$1 = 9;
3689
+ const tokenSymbolFromU64Id = u64 => {
3690
+ const bytes = [];
3691
+ let num = typeof u64 === "number" ? BigInt(u64) : util.isBigInt(u64) ? u64 : u64.toBigInt();
3692
+ do {
3693
+ bytes.unshift(Number(num % 256n));
3694
+ num = num / 256n;
3695
+ } while (num > 0);
3696
+ return new TextDecoder("utf-8").decode(new Uint8Array(bytes)).toUpperCase();
3697
+ };
3698
+
3699
+ const moduleType$3 = "substrate-foreignassets";
3700
+ const subForeignAssetTokenId = (chainId, tokenSymbol) => `${chainId}-substrate-foreignassets-${tokenSymbol}`.toLowerCase().replace(/ /g, "-");
3701
+ const SubForeignAssetsModule = hydrate => {
3702
+ const {
3703
+ chainConnectors,
3704
+ chaindataProvider: chaindataProvider$1
3705
+ } = hydrate;
3706
+ const chainConnector = chainConnectors.substrate;
3707
+ util$1.assert(chainConnector, "This module requires a substrate chain connector");
3708
+ return {
3709
+ ...DefaultBalanceModule(moduleType$3),
3710
+ async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) {
3711
+ const isTestnet = (await chaindataProvider$1.chainById(chainId))?.isTestnet || false;
3712
+ if (metadataRpc === undefined) return {
3713
+ isTestnet
3714
+ };
3715
+ if ((moduleConfig?.tokens ?? []).length < 1) return {
3716
+ isTestnet
3717
+ };
3718
+ const metadataVersion = scale.getMetadataVersion(metadataRpc);
3719
+ const metadata = scale.decAnyMetadata(metadataRpc);
3720
+ scale.compactMetadata(metadata, [{
3721
+ pallet: "ForeignAssets",
3722
+ items: ["Account", "Asset", "Metadata"]
3723
+ }]);
3724
+ const miniMetadata = scale.encodeMetadata(metadata);
3725
+ return {
3726
+ isTestnet,
3727
+ miniMetadata,
3728
+ metadataVersion
3729
+ };
3730
+ },
3731
+ async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) {
3732
+ if ((moduleConfig?.tokens ?? []).length < 1) return {};
3733
+ const {
3734
+ isTestnet,
3735
+ miniMetadata,
3736
+ metadataVersion
3737
+ } = chainMeta;
3738
+ if (miniMetadata === undefined || metadataVersion === undefined) return {};
3739
+ if (metadataVersion < 14) return {};
3740
+ const metadata = scale.decAnyMetadata(miniMetadata);
3741
+ const unifiedMetadata = scale.unifyMetadata(metadata);
3742
+ const scaleBuilder = scale.getDynamicBuilder(scale.getLookupFn(unifiedMetadata));
3743
+ const assetCoder = scaleBuilder.buildStorage("ForeignAssets", "Asset");
3744
+ const metadataCoder = scaleBuilder.buildStorage("ForeignAssets", "Metadata");
3745
+ const tokens = {};
3746
+ for (const tokenConfig of moduleConfig?.tokens ?? []) {
3747
+ try {
3748
+ const onChainId = (() => {
3749
+ try {
3750
+ return scale.papiParse(tokenConfig.onChainId);
3751
+ } catch (error) {
3752
+ return tokenConfig.onChainId;
3753
+ }
3754
+ })();
3755
+ if (onChainId === undefined) continue;
3355
3756
  const assetStateKey = assetCoder.keys.enc(onChainId);
3356
3757
  const metadataStateKey = metadataCoder.keys.enc(onChainId);
3357
3758
  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)]);
3358
3759
  const existentialDeposit = assetsAsset?.min_balance?.toString?.() ?? "0";
3359
3760
  const symbol = assetsMetadata?.symbol?.asText?.() ?? "Unit";
3360
- const name = assetsMetadata?.name?.asText?.() ?? symbol;
3361
3761
  const decimals = assetsMetadata?.decimals ?? 0;
3362
3762
  const isFrozen = assetsMetadata?.is_frozen ?? false;
3363
- const id = chaindataProvider.subForeignAssetTokenId(chainId, tokenConfig.onChainId);
3763
+ const id = subForeignAssetTokenId(chainId, symbol);
3364
3764
  const token = {
3365
3765
  id,
3366
3766
  type: "substrate-foreignassets",
3367
- platform: "polkadot",
3767
+ isTestnet,
3368
3768
  isDefault: tokenConfig?.isDefault ?? true,
3369
3769
  symbol,
3370
3770
  decimals,
3371
- name: tokenConfig?.name ?? name,
3372
- logo: tokenConfig?.logo,
3771
+ logo: tokenConfig?.logo || chaindataProvider.githubTokenLogoUrl(id),
3373
3772
  existentialDeposit,
3374
3773
  onChainId: tokenConfig.onChainId,
3375
3774
  isFrozen,
3376
- networkId: chainId
3775
+ chain: {
3776
+ id: chainId
3777
+ }
3377
3778
  };
3779
+ if (tokenConfig?.symbol) {
3780
+ token.symbol = tokenConfig?.symbol;
3781
+ token.id = subForeignAssetTokenId(chainId, token.symbol);
3782
+ }
3378
3783
  if (tokenConfig?.coingeckoId) token.coingeckoId = tokenConfig?.coingeckoId;
3784
+ if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName;
3379
3785
  if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf;
3380
- tokenList[token.id] = token;
3786
+ tokens[token.id] = token;
3381
3787
  } catch (error) {
3382
3788
  log.error(`Failed to build substrate-foreignassets token ${tokenConfig.onChainId} (${tokenConfig.symbol}) on ${chainId}`, error?.message ?? error);
3383
3789
  continue;
3384
3790
  }
3385
3791
  }
3386
- return tokenList;
3792
+ return tokens;
3387
3793
  },
3388
3794
  // TODO: Don't create empty subscriptions
3389
3795
  async subscribeBalances({
3390
3796
  addressesByToken
3391
3797
  }, callback) {
3392
- const byNetwork = lodash.keys(addressesByToken).reduce((acc, tokenId) => {
3393
- const networkId = chaindataProvider.parseSubForeignAssetTokenId(tokenId).networkId;
3394
- if (!acc[networkId]) acc[networkId] = {};
3395
- acc[networkId][tokenId] = addressesByToken[tokenId];
3396
- return acc;
3397
- }, {});
3398
- const controller = new AbortController();
3399
- const pUnsubs = Promise.all(lodash.toPairs(byNetwork).map(async ([networkId, addressesByToken]) => {
3400
- try {
3401
- const queries = await buildNetworkQueries$1(networkId, chainConnector, chaindataProvider$1, addressesByToken, controller.signal);
3402
- if (controller.signal.aborted) return () => {};
3403
- const stateHelper = new RpcStateQueryHelper(chainConnector, queries);
3404
- return await stateHelper.subscribe((error, result) => {
3405
- if (error) return callback(error);
3406
- const balances = result?.filter(b => b !== null) ?? [];
3407
- if (balances.length > 0) callback(null, new Balances(balances));
3408
- });
3409
- } catch (err) {
3410
- if (!util.isAbortError(err)) log.error(`Failed to subscribe ${moduleType$3} balances for network ${networkId}`, err);
3411
- return () => {};
3412
- }
3413
- }));
3414
- return () => {
3415
- pUnsubs.then(unsubs => {
3416
- unsubs.forEach(unsubscribe => unsubscribe());
3417
- });
3418
- controller.abort();
3419
- };
3798
+ const queries = await buildQueries$2(chaindataProvider$1, addressesByToken);
3799
+ const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe((error, result) => {
3800
+ if (error) return callback(error);
3801
+ const balances = result?.filter(b => b !== null) ?? [];
3802
+ if (balances.length > 0) callback(null, new Balances(balances));
3803
+ });
3804
+ return unsubscribe;
3420
3805
  },
3421
3806
  async fetchBalances(addressesByToken) {
3422
3807
  util$1.assert(chainConnectors.substrate, "This module requires a substrate chain connector");
3423
- const queries = await buildQueries$2(chainConnector, chaindataProvider$1, addressesByToken);
3808
+ const queries = await buildQueries$2(chaindataProvider$1, addressesByToken);
3424
3809
  const result = await new RpcStateQueryHelper(chainConnectors.substrate, queries).fetch();
3425
3810
  const balances = result?.filter(b => b !== null) ?? [];
3426
3811
  return new Balances(balances);
@@ -3432,10 +3817,11 @@ const SubForeignAssetsModule = hydrate => {
3432
3817
  transferMethod,
3433
3818
  metadataRpc
3434
3819
  }) {
3435
- const token = await chaindataProvider$1.getTokenById(tokenId, "substrate-foreignassets");
3820
+ const token = await chaindataProvider$1.tokenById(tokenId);
3436
3821
  util$1.assert(token, `Token ${tokenId} not found in store`);
3437
- const chainId = token.networkId;
3438
- const chain = await chaindataProvider$1.getNetworkById(chainId, "polkadot");
3822
+ if (token.type !== "substrate-foreignassets") throw new Error(`This module doesn't handle tokens of type ${token.type}`);
3823
+ const chainId = token.chain.id;
3824
+ const chain = await chaindataProvider$1.chainById(chainId);
3439
3825
  util$1.assert(chain?.genesisHash, `Chain ${chainId} not found in store`);
3440
3826
  const onChainId = (() => {
3441
3827
  try {
@@ -3475,16 +3861,23 @@ const SubForeignAssetsModule = hydrate => {
3475
3861
  }
3476
3862
  };
3477
3863
  };
3478
- async function buildNetworkQueries$1(networkId, chainConnector, chaindataProvider, addressesByToken, signal) {
3479
- const miniMetadata = await getMiniMetadata(chaindataProvider, chainConnector, networkId, moduleType$3, signal);
3480
- const chain = await chaindataProvider.getNetworkById(networkId, "polkadot");
3481
- const tokensById = await chaindataProvider.getTokensMapById();
3482
- signal?.throwIfAborted();
3483
- const networkStorageCoders = buildNetworkStorageCoders(networkId, miniMetadata, {
3484
- storage: ["ForeignAssets", "Account"]
3864
+ async function buildQueries$2(chaindataProvider, addressesByToken) {
3865
+ const allChains = await chaindataProvider.chainsById();
3866
+ const tokens = await chaindataProvider.tokensById();
3867
+ const miniMetadatas = new Map((await db.miniMetadatas.toArray()).map(miniMetadata => [miniMetadata.id, miniMetadata]));
3868
+ const uniqueChainIds = getUniqueChainIds(addressesByToken, tokens);
3869
+ const chains = Object.fromEntries(uniqueChainIds.map(chainId => [chainId, allChains[chainId]]));
3870
+ const chainStorageCoders = buildStorageCoders({
3871
+ chainIds: uniqueChainIds,
3872
+ chains,
3873
+ miniMetadatas,
3874
+ moduleType: "substrate-foreignassets",
3875
+ coders: {
3876
+ storage: ["ForeignAssets", "Account"]
3877
+ }
3485
3878
  });
3486
3879
  return Object.entries(addressesByToken).flatMap(([tokenId, addresses]) => {
3487
- const token = tokensById[tokenId];
3880
+ const token = tokens[tokenId];
3488
3881
  if (!token) {
3489
3882
  log.warn(`Token ${tokenId} not found`);
3490
3883
  return [];
@@ -3493,12 +3886,18 @@ async function buildNetworkQueries$1(networkId, chainConnector, chaindataProvide
3493
3886
  log.debug(`This module doesn't handle tokens of type ${token.type}`);
3494
3887
  return [];
3495
3888
  }
3889
+ const chainId = token.chain?.id;
3890
+ if (!chainId) {
3891
+ log.warn(`Token ${tokenId} has no chain`);
3892
+ return [];
3893
+ }
3894
+ const chain = chains[chainId];
3496
3895
  if (!chain) {
3497
- log.warn(`Chain ${networkId} for token ${tokenId} not found`);
3896
+ log.warn(`Chain ${chainId} for token ${tokenId} not found`);
3498
3897
  return [];
3499
3898
  }
3500
3899
  return addresses.flatMap(address => {
3501
- const scaleCoder = networkStorageCoders?.storage;
3900
+ const scaleCoder = chainStorageCoders.get(chainId)?.storage;
3502
3901
  const onChainId = (() => {
3503
3902
  try {
3504
3903
  return scale.papiParse(token.onChainId);
@@ -3506,12 +3905,12 @@ async function buildNetworkQueries$1(networkId, chainConnector, chaindataProvide
3506
3905
  return token.onChainId;
3507
3906
  }
3508
3907
  })();
3509
- const stateKey = scale.encodeStateKey(scaleCoder, `Invalid address / token onChainId in ${networkId} storage query ${address} / ${token.onChainId}`, onChainId, address);
3908
+ const stateKey = scale.encodeStateKey(scaleCoder, `Invalid address / token onChainId in ${chainId} storage query ${address} / ${token.onChainId}`, onChainId, address);
3510
3909
  if (!stateKey) return [];
3511
3910
  const decodeResult = change => {
3512
3911
  /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
3513
3912
 
3514
- const decoded = scale.decodeScale(scaleCoder, change, `Failed to decode substrate-foreignassets balance on chain ${networkId}`) ?? {
3913
+ const decoded = scale.decodeScale(scaleCoder, change, `Failed to decode substrate-foreignassets balance on chain ${chainId}`) ?? {
3515
3914
  balance: 0n,
3516
3915
  status: {
3517
3916
  type: "Liquid"
@@ -3545,54 +3944,29 @@ async function buildNetworkQueries$1(networkId, chainConnector, chaindataProvide
3545
3944
  source: "substrate-foreignassets",
3546
3945
  status: "live",
3547
3946
  address,
3548
- networkId,
3947
+ multiChainId: {
3948
+ subChainId: chainId
3949
+ },
3950
+ chainId,
3549
3951
  tokenId: token.id,
3550
3952
  values: balanceValues
3551
3953
  };
3552
3954
  };
3553
3955
  return {
3554
- chainId: networkId,
3956
+ chainId,
3555
3957
  stateKey,
3556
3958
  decodeResult
3557
3959
  };
3558
3960
  });
3559
3961
  });
3560
3962
  }
3561
- async function buildQueries$2(chainConnector, chaindataProvider$1, addressesByToken, signal) {
3562
- const byNetwork = lodash.keys(addressesByToken).reduce((acc, tokenId) => {
3563
- const networkId = chaindataProvider.parseSubForeignAssetTokenId(tokenId).networkId;
3564
- if (!acc[networkId]) acc[networkId] = {};
3565
- acc[networkId][tokenId] = addressesByToken[tokenId];
3566
- return acc;
3567
- }, {});
3568
- return (await Promise.all(lodash.toPairs(byNetwork).map(([networkId, addressesByToken]) => {
3569
- return buildNetworkQueries$1(networkId, chainConnector, chaindataProvider$1, addressesByToken, signal);
3570
- }))).flat();
3571
- }
3572
-
3573
- const getAddresssesByTokenByNetwork = addressesByToken => {
3574
- const addressesByTokenByNetwork = lodash.toPairs(addressesByToken).reduce((acc, [tokenId, addresses]) => {
3575
- const networkId = chaindataProvider.parseTokenId(tokenId).networkId;
3576
- if (!acc[networkId]) acc[networkId] = {};
3577
- acc[networkId][tokenId] = addresses;
3578
- return acc;
3579
- }, {});
3580
- return addressesByTokenByNetwork;
3581
- };
3582
3963
 
3583
3964
  async function subscribeBase(queries, chainConnector, callback) {
3584
- try {
3585
- const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe((error, result) => {
3586
- if (error) callback(error);
3587
- if (result && result.length > 0) callback(null, result);
3588
- });
3589
- return unsubscribe;
3590
- } catch (err) {
3591
- if (!util.isAbortError(err)) log.error("Error subscribing to base queries", {
3592
- err
3593
- });
3594
- return () => {};
3595
- }
3965
+ const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe((error, result) => {
3966
+ if (error) callback(error);
3967
+ if (result && result.length > 0) callback(null, result);
3968
+ });
3969
+ return unsubscribe;
3596
3970
  }
3597
3971
 
3598
3972
  /**
@@ -3608,6 +3982,262 @@ const asObservable = handler => (...args) => new rxjs.Observable(subscriber => {
3608
3982
  return unsubscribe;
3609
3983
  });
3610
3984
 
3985
+ /**
3986
+ * Crowdloan contributions are stored in the `childstate` key returned by this function.
3987
+ */
3988
+ const crowdloanFundContributionsChildKey = fundIndex => util$1.u8aToHex(util$1.u8aConcat(":child_storage:default:", utilCrypto.blake2AsU8a(util$1.u8aConcat("crowdloan", scaleTs.u32.enc(fundIndex)))));
3989
+
3990
+ async function subscribeCrowdloans(chaindataProvider, chainConnector, addressesByToken, callback) {
3991
+ const allChains = await chaindataProvider.chainsById();
3992
+ const tokens = await chaindataProvider.tokensById();
3993
+ const miniMetadatas = new Map((await db.miniMetadatas.toArray()).map(miniMetadata => [miniMetadata.id, miniMetadata]));
3994
+ const crowdloanTokenIds = Object.entries(tokens).filter(([, token]) => {
3995
+ // ignore non-native tokens
3996
+ if (token.type !== "substrate-native") return;
3997
+ // ignore tokens on chains with no crowdloans pallet
3998
+ const [chainMeta] = findChainMeta(miniMetadatas, "substrate-native", allChains[token.chain.id]);
3999
+ return typeof chainMeta?.crowdloanPalletId === "string";
4000
+ }).map(([tokenId]) => tokenId);
4001
+
4002
+ // crowdloan contributions can only be done by the native token on chains with the crowdloan pallet
4003
+ const addressesByCrowdloanToken = Object.fromEntries(Object.entries(addressesByToken)
4004
+ // remove ethereum addresses
4005
+ .map(([tokenId, addresses]) => [tokenId, addresses.filter(address => !util.isEthereumAddress(address))])
4006
+ // remove tokens which aren't crowdloan tokens
4007
+ .filter(([tokenId]) => crowdloanTokenIds.includes(tokenId)));
4008
+ const uniqueChainIds = getUniqueChainIds(addressesByCrowdloanToken, tokens);
4009
+ const chains = Object.fromEntries(Object.entries(allChains).filter(([chainId]) => uniqueChainIds.includes(chainId)));
4010
+ const chainStorageCoders = buildStorageCoders({
4011
+ chainIds: uniqueChainIds,
4012
+ chains,
4013
+ miniMetadatas,
4014
+ moduleType: "substrate-native",
4015
+ coders: {
4016
+ parachains: ["Paras", "Parachains"],
4017
+ funds: ["Crowdloan", "Funds"]
4018
+ }
4019
+ });
4020
+ const tokenSubscriptions = [];
4021
+ for (const [tokenId, addresses] of Object.entries(addressesByCrowdloanToken)) {
4022
+ const token = tokens[tokenId];
4023
+ if (!token) {
4024
+ log.warn(`Token ${tokenId} not found`);
4025
+ continue;
4026
+ }
4027
+ if (token.type !== "substrate-native") {
4028
+ log.debug(`This module doesn't handle tokens of type ${token.type}`);
4029
+ continue;
4030
+ }
4031
+ const chainId = token.chain?.id;
4032
+ if (!chainId) {
4033
+ log.warn(`Token ${tokenId} has no chain`);
4034
+ continue;
4035
+ }
4036
+ const chain = chains[chainId];
4037
+ if (!chain) {
4038
+ log.warn(`Chain ${chainId} for token ${tokenId} not found`);
4039
+ continue;
4040
+ }
4041
+ const subscribeParaIds = callback => {
4042
+ const scaleCoder = chainStorageCoders.get(chainId)?.parachains;
4043
+ const queries = [0].flatMap(() => {
4044
+ const stateKey = scale.encodeStateKey(scaleCoder);
4045
+ if (!stateKey) return [];
4046
+ const decodeResult = change => {
4047
+ /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
4048
+
4049
+ const decoded = scale.decodeScale(scaleCoder, change, `Failed to decode parachains on chain ${chainId}`);
4050
+ const paraIds = decoded ?? [];
4051
+ return paraIds;
4052
+ };
4053
+ return {
4054
+ chainId,
4055
+ stateKey,
4056
+ decodeResult
4057
+ };
4058
+ });
4059
+ const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
4060
+ return () => subscription.then(unsubscribe => unsubscribe());
4061
+ };
4062
+ const subscribeParaFundIndexes = (paraIds, callback) => {
4063
+ const scaleCoder = chainStorageCoders.get(chainId)?.funds;
4064
+ const queries = paraIds.flatMap(paraId => {
4065
+ const stateKey = scale.encodeStateKey(scaleCoder, `Invalid paraId in ${chainId} funds query ${paraId}`, paraId);
4066
+ if (!stateKey) return [];
4067
+ const decodeResult = change => {
4068
+ /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
4069
+
4070
+ const decoded = scale.decodeScale(scaleCoder, change, `Failed to decode paras on chain ${chainId}`);
4071
+ const firstPeriod = decoded?.first_period?.toString?.() ?? "";
4072
+ const lastPeriod = decoded?.last_period?.toString?.() ?? "";
4073
+ const fundPeriod = `${firstPeriod}-${lastPeriod}`;
4074
+ const fundIndex = decoded?.fund_index ?? decoded?.trie_index;
4075
+ return {
4076
+ paraId,
4077
+ fundPeriod,
4078
+ fundIndex
4079
+ };
4080
+ };
4081
+ return {
4082
+ chainId,
4083
+ stateKey,
4084
+ decodeResult
4085
+ };
4086
+ });
4087
+ const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
4088
+ return () => subscription.then(unsubscribe => unsubscribe());
4089
+ };
4090
+ const subscribeFundContributions = (funds, addresses, callback) => {
4091
+ // TODO: Watch system_events in order to subscribe to changes, then redo the contributions query when changes are detected:
4092
+ // https://github.com/polkadot-js/api/blob/8fe02a14345b57e6abb8f7f2c2b624cf70c51b23/packages/api-derive/src/crowdloan/ownContributions.ts#L32-L47
4093
+ //
4094
+ // For now we just re-fetch all contributions on a timer and then only send them to the subscription callback when they have changed
4095
+
4096
+ const queries = funds.map(({
4097
+ paraId,
4098
+ fundIndex
4099
+ }) => ({
4100
+ paraId,
4101
+ fundIndex,
4102
+ addresses,
4103
+ childKey: crowdloanFundContributionsChildKey(fundIndex),
4104
+ storageKeys: addresses.map(address => util$1.u8aToHex(util.decodeAnyAddress(address)))
4105
+ }));
4106
+
4107
+ // track whether our caller is still subscribed
4108
+ let subscriptionActive = true;
4109
+ let previousContributions = null;
4110
+ const fetchContributions = async () => {
4111
+ try {
4112
+ const results = await Promise.all(queries.map(async ({
4113
+ paraId,
4114
+ fundIndex,
4115
+ addresses,
4116
+ childKey,
4117
+ storageKeys
4118
+ }) => ({
4119
+ paraId,
4120
+ fundIndex,
4121
+ addresses,
4122
+ result: await chainConnector.send(chainId, "childstate_getStorageEntries", [childKey, storageKeys])
4123
+ })));
4124
+ const contributions = results.flatMap(queryResult => {
4125
+ const {
4126
+ paraId,
4127
+ fundIndex,
4128
+ addresses,
4129
+ result
4130
+ } = queryResult;
4131
+ return (Array.isArray(result) ? result : []).flatMap((encoded, index) => {
4132
+ const amount = (() => {
4133
+ try {
4134
+ return typeof encoded === "string" ? scaleTs.u128.dec(encoded) ?? 0n : 0n;
4135
+ } catch {
4136
+ return 0n;
4137
+ }
4138
+ })().toString();
4139
+ return {
4140
+ paraId,
4141
+ fundIndex,
4142
+ address: addresses[index],
4143
+ amount
4144
+ };
4145
+ });
4146
+ });
4147
+
4148
+ // ignore these results if our caller has tried to close this subscription
4149
+ if (!subscriptionActive) return;
4150
+
4151
+ // ignore these results if they're the same as what we previously fetched
4152
+ if (isEqual__default.default(previousContributions, contributions)) return;
4153
+ previousContributions = contributions;
4154
+ callback(null, contributions);
4155
+ } catch (error) {
4156
+ callback(error);
4157
+ }
4158
+ };
4159
+
4160
+ // set up polling for contributions
4161
+ const crowdloanContributionsPollInterval = 60_000; // 60_000ms === 1 minute
4162
+ const pollContributions = async () => {
4163
+ if (!subscriptionActive) return;
4164
+ try {
4165
+ await fetchContributions();
4166
+ } catch (error) {
4167
+ // log any errors, but don't cancel the poll for contributions when one fetch fails
4168
+ log.error(error);
4169
+ }
4170
+ if (!subscriptionActive) return;
4171
+ setTimeout(pollContributions, crowdloanContributionsPollInterval);
4172
+ };
4173
+
4174
+ // start polling
4175
+ pollContributions();
4176
+ return () => {
4177
+ // stop polling
4178
+ subscriptionActive = false;
4179
+ };
4180
+ };
4181
+ const paraIds$ = asObservable(subscribeParaIds)().pipe(rxjs.scan((_, next) => Array.from(new Set(next.flatMap(paraIds => paraIds))), []), rxjs.share());
4182
+ const fundIndexesByParaId$ = paraIds$.pipe(rxjs.map(paraIds => asObservable(subscribeParaFundIndexes)(paraIds)), rxjs.switchAll(), rxjs.scan((state, next) => {
4183
+ for (const fund of next) {
4184
+ const {
4185
+ paraId,
4186
+ fundIndex
4187
+ } = fund;
4188
+ if (typeof fundIndex === "number") {
4189
+ state.set(paraId, (state.get(paraId) ?? new Set()).add(fundIndex));
4190
+ }
4191
+ }
4192
+ return state;
4193
+ }, new Map()));
4194
+ const contributionsByAddress$ = fundIndexesByParaId$.pipe(rxjs.map(fundIndexesByParaId => Array.from(fundIndexesByParaId).flatMap(([paraId, fundIndexes]) => Array.from(fundIndexes).map(fundIndex => ({
4195
+ paraId,
4196
+ fundIndex
4197
+ })))), rxjs.map(funds => asObservable(subscribeFundContributions)(funds, addresses)), rxjs.switchAll(), rxjs.scan((state, next) => {
4198
+ for (const contribution of next) {
4199
+ const {
4200
+ address
4201
+ } = contribution;
4202
+ state.set(address, (state.get(address) ?? new Set()).add(contribution));
4203
+ }
4204
+ return state;
4205
+ }, new Map()));
4206
+ const subscription = contributionsByAddress$.subscribe({
4207
+ next: contributionsByAddress => {
4208
+ const balances = Array.from(contributionsByAddress).map(([address, contributions]) => {
4209
+ return {
4210
+ source: "substrate-native",
4211
+ status: "live",
4212
+ address,
4213
+ multiChainId: {
4214
+ subChainId: chainId
4215
+ },
4216
+ chainId,
4217
+ tokenId,
4218
+ values: Array.from(contributions).map(({
4219
+ amount,
4220
+ paraId
4221
+ }) => ({
4222
+ type: "crowdloan",
4223
+ label: "crowdloan",
4224
+ source: "crowdloan",
4225
+ amount: amount,
4226
+ meta: {
4227
+ paraId
4228
+ }
4229
+ }))
4230
+ };
4231
+ });
4232
+ if (balances.length > 0) callback(null, balances);
4233
+ },
4234
+ error: error => callback(error)
4235
+ });
4236
+ tokenSubscriptions.push(() => subscription.unsubscribe());
4237
+ }
4238
+ return () => tokenSubscriptions.forEach(unsub => unsub());
4239
+ }
4240
+
3611
4241
  /**
3612
4242
  * Each nominationPool in the nominationPools pallet has access to some accountIds which have no
3613
4243
  * associated private key. Instead, they are derived from this function.
@@ -3627,293 +4257,280 @@ const nompoolAccountId = (palletId, poolId, index) => {
3627
4257
  /** The stash account for the nomination pool */
3628
4258
  const nompoolStashAccountId = (palletId, poolId) => nompoolAccountId(palletId, poolId, 0);
3629
4259
 
3630
- // TODO make this method chain-specific
3631
- async function subscribeNompoolStaking(chaindataProvider$1, chainConnector, addressesByToken, callback, signal) {
3632
- try {
3633
- const allChains = await chaindataProvider$1.getNetworksMapById("polkadot");
3634
- const tokens = await chaindataProvider$1.getTokensMapById();
3635
-
3636
- // there should be only one network here when subscribing to balances, we've split it up by network at the top level
3637
- const networkIds = lodash.keys(addressesByToken).map(tokenId => chaindataProvider.parseTokenId(tokenId).networkId);
3638
- const miniMetadatas = new Map();
3639
- for (const networkId of networkIds) {
3640
- const miniMetadata = await getMiniMetadata(chaindataProvider$1, chainConnector, networkId, "substrate-native");
3641
- miniMetadatas.set(networkId, miniMetadata);
4260
+ async function subscribeNompoolStaking(chaindataProvider, chainConnector, addressesByToken, callback) {
4261
+ const allChains = await chaindataProvider.chainsById();
4262
+ const tokens = await chaindataProvider.tokensById();
4263
+ const miniMetadatas = new Map((await db.miniMetadatas.toArray()).map(miniMetadata => [miniMetadata.id, miniMetadata]));
4264
+ const nomPoolTokenIds = Object.entries(tokens).filter(([, token]) => {
4265
+ // ignore non-native tokens
4266
+ if (token.type !== "substrate-native") return false;
4267
+ // ignore tokens on chains with no nompools pallet
4268
+ const [chainMeta] = findChainMeta(miniMetadatas, "substrate-native", allChains[token.chain.id]);
4269
+ return typeof chainMeta?.nominationPoolsPalletId === "string";
4270
+ }).map(([tokenId]) => tokenId);
4271
+
4272
+ // staking can only be done by the native token on chains with the staking pallet
4273
+ const addressesByNomPoolToken = Object.fromEntries(Object.entries(addressesByToken)
4274
+ // remove ethereum addresses
4275
+ .map(([tokenId, addresses]) => [tokenId, addresses.filter(address => !util.isEthereumAddress(address))])
4276
+ // remove tokens which aren't nom pool tokens
4277
+ .filter(([tokenId]) => nomPoolTokenIds.includes(tokenId)));
4278
+ const uniqueChainIds = getUniqueChainIds(addressesByNomPoolToken, tokens);
4279
+ const chains = Object.fromEntries(Object.entries(allChains).filter(([chainId]) => uniqueChainIds.includes(chainId)));
4280
+ const chainStorageCoders = buildStorageCoders({
4281
+ chainIds: uniqueChainIds,
4282
+ chains,
4283
+ miniMetadatas,
4284
+ moduleType: "substrate-native",
4285
+ coders: {
4286
+ poolMembers: ["NominationPools", "PoolMembers"],
4287
+ bondedPools: ["NominationPools", "BondedPools"],
4288
+ ledger: ["Staking", "Ledger"],
4289
+ metadata: ["NominationPools", "Metadata"]
3642
4290
  }
3643
- signal?.throwIfAborted();
3644
- const nomPoolTokenIds = Object.entries(tokens).filter(([, token]) => {
3645
- // ignore non-native tokens
3646
- if (token.type !== "substrate-native") return false;
3647
-
3648
- // ignore tokens on chains with no nompools pallet
3649
- const miniMetadata = miniMetadatas.get(token.networkId);
3650
- return typeof miniMetadata?.extra?.nominationPoolsPalletId === "string";
3651
- }).map(([tokenId]) => tokenId);
3652
-
3653
- // staking can only be done by the native token on chains with the staking pallet
3654
- const addressesByNomPoolToken = Object.fromEntries(Object.entries(addressesByToken)
3655
- // remove ethereum addresses
3656
- .map(([tokenId, addresses]) => [tokenId, addresses.filter(address => !util.isEthereumAddress(address))])
3657
- // remove tokens which aren't nom pool tokens
3658
- .filter(([tokenId]) => nomPoolTokenIds.includes(tokenId)));
3659
- const uniqueChainIds = getUniqueChainIds(addressesByNomPoolToken, tokens);
3660
- const chains = Object.fromEntries(Object.entries(allChains).filter(([chainId]) => uniqueChainIds.includes(chainId)));
3661
- const chainStorageCoders = buildStorageCoders({
3662
- chainIds: uniqueChainIds,
3663
- chains,
3664
- miniMetadatas,
3665
- coders: {
3666
- poolMembers: ["NominationPools", "PoolMembers"],
3667
- bondedPools: ["NominationPools", "BondedPools"],
3668
- ledger: ["Staking", "Ledger"],
3669
- metadata: ["NominationPools", "Metadata"]
3670
- }
3671
- });
3672
- const resultUnsubscribes = [];
3673
- for (const [tokenId, addresses] of Object.entries(addressesByNomPoolToken)) {
3674
- const token = tokens[tokenId];
3675
- if (!token) {
3676
- log.warn(`Token ${tokenId} not found`);
3677
- continue;
3678
- }
3679
- if (token.type !== "substrate-native") {
3680
- log.debug(`This module doesn't handle tokens of type ${token.type}`);
3681
- continue;
3682
- }
3683
- const chainId = token.networkId;
3684
- if (!chainId) {
3685
- log.warn(`Token ${tokenId} has no chain`);
3686
- continue;
3687
- }
3688
- const chain = chains[chainId];
3689
- if (!chain) {
3690
- log.warn(`Chain ${chainId} for token ${tokenId} not found`);
3691
- continue;
3692
- }
3693
- const miniMetadata = miniMetadatas.get(chainId);
3694
- const {
3695
- nominationPoolsPalletId
3696
- } = miniMetadata?.extra ?? {};
3697
- const subscribePoolMembers = (addresses, callback) => {
3698
- const scaleCoder = chainStorageCoders.get(chainId)?.poolMembers;
3699
- const queries = addresses.flatMap(address => {
3700
- const stateKey = scale.encodeStateKey(scaleCoder, `Invalid address in ${chainId} poolMembers query ${address}`, address);
3701
- if (!stateKey) return [];
3702
- const decodeResult = change => {
3703
- /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
3704
-
3705
- const decoded = scale.decodeScale(scaleCoder, change, `Failed to decode poolMembers on chain ${chainId}`);
3706
- const poolId = decoded?.pool_id?.toString?.();
3707
- const points = decoded?.points?.toString?.();
3708
- const unbondingEras = Array.from(decoded?.unbonding_eras ?? []).flatMap(entry => {
3709
- if (entry === undefined) return [];
3710
- const [key, value] = Array.from(entry);
3711
- const era = key?.toString?.();
3712
- const amount = value?.toString?.();
3713
- if (typeof era !== "string" || typeof amount !== "string") return [];
3714
- return {
3715
- era,
3716
- amount
3717
- };
3718
- });
3719
- return {
3720
- tokenId,
3721
- address,
3722
- poolId,
3723
- points,
3724
- unbondingEras
3725
- };
3726
- };
3727
- return {
3728
- chainId,
3729
- stateKey,
3730
- decodeResult
3731
- };
3732
- });
3733
- const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
3734
- return () => subscription.then(unsubscribe => unsubscribe());
3735
- };
3736
- const subscribePoolPoints = (poolIds, callback) => {
3737
- if (poolIds.length === 0) callback(null, []);
3738
- const scaleCoder = chainStorageCoders.get(chainId)?.bondedPools;
3739
- const queries = poolIds.flatMap(poolId => {
3740
- const stateKey = scale.encodeStateKey(scaleCoder, `Invalid poolId in ${chainId} bondedPools query ${poolId}`, poolId);
3741
- if (!stateKey) return [];
3742
- const decodeResult = change => {
3743
- /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
3744
-
3745
- const decoded = scale.decodeScale(scaleCoder, change, `Failed to decode bondedPools on chain ${chainId}`);
3746
- const points = decoded?.points?.toString?.();
3747
- return {
3748
- poolId,
3749
- points
3750
- };
3751
- };
3752
- return {
3753
- chainId,
3754
- stateKey,
3755
- decodeResult
3756
- };
3757
- });
3758
- const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
3759
- return () => subscription.then(unsubscribe => unsubscribe());
3760
- };
3761
- const subscribePoolStake = (poolIds, callback) => {
3762
- if (poolIds.length === 0) callback(null, []);
3763
- const scaleCoder = chainStorageCoders.get(chainId)?.ledger;
3764
- const queries = poolIds.flatMap(poolId => {
3765
- if (!nominationPoolsPalletId) return [];
3766
- const stashAddress = nompoolStashAccountId(nominationPoolsPalletId, poolId);
3767
- const stateKey = scale.encodeStateKey(scaleCoder, `Invalid address in ${chainId} ledger query ${stashAddress}`, stashAddress);
3768
- if (!stateKey) return [];
3769
- const decodeResult = change => {
3770
- /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
3771
-
3772
- const decoded = scale.decodeScale(scaleCoder, change, `Failed to decode ledger on chain ${chainId}`);
3773
- const activeStake = decoded?.active?.toString?.();
3774
- return {
3775
- poolId,
3776
- activeStake
3777
- };
3778
- };
3779
- return {
3780
- chainId,
3781
- stateKey,
3782
- decodeResult
3783
- };
3784
- });
3785
- const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
3786
- return () => subscription.then(unsubscribe => unsubscribe());
3787
- };
3788
- const subscribePoolMetadata = (poolIds, callback) => {
3789
- if (poolIds.length === 0) callback(null, []);
3790
- const scaleCoder = chainStorageCoders.get(chainId)?.metadata;
3791
- const queries = poolIds.flatMap(poolId => {
3792
- if (!nominationPoolsPalletId) return [];
3793
- const stateKey = scale.encodeStateKey(scaleCoder, `Invalid poolId in ${chainId} metadata query ${poolId}`, poolId);
3794
- if (!stateKey) return [];
3795
- const decodeResult = change => {
3796
- /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
3797
-
3798
- const decoded = scale.decodeScale(scaleCoder, change, `Failed to decode metadata on chain ${chainId}`);
3799
- const metadata = decoded?.asText?.();
4291
+ });
4292
+ const resultUnsubscribes = [];
4293
+ for (const [tokenId, addresses] of Object.entries(addressesByNomPoolToken)) {
4294
+ const token = tokens[tokenId];
4295
+ if (!token) {
4296
+ log.warn(`Token ${tokenId} not found`);
4297
+ continue;
4298
+ }
4299
+ if (token.type !== "substrate-native") {
4300
+ log.debug(`This module doesn't handle tokens of type ${token.type}`);
4301
+ continue;
4302
+ }
4303
+ const chainId = token.chain?.id;
4304
+ if (!chainId) {
4305
+ log.warn(`Token ${tokenId} has no chain`);
4306
+ continue;
4307
+ }
4308
+ const chain = chains[chainId];
4309
+ if (!chain) {
4310
+ log.warn(`Chain ${chainId} for token ${tokenId} not found`);
4311
+ continue;
4312
+ }
4313
+ const [chainMeta] = findChainMeta(miniMetadatas, "substrate-native", chain);
4314
+ const {
4315
+ nominationPoolsPalletId
4316
+ } = chainMeta ?? {};
4317
+ const subscribePoolMembers = (addresses, callback) => {
4318
+ const scaleCoder = chainStorageCoders.get(chainId)?.poolMembers;
4319
+ const queries = addresses.flatMap(address => {
4320
+ const stateKey = scale.encodeStateKey(scaleCoder, `Invalid address in ${chainId} poolMembers query ${address}`, address);
4321
+ if (!stateKey) return [];
4322
+ const decodeResult = change => {
4323
+ /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
4324
+
4325
+ const decoded = scale.decodeScale(scaleCoder, change, `Failed to decode poolMembers on chain ${chainId}`);
4326
+ const poolId = decoded?.pool_id?.toString?.();
4327
+ const points = decoded?.points?.toString?.();
4328
+ const unbondingEras = Array.from(decoded?.unbonding_eras ?? []).flatMap(entry => {
4329
+ if (entry === undefined) return [];
4330
+ const [key, value] = Array.from(entry);
4331
+ const era = key?.toString?.();
4332
+ const amount = value?.toString?.();
4333
+ if (typeof era !== "string" || typeof amount !== "string") return [];
3800
4334
  return {
3801
- poolId,
3802
- metadata
4335
+ era,
4336
+ amount
3803
4337
  };
3804
- };
4338
+ });
3805
4339
  return {
3806
- chainId,
3807
- stateKey,
3808
- decodeResult
3809
- };
3810
- });
3811
- const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
3812
- return () => subscription.then(unsubscribe => unsubscribe());
3813
- };
3814
- const poolMembersByAddress$ = asObservable(subscribePoolMembers)(addresses).pipe(rxjs.scan((state, next) => {
3815
- for (const poolMembers of next) {
3816
- const {
3817
- poolId,
3818
- points,
3819
- unbondingEras
3820
- } = poolMembers;
3821
- if (typeof poolId === "string" && typeof points === "string") state.set(poolMembers.address, {
4340
+ tokenId,
4341
+ address,
3822
4342
  poolId,
3823
4343
  points,
3824
4344
  unbondingEras
3825
- });else state.set(poolMembers.address, null);
3826
- }
3827
- return state;
3828
- }, new Map()), rxjs.share());
3829
- const poolIdByAddress$ = poolMembersByAddress$.pipe(rxjs.map(pm => new Map(Array.from(pm).map(([address, pm]) => [address, pm?.poolId ?? null]))));
3830
- const pointsByAddress$ = poolMembersByAddress$.pipe(rxjs.map(pm => new Map(Array.from(pm).map(([address, pm]) => [address, pm?.points ?? null]))));
3831
- const unbondingErasByAddress$ = poolMembersByAddress$.pipe(rxjs.map(pm => new Map(Array.from(pm).map(([address, pm]) => [address, pm?.unbondingEras ?? null]))));
3832
- const poolIds$ = poolIdByAddress$.pipe(rxjs.map(byAddress => [...new Set(Array.from(byAddress.values()).flatMap(poolId => poolId ?? []))]));
3833
- const pointsByPool$ = poolIds$.pipe(rxjs.map(poolIds => asObservable(subscribePoolPoints)(poolIds)), rxjs.switchAll(), rxjs.scan((state, next) => {
3834
- for (const poolPoints of next) {
3835
- const {
4345
+ };
4346
+ };
4347
+ return {
4348
+ chainId,
4349
+ stateKey,
4350
+ decodeResult
4351
+ };
4352
+ });
4353
+ const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
4354
+ return () => subscription.then(unsubscribe => unsubscribe());
4355
+ };
4356
+ const subscribePoolPoints = (poolIds, callback) => {
4357
+ if (poolIds.length === 0) callback(null, []);
4358
+ const scaleCoder = chainStorageCoders.get(chainId)?.bondedPools;
4359
+ const queries = poolIds.flatMap(poolId => {
4360
+ const stateKey = scale.encodeStateKey(scaleCoder, `Invalid poolId in ${chainId} bondedPools query ${poolId}`, poolId);
4361
+ if (!stateKey) return [];
4362
+ const decodeResult = change => {
4363
+ /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
4364
+
4365
+ const decoded = scale.decodeScale(scaleCoder, change, `Failed to decode bondedPools on chain ${chainId}`);
4366
+ const points = decoded?.points?.toString?.();
4367
+ return {
3836
4368
  poolId,
3837
4369
  points
3838
- } = poolPoints;
3839
- if (typeof points === "string") state.set(poolId, points);else state.delete(poolId);
3840
- }
3841
- return state;
3842
- }, new Map()));
3843
- const stakeByPool$ = poolIds$.pipe(rxjs.map(poolIds => asObservable(subscribePoolStake)(poolIds)), rxjs.switchAll(), rxjs.scan((state, next) => {
3844
- for (const poolStake of next) {
3845
- const {
4370
+ };
4371
+ };
4372
+ return {
4373
+ chainId,
4374
+ stateKey,
4375
+ decodeResult
4376
+ };
4377
+ });
4378
+ const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
4379
+ return () => subscription.then(unsubscribe => unsubscribe());
4380
+ };
4381
+ const subscribePoolStake = (poolIds, callback) => {
4382
+ if (poolIds.length === 0) callback(null, []);
4383
+ const scaleCoder = chainStorageCoders.get(chainId)?.ledger;
4384
+ const queries = poolIds.flatMap(poolId => {
4385
+ if (!nominationPoolsPalletId) return [];
4386
+ const stashAddress = nompoolStashAccountId(nominationPoolsPalletId, poolId);
4387
+ const stateKey = scale.encodeStateKey(scaleCoder, `Invalid address in ${chainId} ledger query ${stashAddress}`, stashAddress);
4388
+ if (!stateKey) return [];
4389
+ const decodeResult = change => {
4390
+ /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
4391
+
4392
+ const decoded = scale.decodeScale(scaleCoder, change, `Failed to decode ledger on chain ${chainId}`);
4393
+ const activeStake = decoded?.active?.toString?.();
4394
+ return {
3846
4395
  poolId,
3847
4396
  activeStake
3848
- } = poolStake;
3849
- if (typeof activeStake === "string") state.set(poolId, activeStake);else state.delete(poolId);
3850
- }
3851
- return state;
3852
- }, new Map()));
3853
- const metadataByPool$ = poolIds$.pipe(rxjs.map(poolIds => asObservable(subscribePoolMetadata)(poolIds)), rxjs.switchAll(), rxjs.scan((state, next) => {
3854
- for (const poolMetadata of next) {
3855
- const {
4397
+ };
4398
+ };
4399
+ return {
4400
+ chainId,
4401
+ stateKey,
4402
+ decodeResult
4403
+ };
4404
+ });
4405
+ const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
4406
+ return () => subscription.then(unsubscribe => unsubscribe());
4407
+ };
4408
+ const subscribePoolMetadata = (poolIds, callback) => {
4409
+ if (poolIds.length === 0) callback(null, []);
4410
+ const scaleCoder = chainStorageCoders.get(chainId)?.metadata;
4411
+ const queries = poolIds.flatMap(poolId => {
4412
+ if (!nominationPoolsPalletId) return [];
4413
+ const stateKey = scale.encodeStateKey(scaleCoder, `Invalid poolId in ${chainId} metadata query ${poolId}`, poolId);
4414
+ if (!stateKey) return [];
4415
+ const decodeResult = change => {
4416
+ /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
4417
+
4418
+ const decoded = scale.decodeScale(scaleCoder, change, `Failed to decode metadata on chain ${chainId}`);
4419
+ const metadata = decoded?.asText?.();
4420
+ return {
3856
4421
  poolId,
3857
4422
  metadata
3858
- } = poolMetadata;
3859
- if (typeof metadata === "string") state.set(poolId, metadata);else state.delete(poolId);
3860
- }
3861
- return state;
3862
- }, new Map()));
3863
- const subscription = rxjs.combineLatest([poolIdByAddress$, pointsByAddress$, unbondingErasByAddress$, pointsByPool$, stakeByPool$, metadataByPool$]).subscribe({
3864
- next: ([poolIdByAddress, pointsByAddress, unbondingErasByAddress, pointsByPool, stakeByPool, metadataByPool]) => {
3865
- const balances = Array.from(poolIdByAddress).map(([address, poolId]) => {
3866
- const parsedPoolId = poolId === null ? undefined : parseInt(poolId);
3867
- const points = pointsByAddress.get(address) ?? "0";
3868
- const poolPoints = pointsByPool.get(poolId ?? "") ?? "0";
3869
- const poolStake = stakeByPool.get(poolId ?? "") ?? "0";
3870
- const poolMetadata = poolId ? metadataByPool.get(poolId) ?? `Pool ${poolId}` : undefined;
3871
- const amount = points === "0" || poolPoints === "0" || poolStake === "0" ? 0n : BigInt(poolStake) * BigInt(points) / BigInt(poolPoints);
3872
- const unbondingAmount = (unbondingErasByAddress.get(address) ?? []).reduce((total, {
3873
- amount
3874
- }) => total + BigInt(amount ?? "0"), 0n);
3875
- return {
3876
- source: "substrate-native",
3877
- status: "live",
3878
- address,
3879
- networkId: chainId,
3880
- tokenId,
3881
- values: [{
3882
- source: "nompools-staking",
3883
- type: "nompool",
3884
- label: "nompools-staking",
3885
- amount: amount.toString(),
3886
- meta: {
3887
- type: "nompool",
3888
- poolId: parsedPoolId,
3889
- description: poolMetadata
3890
- }
3891
- }, {
3892
- source: "nompools-staking",
3893
- type: "nompool",
3894
- label: "nompools-unbonding",
3895
- amount: unbondingAmount.toString(),
3896
- meta: {
3897
- poolId: parsedPoolId,
3898
- description: poolMetadata,
3899
- unbonding: true
3900
- }
3901
- }]
3902
- };
3903
- }).filter(util.isNotNil);
3904
- if (balances.length > 0) callback(null, balances);
3905
- },
3906
- error: error => callback(error)
4423
+ };
4424
+ };
4425
+ return {
4426
+ chainId,
4427
+ stateKey,
4428
+ decodeResult
4429
+ };
3907
4430
  });
3908
- resultUnsubscribes.push(() => subscription.unsubscribe());
3909
- }
3910
- return () => resultUnsubscribes.forEach(unsub => unsub());
3911
- } catch (err) {
3912
- if (!util.isAbortError(err)) log.error("Error subscribing to nom pool staking", {
3913
- err
4431
+ const subscription = new RpcStateQueryHelper(chainConnector, queries).subscribe(callback);
4432
+ return () => subscription.then(unsubscribe => unsubscribe());
4433
+ };
4434
+ const poolMembersByAddress$ = asObservable(subscribePoolMembers)(addresses).pipe(rxjs.scan((state, next) => {
4435
+ for (const poolMembers of next) {
4436
+ const {
4437
+ poolId,
4438
+ points,
4439
+ unbondingEras
4440
+ } = poolMembers;
4441
+ if (typeof poolId === "string" && typeof points === "string") state.set(poolMembers.address, {
4442
+ poolId,
4443
+ points,
4444
+ unbondingEras
4445
+ });else state.set(poolMembers.address, null);
4446
+ }
4447
+ return state;
4448
+ }, new Map()), rxjs.share());
4449
+ const poolIdByAddress$ = poolMembersByAddress$.pipe(rxjs.map(pm => new Map(Array.from(pm).map(([address, pm]) => [address, pm?.poolId ?? null]))));
4450
+ const pointsByAddress$ = poolMembersByAddress$.pipe(rxjs.map(pm => new Map(Array.from(pm).map(([address, pm]) => [address, pm?.points ?? null]))));
4451
+ const unbondingErasByAddress$ = poolMembersByAddress$.pipe(rxjs.map(pm => new Map(Array.from(pm).map(([address, pm]) => [address, pm?.unbondingEras ?? null]))));
4452
+ const poolIds$ = poolIdByAddress$.pipe(rxjs.map(byAddress => [...new Set(Array.from(byAddress.values()).flatMap(poolId => poolId ?? []))]));
4453
+ const pointsByPool$ = poolIds$.pipe(rxjs.map(poolIds => asObservable(subscribePoolPoints)(poolIds)), rxjs.switchAll(), rxjs.scan((state, next) => {
4454
+ for (const poolPoints of next) {
4455
+ const {
4456
+ poolId,
4457
+ points
4458
+ } = poolPoints;
4459
+ if (typeof points === "string") state.set(poolId, points);else state.delete(poolId);
4460
+ }
4461
+ return state;
4462
+ }, new Map()));
4463
+ const stakeByPool$ = poolIds$.pipe(rxjs.map(poolIds => asObservable(subscribePoolStake)(poolIds)), rxjs.switchAll(), rxjs.scan((state, next) => {
4464
+ for (const poolStake of next) {
4465
+ const {
4466
+ poolId,
4467
+ activeStake
4468
+ } = poolStake;
4469
+ if (typeof activeStake === "string") state.set(poolId, activeStake);else state.delete(poolId);
4470
+ }
4471
+ return state;
4472
+ }, new Map()));
4473
+ const metadataByPool$ = poolIds$.pipe(rxjs.map(poolIds => asObservable(subscribePoolMetadata)(poolIds)), rxjs.switchAll(), rxjs.scan((state, next) => {
4474
+ for (const poolMetadata of next) {
4475
+ const {
4476
+ poolId,
4477
+ metadata
4478
+ } = poolMetadata;
4479
+ if (typeof metadata === "string") state.set(poolId, metadata);else state.delete(poolId);
4480
+ }
4481
+ return state;
4482
+ }, new Map()));
4483
+ const subscription = rxjs.combineLatest([poolIdByAddress$, pointsByAddress$, unbondingErasByAddress$, pointsByPool$, stakeByPool$, metadataByPool$]).subscribe({
4484
+ next: ([poolIdByAddress, pointsByAddress, unbondingErasByAddress, pointsByPool, stakeByPool, metadataByPool]) => {
4485
+ const balances = Array.from(poolIdByAddress).map(([address, poolId]) => {
4486
+ const parsedPoolId = poolId === null ? undefined : parseInt(poolId);
4487
+ const points = pointsByAddress.get(address) ?? "0";
4488
+ const poolPoints = pointsByPool.get(poolId ?? "") ?? "0";
4489
+ const poolStake = stakeByPool.get(poolId ?? "") ?? "0";
4490
+ const poolMetadata = poolId ? metadataByPool.get(poolId) ?? `Pool ${poolId}` : undefined;
4491
+ const amount = points === "0" || poolPoints === "0" || poolStake === "0" ? 0n : BigInt(poolStake) * BigInt(points) / BigInt(poolPoints);
4492
+ const unbondingAmount = (unbondingErasByAddress.get(address) ?? []).reduce((total, {
4493
+ amount
4494
+ }) => total + BigInt(amount ?? "0"), 0n);
4495
+ return {
4496
+ source: "substrate-native",
4497
+ status: "live",
4498
+ address,
4499
+ multiChainId: {
4500
+ subChainId: chainId
4501
+ },
4502
+ chainId,
4503
+ tokenId,
4504
+ values: [{
4505
+ source: "nompools-staking",
4506
+ type: "nompool",
4507
+ label: "nompools-staking",
4508
+ amount: amount.toString(),
4509
+ meta: {
4510
+ type: "nompool",
4511
+ poolId: parsedPoolId,
4512
+ description: poolMetadata
4513
+ }
4514
+ }, {
4515
+ source: "nompools-staking",
4516
+ type: "nompool",
4517
+ label: "nompools-unbonding",
4518
+ amount: unbondingAmount.toString(),
4519
+ meta: {
4520
+ poolId: parsedPoolId,
4521
+ description: poolMetadata,
4522
+ unbonding: true
4523
+ }
4524
+ }]
4525
+ };
4526
+ }).filter(util.isNotNil);
4527
+ if (balances.length > 0) callback(null, balances);
4528
+ },
4529
+ error: error => callback(error)
3914
4530
  });
3915
- return () => {};
4531
+ resultUnsubscribes.push(() => subscription.unsubscribe());
3916
4532
  }
4533
+ return () => resultUnsubscribes.forEach(unsub => unsub());
3917
4534
  }
3918
4535
 
3919
4536
  const SUBTENSOR_ROOT_NETUID = 0;
@@ -3955,236 +4572,225 @@ const calculateTaoFromDynamicInfo = ({
3955
4572
  });
3956
4573
  };
3957
4574
 
3958
- // TODO make this method chain-specific
3959
- async function subscribeSubtensorStaking(chaindataProvider$1, chainConnector, addressesByToken, callback, signal) {
3960
- try {
3961
- const allChains = await chaindataProvider$1.getNetworksMapById("polkadot");
3962
- const tokens = await chaindataProvider$1.getTokensMapById();
3963
-
3964
- // there should be only one network here when subscribing to balances, we've split it up by network at the top level
3965
- const networkIds = lodash.keys(addressesByToken).map(tokenId => chaindataProvider.parseTokenId(tokenId).networkId);
3966
- const miniMetadatas = new Map();
3967
- for (const networkId of networkIds) {
3968
- const miniMetadata = await getMiniMetadata(chaindataProvider$1, chainConnector, networkId, "substrate-native", signal);
3969
- miniMetadatas.set(networkId, miniMetadata);
4575
+ async function subscribeSubtensorStaking(chaindataProvider, chainConnector, addressesByToken, callback) {
4576
+ const allChains = await chaindataProvider.chainsById();
4577
+ const tokens = await chaindataProvider.tokensById();
4578
+ const miniMetadatas = new Map((await db.miniMetadatas.toArray()).map(miniMetadata => [miniMetadata.id, miniMetadata]));
4579
+ const subtensorTokenIds = Object.entries(tokens).filter(([, token]) => {
4580
+ // ignore non-native tokens
4581
+ if (token.type !== "substrate-native") return false;
4582
+ // ignore tokens on chains with no subtensor pallet
4583
+ const [chainMeta] = findChainMeta(miniMetadatas, "substrate-native", allChains[token.chain.id]);
4584
+ return chainMeta?.hasSubtensorPallet === true;
4585
+ }).map(([tokenId]) => tokenId);
4586
+
4587
+ // staking can only be done by the native token on chains with the subtensor pallet
4588
+ const addressesBySubtensorToken = Object.fromEntries(Object.entries(addressesByToken)
4589
+ // remove ethereum addresses
4590
+ .map(([tokenId, addresses]) => [tokenId, addresses.filter(address => !util.isEthereumAddress(address))])
4591
+ // remove tokens which aren't subtensor staking tokens
4592
+ .filter(([tokenId]) => subtensorTokenIds.includes(tokenId)));
4593
+ const uniqueChainIds = getUniqueChainIds(addressesBySubtensorToken, tokens);
4594
+ const chains = Object.fromEntries(Object.entries(allChains).filter(([chainId]) => uniqueChainIds.includes(chainId)));
4595
+ const abortController = new AbortController();
4596
+ for (const [tokenId, addresses] of Object.entries(addressesBySubtensorToken)) {
4597
+ const token = tokens[tokenId];
4598
+ if (!token) {
4599
+ log.warn(`Token ${tokenId} not found`);
4600
+ continue;
4601
+ }
4602
+ if (token.type !== "substrate-native") {
4603
+ log.debug(`This module doesn't handle tokens of type ${token.type}`);
4604
+ continue;
4605
+ }
4606
+ const chainId = token.chain?.id;
4607
+ if (!chainId) {
4608
+ log.warn(`Token ${tokenId} has no chain`);
4609
+ continue;
3970
4610
  }
3971
- signal?.throwIfAborted();
3972
- const subtensorTokenIds = Object.entries(tokens).filter(([, token]) => {
3973
- // ignore non-native tokens
3974
- if (token.type !== "substrate-native") return false;
3975
- // ignore tokens on chains with no subtensor pallet
3976
- const miniMetadata = miniMetadatas.get(token.networkId);
3977
- return miniMetadata?.extra?.hasSubtensorPallet === true;
3978
- }).map(([tokenId]) => tokenId);
3979
-
3980
- // staking can only be done by the native token on chains with the subtensor pallet
3981
- const addressesBySubtensorToken = Object.fromEntries(Object.entries(addressesByToken)
3982
- // remove ethereum addresses
3983
- .map(([tokenId, addresses]) => [tokenId, addresses.filter(address => !util.isEthereumAddress(address))])
3984
- // remove tokens which aren't subtensor staking tokens
3985
- .filter(([tokenId]) => subtensorTokenIds.includes(tokenId)));
3986
- const uniqueChainIds = getUniqueChainIds(addressesBySubtensorToken, tokens);
3987
- const chains = Object.fromEntries(Object.entries(allChains).filter(([chainId]) => uniqueChainIds.includes(chainId)));
3988
- const abortController = new AbortController();
3989
- for (const [tokenId, addresses] of Object.entries(addressesBySubtensorToken)) {
3990
- const token = tokens[tokenId];
3991
- if (!token) {
3992
- log.warn(`Token ${tokenId} not found`);
3993
- continue;
3994
- }
3995
- if (token.type !== "substrate-native") {
3996
- log.debug(`This module doesn't handle tokens of type ${token.type}`);
3997
- continue;
3998
- }
3999
- const chainId = token.networkId;
4000
- const chain = chains[chainId];
4001
- if (!chain) {
4002
- log.warn(`Chain ${chainId} for token ${tokenId} not found`);
4003
- continue;
4004
- }
4005
- const miniMetadata = miniMetadatas.get(token.networkId);
4006
- if (!miniMetadata?.data) {
4007
- log.warn(`MiniMetadata for chain ${chainId} not found`);
4008
- continue;
4009
- }
4010
- const scaleApi = sapi.getScaleApi({
4011
- chainId,
4012
- send: (...args) => chainConnector.send(chainId, ...args, {
4013
- expectErrors: true
4014
- } // don't pollute the wallet logs when this request fails
4015
- )
4016
- }, miniMetadata.data, token, chain.hasCheckMetadataHash, chain.signedExtensions, chain.registryTypes);
4017
-
4018
- // sets the number of addresses to query in parallel (per chain, since each chain runs in parallel to the others)
4019
- const concurrency = 4;
4020
- // In-memory cache for successful dynamic info results
4021
- const dynamicInfoCache = new Map();
4022
- const fetchDynamicInfoForNetuids = async uniqueNetuids => {
4023
- const MAX_RETRIES = 3;
4024
- const RETRY_DELAY_MS = 500;
4025
- const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
4026
- const fetchInfo = async netuid => {
4027
- if (netuid === 0) return null;
4028
- for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
4029
- try {
4030
- const params = [netuid];
4031
- const result = await scaleApi.getRuntimeCallValue("SubnetInfoRuntimeApi", "get_dynamic_info", params);
4032
- dynamicInfoCache.set(netuid, result); // Cache successful response
4033
- return result;
4034
- } catch (error) {
4035
- log.trace(`Attempt ${attempt} failed for netuid ${netuid}:`, error);
4036
- if (attempt < MAX_RETRIES) {
4037
- const backoffTime = RETRY_DELAY_MS * 2 ** (attempt - 1);
4038
- log.trace(`Retrying in ${backoffTime}ms...`);
4039
- await delay(backoffTime);
4040
- }
4611
+ const chain = chains[chainId];
4612
+ if (!chain) {
4613
+ log.warn(`Chain ${chainId} for token ${tokenId} not found`);
4614
+ continue;
4615
+ }
4616
+ const [chainMeta] = findChainMeta(miniMetadatas, "substrate-native", chain);
4617
+ if (!chainMeta?.miniMetadata) {
4618
+ log.warn(`MiniMetadata for chain ${chainId} not found`);
4619
+ continue;
4620
+ }
4621
+ const scaleApi = sapi.getScaleApi({
4622
+ chainId,
4623
+ send: (...args) => chainConnector.send(chainId, ...args, {
4624
+ expectErrors: true
4625
+ } // don't pollute the wallet logs when this request fails
4626
+ )
4627
+ }, chainMeta.miniMetadata, token, chain.hasCheckMetadataHash, chain.signedExtensions, chain.registryTypes);
4628
+
4629
+ // sets the number of addresses to query in parallel (per chain, since each chain runs in parallel to the others)
4630
+ const concurrency = 4;
4631
+ // In-memory cache for successful dynamic info results
4632
+ const dynamicInfoCache = new Map();
4633
+ const fetchDynamicInfoForNetuids = async uniqueNetuids => {
4634
+ const MAX_RETRIES = 3;
4635
+ const RETRY_DELAY_MS = 500;
4636
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
4637
+ const fetchInfo = async netuid => {
4638
+ if (netuid === 0) return null;
4639
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
4640
+ try {
4641
+ const params = [netuid];
4642
+ const result = await scaleApi.getRuntimeCallValue("SubnetInfoRuntimeApi", "get_dynamic_info", params);
4643
+ dynamicInfoCache.set(netuid, result); // Cache successful response
4644
+ return result;
4645
+ } catch (error) {
4646
+ log.trace(`Attempt ${attempt} failed for netuid ${netuid}:`, error);
4647
+ if (attempt < MAX_RETRIES) {
4648
+ const backoffTime = RETRY_DELAY_MS * 2 ** (attempt - 1);
4649
+ log.trace(`Retrying in ${backoffTime}ms...`);
4650
+ await delay(backoffTime);
4041
4651
  }
4042
4652
  }
4043
- if (dynamicInfoCache.has(netuid)) {
4044
- return dynamicInfoCache.get(netuid); // Use cached value on failure
4045
- }
4046
- log.trace(`Failed to fetch dynamic info for netuid ${netuid} after ${MAX_RETRIES} attempts.`);
4047
- return null;
4048
- };
4049
- return Promise.all(uniqueNetuids.map(fetchInfo));
4653
+ }
4654
+ if (dynamicInfoCache.has(netuid)) {
4655
+ return dynamicInfoCache.get(netuid); // Use cached value on failure
4656
+ }
4657
+ log.trace(`Failed to fetch dynamic info for netuid ${netuid} after ${MAX_RETRIES} attempts.`);
4658
+ return null;
4050
4659
  };
4051
- const subtensorQueries = rxjs.from(addresses).pipe(
4052
- // mergeMap lets us run N concurrent queries, where N is the value of `concurrency`
4053
- rxjs.mergeMap(async address => {
4054
- const queryMethods = [async () => {
4055
- if (chain.isTestnet) return [];
4056
- const params = [address];
4057
- const result = await scaleApi.getRuntimeCallValue("StakeInfoRuntimeApi", "get_stake_info_for_coldkey", params);
4058
- if (!Array.isArray(result)) return [];
4059
- const uniqueNetuids = Array.from(new Set(result.map(item => Number(item.netuid)).filter(netuid => netuid !== SUBTENSOR_ROOT_NETUID)));
4060
- await fetchDynamicInfoForNetuids(uniqueNetuids);
4061
- const stakes = result?.map(({
4062
- coldkey,
4660
+ return Promise.all(uniqueNetuids.map(fetchInfo));
4661
+ };
4662
+ const subtensorQueries = rxjs.from(addresses).pipe(
4663
+ // mergeMap lets us run N concurrent queries, where N is the value of `concurrency`
4664
+ rxjs.mergeMap(async address => {
4665
+ const queryMethods = [async () => {
4666
+ if (chain.isTestnet) return [];
4667
+ const params = [address];
4668
+ const result = await scaleApi.getRuntimeCallValue("StakeInfoRuntimeApi", "get_stake_info_for_coldkey", params);
4669
+ if (!Array.isArray(result)) return [];
4670
+ const uniqueNetuids = Array.from(new Set(result.map(item => Number(item.netuid)).filter(netuid => netuid !== SUBTENSOR_ROOT_NETUID)));
4671
+ await fetchDynamicInfoForNetuids(uniqueNetuids);
4672
+ const stakes = result?.map(({
4673
+ coldkey,
4674
+ hotkey,
4675
+ netuid,
4676
+ stake
4677
+ }) => {
4678
+ return {
4679
+ address: coldkey,
4063
4680
  hotkey,
4064
- netuid,
4065
- stake
4066
- }) => {
4067
- return {
4068
- address: coldkey,
4069
- hotkey,
4070
- netuid: Number(netuid),
4071
- stake: BigInt(stake),
4072
- dynamicInfo: dynamicInfoCache.get(Number(netuid))
4073
- };
4074
- }).filter(({
4075
- stake
4076
- }) => stake >= SUBTENSOR_MIN_STAKE_AMOUNT_PLANK);
4077
- return stakes;
4078
- }];
4079
- const errors = [];
4080
- for (const queryMethod of queryMethods) {
4081
- try {
4082
- // try each query method
4083
- return await queryMethod();
4084
- } catch (cause) {
4085
- // if it fails, keep track of the error and try the next one
4086
- errors.push(cause);
4087
- }
4681
+ netuid: Number(netuid),
4682
+ stake: BigInt(stake),
4683
+ dynamicInfo: dynamicInfoCache.get(Number(netuid))
4684
+ };
4685
+ }).filter(({
4686
+ stake
4687
+ }) => stake >= SUBTENSOR_MIN_STAKE_AMOUNT_PLANK);
4688
+ return stakes;
4689
+ }];
4690
+ const errors = [];
4691
+ for (const queryMethod of queryMethods) {
4692
+ try {
4693
+ // try each query method
4694
+ return await queryMethod();
4695
+ } catch (cause) {
4696
+ // if it fails, keep track of the error and try the next one
4697
+ errors.push(cause);
4088
4698
  }
4699
+ }
4089
4700
 
4090
- // if we get to here, that means that all query methods failed
4091
- // let's throw the errors back to the native balance module
4092
- throw new Error([`Failed to fetch ${tokenId} subtensor staked balance for ${address}:`, ...errors.map(error => String(error))].join("\n\t"));
4093
- }, concurrency),
4094
- // instead of emitting each balance as it's fetched, toArray waits for them all to fetch and then it collects them into an array
4095
- rxjs.toArray(),
4096
- // this mergeMap flattens our Array<Array<Stakes>> into just an Array<Stakes>
4097
- rxjs.mergeMap(stakes => stakes),
4098
- // convert our Array<Stakes> into Array<Balances>, which we can then return to the native balance module
4099
- rxjs.map(stakes => stakes.map(({
4701
+ // if we get to here, that means that all query methods failed
4702
+ // let's throw the errors back to the native balance module
4703
+ throw new Error([`Failed to fetch ${tokenId} subtensor staked balance for ${address}:`, ...errors.map(error => String(error))].join("\n\t"));
4704
+ }, concurrency),
4705
+ // instead of emitting each balance as it's fetched, toArray waits for them all to fetch and then it collects them into an array
4706
+ rxjs.toArray(),
4707
+ // this mergeMap flattens our Array<Array<Stakes>> into just an Array<Stakes>
4708
+ rxjs.mergeMap(stakes => stakes),
4709
+ // convert our Array<Stakes> into Array<Balances>, which we can then return to the native balance module
4710
+ rxjs.map(stakes => stakes.map(({
4711
+ address,
4712
+ hotkey,
4713
+ stake,
4714
+ netuid,
4715
+ dynamicInfo
4716
+ }) => {
4717
+ const {
4718
+ token_symbol,
4719
+ subnet_name,
4720
+ subnet_identity
4721
+ } = dynamicInfo ?? {};
4722
+ const tokenSymbol = new TextDecoder().decode(Uint8Array.from(token_symbol ?? []));
4723
+ const subnetName = new TextDecoder().decode(Uint8Array.from(subnet_name ?? []));
4724
+
4725
+ /** Map from Record<string, Binary> to Record<string, string> */
4726
+ const binaryToText = input => Object.entries(input).reduce((acc, [key, value]) => {
4727
+ acc[key] = value.asText();
4728
+ return acc;
4729
+ }, {});
4730
+ const subnetIdentity = subnet_identity ? binaryToText(subnet_identity) : undefined;
4731
+
4732
+ // Add 1n balance if failed to fetch dynamic info, so the position is not ignored by Balance lib and is displayed in the UI.
4733
+ const alphaStakedInTao = dynamicInfo ? calculateTaoFromDynamicInfo({
4734
+ dynamicInfo,
4735
+ alphaStaked: stake
4736
+ }) : 1n;
4737
+ const alphaToTaoRate = calculateTaoFromDynamicInfo({
4738
+ dynamicInfo: dynamicInfo ?? null,
4739
+ alphaStaked: ONE_ALPHA_TOKEN
4740
+ }).toString();
4741
+ const stakeByNetuid = Number(netuid) === SUBTENSOR_ROOT_NETUID ? stake : alphaStakedInTao;
4742
+ return {
4743
+ source: "substrate-native",
4744
+ status: "live",
4100
4745
  address,
4101
- hotkey,
4102
- stake,
4103
- netuid,
4104
- dynamicInfo
4105
- }) => {
4106
- const {
4107
- token_symbol,
4108
- subnet_name,
4109
- subnet_identity
4110
- } = dynamicInfo ?? {};
4111
- const tokenSymbol = new TextDecoder().decode(Uint8Array.from(token_symbol ?? []));
4112
- const subnetName = new TextDecoder().decode(Uint8Array.from(subnet_name ?? []));
4113
-
4114
- /** Map from Record<string, Binary> to Record<string, string> */
4115
- const binaryToText = input => Object.entries(input).reduce((acc, [key, value]) => {
4116
- acc[key] = value.asText();
4117
- return acc;
4118
- }, {});
4119
- const subnetIdentity = subnet_identity ? binaryToText(subnet_identity) : undefined;
4120
-
4121
- // Add 1n balance if failed to fetch dynamic info, so the position is not ignored by Balance lib and is displayed in the UI.
4122
- const alphaStakedInTao = dynamicInfo ? calculateTaoFromDynamicInfo({
4123
- dynamicInfo,
4124
- alphaStaked: stake
4125
- }) : 1n;
4126
- const alphaToTaoRate = calculateTaoFromDynamicInfo({
4127
- dynamicInfo: dynamicInfo ?? null,
4128
- alphaStaked: ONE_ALPHA_TOKEN
4129
- }).toString();
4130
- const stakeByNetuid = Number(netuid) === SUBTENSOR_ROOT_NETUID ? stake : alphaStakedInTao;
4131
- return {
4132
- source: "substrate-native",
4133
- status: "live",
4134
- address,
4135
- networkId: chainId,
4136
- tokenId,
4137
- values: [{
4138
- source: "subtensor-staking",
4139
- type: "subtensor",
4140
- label: "subtensor-staking",
4141
- amount: stakeByNetuid.toString(),
4142
- meta: {
4143
- type: "subtensor-staking",
4144
- hotkey,
4145
- netuid,
4146
- amountStaked: stake.toString(),
4147
- alphaToTaoRate,
4148
- dynamicInfo: {
4149
- tokenSymbol,
4150
- subnetName,
4151
- subnetIdentity: {
4152
- ...subnetIdentity,
4153
- subnetName: subnetIdentity?.subnet_name || subnetName
4154
- }
4746
+ multiChainId: {
4747
+ subChainId: chainId
4748
+ },
4749
+ chainId,
4750
+ tokenId,
4751
+ values: [{
4752
+ source: "subtensor-staking",
4753
+ type: "subtensor",
4754
+ label: "subtensor-staking",
4755
+ amount: stakeByNetuid.toString(),
4756
+ meta: {
4757
+ type: "subtensor-staking",
4758
+ hotkey,
4759
+ netuid,
4760
+ amountStaked: stake.toString(),
4761
+ alphaToTaoRate,
4762
+ dynamicInfo: {
4763
+ tokenSymbol,
4764
+ subnetName,
4765
+ subnetIdentity: {
4766
+ ...subnetIdentity,
4767
+ subnetName: subnetIdentity?.subnet_name || subnetName
4155
4768
  }
4156
4769
  }
4157
- }]
4158
- };
4159
- })));
4160
-
4161
- // This observable will run the subtensorQueries on a 30s (30_000ms) interval.
4162
- // However, if the last run has not yet completed (e.g. its been 30s but we're still fetching some balances),
4163
- // then exhaustMap will wait until the next interval (so T: 60s, T: 90s, T: 120s, etc) before re-executing the subtensorQueries.
4164
- const subtensorQueriesInterval = rxjs.interval(30_000).pipe(rxjs.startWith(0),
4165
- // start immediately
4166
- rxjs.exhaustMap(() => {
4167
- return subtensorQueries;
4168
- }));
4770
+ }
4771
+ }]
4772
+ };
4773
+ })));
4169
4774
 
4170
- // subscribe to the balances
4171
- const subscription = subtensorQueriesInterval.subscribe({
4172
- next: balances => callback(null, balances),
4173
- error: error => callback(error)
4174
- });
4775
+ // This observable will run the subtensorQueries on a 30s (30_000ms) interval.
4776
+ // However, if the last run has not yet completed (e.g. its been 30s but we're still fetching some balances),
4777
+ // then exhaustMap will wait until the next interval (so T: 60s, T: 90s, T: 120s, etc) before re-executing the subtensorQueries.
4778
+ const subtensorQueriesInterval = rxjs.interval(30_000).pipe(rxjs.startWith(0),
4779
+ // start immediately
4780
+ rxjs.exhaustMap(() => {
4781
+ return subtensorQueries;
4782
+ }));
4175
4783
 
4176
- // use the abortController to tear the subscription down when we don't need it anymore
4177
- abortController.signal.addEventListener("abort", () => {
4178
- subscription.unsubscribe();
4179
- });
4180
- }
4181
- return () => abortController.abort();
4182
- } catch (err) {
4183
- if (!util.isAbortError(err)) log.error("Error subscribing to subtensor staking", {
4184
- err
4784
+ // subscribe to the balances
4785
+ const subscription = subtensorQueriesInterval.subscribe({
4786
+ next: balances => callback(null, balances),
4787
+ error: error => callback(error)
4185
4788
  });
4186
- return () => {};
4789
+
4790
+ // use the abortController to tear the subscription down when we don't need it anymore
4791
+ abortController.signal.onabort = () => subscription.unsubscribe();
4187
4792
  }
4793
+ return () => abortController.abort();
4188
4794
  }
4189
4795
 
4190
4796
  const getOtherType = input => `other-${input}`;
@@ -4240,14 +4846,18 @@ const filterBaseLocks = locks => {
4240
4846
  };
4241
4847
 
4242
4848
  // TODO: Make these titles translatable
4243
- const getLockTitle = (lock,
4244
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
4245
- {
4849
+ const getLockTitle = (lock, {
4246
4850
  balance
4247
4851
  } = {}) => {
4248
4852
  if (!lock.label) return lock.label;
4249
4853
  if (lock.label === "democracy") return "Governance";
4250
- if (lock.label === "crowdloan") return "Crowdloan";
4854
+ if (lock.label === "crowdloan") {
4855
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
4856
+ const paraId = lock.meta?.paraId;
4857
+ if (!paraId) return "Crowdloan";
4858
+ const name = balance?.chain?.parathreads?.find(parathread => parathread?.paraId === paraId)?.name;
4859
+ return `${name ? name : `Parachain ${paraId}`} Crowdloan`;
4860
+ }
4251
4861
  if (lock.label === "nompools-staking") return "Pooled Staking";
4252
4862
  if (lock.label === "nompools-unbonding") return "Pooled Staking";
4253
4863
  if (lock.label === "subtensor-staking") return "Root Staking";
@@ -4259,6 +4869,7 @@ const getLockTitle = (lock,
4259
4869
  };
4260
4870
 
4261
4871
  const moduleType$2 = "substrate-native";
4872
+ const subNativeTokenId = chainId => `${chainId}-substrate-native`.toLowerCase().replace(/ /g, "-");
4262
4873
 
4263
4874
  /**
4264
4875
  * Function to merge two 'sub sources' of the same balance together, or
@@ -4336,7 +4947,7 @@ async function buildQueries$1(chains, tokens, chainStorageCoders, miniMetadatas,
4336
4947
  log.debug(`This module doesn't handle tokens of type ${token.type}`);
4337
4948
  return outerResult;
4338
4949
  }
4339
- const chainId = token.networkId;
4950
+ const chainId = token.chain?.id;
4340
4951
  if (!chainId) {
4341
4952
  log.warn(`Token ${tokenId} has no chain`);
4342
4953
  return outerResult;
@@ -4346,10 +4957,10 @@ async function buildQueries$1(chains, tokens, chainStorageCoders, miniMetadatas,
4346
4957
  log.warn(`Chain ${chainId} for token ${tokenId} not found`);
4347
4958
  return outerResult;
4348
4959
  }
4349
- const miniMetadata = miniMetadatas.get(chainId);
4960
+ const [chainMeta] = findChainMeta(miniMetadatas, "substrate-native", chain);
4350
4961
  const {
4351
4962
  useLegacyTransferableCalculation
4352
- } = miniMetadata?.extra ?? {};
4963
+ } = chainMeta ?? {};
4353
4964
  addresses.flat().forEach(address => {
4354
4965
  const queryKey = `${tokenId}-${address}`;
4355
4966
  // We share this balanceJson between the base and the lock query for this address
@@ -4357,7 +4968,10 @@ async function buildQueries$1(chains, tokens, chainStorageCoders, miniMetadatas,
4357
4968
  source: "substrate-native",
4358
4969
  status: "live",
4359
4970
  address,
4360
- networkId: chainId,
4971
+ multiChainId: {
4972
+ subChainId: chainId
4973
+ },
4974
+ chainId,
4361
4975
  tokenId,
4362
4976
  values: []
4363
4977
  };
@@ -4629,19 +5243,75 @@ const updateStakingLocksUsingUnbondingLocks = values => {
4629
5243
  return [...otherValues, ...stakingLocks];
4630
5244
  };
4631
5245
 
5246
+ const detectMiniMetadataChanges = () => {
5247
+ let previousMap = null;
5248
+ return rxjs.pipe(rxjs.map(currMap => {
5249
+ if (!currMap) return null;
5250
+ const changes = new Set();
5251
+ if (previousMap) {
5252
+ // Check for added or changed keys/values
5253
+ for (const [key, value] of currMap) {
5254
+ if (!previousMap.has(key) || !isEqual__default.default(previousMap.get(key), value)) {
5255
+ changes.add(value.chainId);
5256
+ }
5257
+ }
5258
+
5259
+ // Check for removed keys
5260
+ for (const [key, value] of previousMap) {
5261
+ if (!currMap.has(key)) {
5262
+ changes.add(value.chainId);
5263
+ }
5264
+ }
5265
+ }
5266
+ previousMap = currMap;
5267
+ return changes.size > 0 ? changes : null;
5268
+ }),
5269
+ // Filter out null emissions (no changes)
5270
+ rxjs.filter(changes => changes !== null));
5271
+ };
5272
+
5273
+ // NOTE: `liveQuery` is not initialized until commonMetadataObservable is subscribed to.
5274
+ const commonMetadataObservable = rxjs.from(dexie.liveQuery(() => db.miniMetadatas.where("source").equals("substrate-native").toArray())).pipe(rxjs.map(items => new Map(items.map(item => [item.id, item]))),
5275
+ // `refCount: true` will unsubscribe from the DB when commonMetadataObservable has no more subscribers
5276
+ rxjs.shareReplay({
5277
+ bufferSize: 1,
5278
+ refCount: true
5279
+ }));
4632
5280
  class QueryCache {
4633
- #chaindataProvider;
4634
- #chainConnector;
4635
- miniMetadatas = new Map();
4636
5281
  balanceQueryCache = new Map();
4637
- constructor(chaindataProvider, chainConnector) {
5282
+ metadataSub = null;
5283
+ constructor(chaindataProvider) {
4638
5284
  this.chaindataProvider = chaindataProvider;
4639
- this.#chaindataProvider = chaindataProvider;
4640
- this.#chainConnector = chainConnector;
5285
+ }
5286
+ ensureSetup() {
5287
+ if (this.metadataSub) return;
5288
+ this.metadataSub = commonMetadataObservable.pipe(util.firstThenDebounce(500), detectMiniMetadataChanges(), rxjs.combineLatestWith(this.chaindataProvider.tokensObservable), rxjs.distinctUntilChanged()).subscribe(([miniMetadataChanges, tokens]) => {
5289
+ // invalidate cache entries for any chains with new metadata
5290
+ const tokensByChainId = tokens.filter(token => token.type === "substrate-native").reduce((result, token) => {
5291
+ if (!token.chain?.id) return result;
5292
+ result[token.chain.id] ? result[token.chain.id].push(token) : result[token.chain.id] = [token];
5293
+ return result;
5294
+ }, {});
5295
+ miniMetadataChanges.forEach(chainId => {
5296
+ const chainTokens = tokensByChainId[chainId];
5297
+ if (!chainTokens) return;
5298
+ chainTokens.forEach(token => {
5299
+ const tokenId = token.id;
5300
+ const cacheKeys = this.balanceQueryCache.keys();
5301
+ for (const key of cacheKeys) {
5302
+ if (key.startsWith(`${tokenId}-`)) this.balanceQueryCache.delete(key);
5303
+ }
5304
+ });
5305
+ });
5306
+ });
5307
+ }
5308
+ destroy() {
5309
+ this.metadataSub?.unsubscribe();
4641
5310
  }
4642
5311
  async getQueries(addressesByToken) {
4643
- const chains = await this.chaindataProvider.getNetworksMapById("polkadot");
4644
- const tokens = await this.chaindataProvider.getTokensMapById();
5312
+ this.ensureSetup();
5313
+ const chains = await this.chaindataProvider.chainsById();
5314
+ const tokens = await this.chaindataProvider.tokensById();
4645
5315
  const queryResults = Object.entries(addressesByToken).reduce((result, [tokenId, addresses]) => {
4646
5316
  addresses.forEach(address => {
4647
5317
  const key = `${tokenId}-${address}`;
@@ -4657,19 +5327,15 @@ class QueryCache {
4657
5327
  existing: [],
4658
5328
  newAddressesByToken: {}
4659
5329
  });
4660
- const byNetwork = getAddresssesByTokenByNetwork(addressesByToken);
4661
- for (const networkId of lodash.keys(byNetwork)) {
4662
- if (this.miniMetadatas.has(networkId)) continue;
4663
- const miniMetadata = await getMiniMetadata(this.#chaindataProvider, this.#chainConnector, networkId, "substrate-native");
4664
- this.miniMetadatas.set(networkId, miniMetadata);
4665
- }
4666
5330
 
4667
5331
  // build queries for token/address pairs which have not been queried before
4668
- const uniqueChainIds = lodash.keys(byNetwork); // getUniqueChainIds(queryResults.newAddressesByToken, tokens)
5332
+ const miniMetadatas = await rxjs.firstValueFrom(commonMetadataObservable);
5333
+ const uniqueChainIds = getUniqueChainIds(queryResults.newAddressesByToken, tokens);
4669
5334
  const chainStorageCoders = buildStorageCoders({
4670
5335
  chainIds: uniqueChainIds,
4671
5336
  chains,
4672
- miniMetadatas: this.miniMetadatas,
5337
+ miniMetadatas,
5338
+ moduleType: "substrate-native",
4673
5339
  coders: {
4674
5340
  base: ["System", "Account"],
4675
5341
  stakingLedger: ["Staking", "Ledger"],
@@ -4679,7 +5345,7 @@ class QueryCache {
4679
5345
  freezes: ["Balances", "Freezes"]
4680
5346
  }
4681
5347
  });
4682
- const queries = await buildQueries$1(chains, tokens, chainStorageCoders, this.miniMetadatas, queryResults.newAddressesByToken);
5348
+ const queries = await buildQueries$1(chains, tokens, chainStorageCoders, miniMetadatas, queryResults.newAddressesByToken);
4683
5349
  // now update the cache
4684
5350
  Object.entries(queries).forEach(([key, query]) => {
4685
5351
  this.balanceQueryCache.set(key, query);
@@ -4688,11 +5354,14 @@ class QueryCache {
4688
5354
  }
4689
5355
  }
4690
5356
 
4691
- const IMPORTANT_TOKENS = [chaindataProvider.subNativeTokenId("polkadot"), chaindataProvider.subNativeTokenId("kusama"), chaindataProvider.subNativeTokenId("polkadot-asset-hub"), chaindataProvider.subNativeTokenId("kusama-asset-hub"), chaindataProvider.subNativeTokenId("bittensor")];
4692
- const sortChainsNativeTokensByPriority = (a, b) => {
5357
+ const RELAY_TOKENS = ["polkadot-substrate-native", "kusama-substrate-native"];
5358
+ const PUBLIC_GOODS_TOKENS = ["polkadot-asset-hub-substrate-native", "kusama-asset-hub-substrate-native"];
5359
+ const sortChains = (a, b) => {
4693
5360
  // polkadot and kusama should be checked first
4694
- if (IMPORTANT_TOKENS.includes(a)) return -1;
4695
- if (IMPORTANT_TOKENS.includes(b)) return 1;
5361
+ if (RELAY_TOKENS.includes(a)) return -1;
5362
+ if (RELAY_TOKENS.includes(b)) return 1;
5363
+ if (PUBLIC_GOODS_TOKENS.includes(a)) return -1;
5364
+ if (PUBLIC_GOODS_TOKENS.includes(b)) return 1;
4696
5365
  return 0;
4697
5366
  };
4698
5367
 
@@ -4708,30 +5377,10 @@ class SubNativeBalanceError extends Error {
4708
5377
  }
4709
5378
  }
4710
5379
 
4711
- const DotNetworkPropertiesSimple = z__default.default.object({
4712
- tokenDecimals: z__default.default.number().optional().default(0),
4713
- tokenSymbol: z__default.default.string().optional().default("Unit")
4714
- });
4715
- const DotNetworkPropertiesArray = z__default.default.object({
4716
- tokenDecimals: z__default.default.array(z__default.default.number()).nonempty(),
4717
- tokenSymbol: z__default.default.array(z__default.default.string()).nonempty()
4718
- });
4719
- const DotNetworkPropertiesSchema = z__default.default.union([DotNetworkPropertiesSimple, DotNetworkPropertiesArray]).transform(val => ({
4720
- tokenDecimals: Array.isArray(val.tokenDecimals) ? val.tokenDecimals[0] : val.tokenDecimals,
4721
- tokenSymbol: Array.isArray(val.tokenSymbol) ? val.tokenSymbol[0] : val.tokenSymbol
4722
- }));
4723
- const getChainProperties = async (chainConnector, networkId) => {
4724
- const properties = await chainConnector.send(networkId, "system_properties", [], true);
4725
- return DotNetworkPropertiesSchema.parse(properties);
4726
- };
4727
-
5380
+ const DEFAULT_SYMBOL = "Unit";
5381
+ const DEFAULT_DECIMALS = 0;
4728
5382
  const POLLING_WINDOW_SIZE = 20;
4729
5383
  const MAX_SUBSCRIPTION_SIZE = 40;
4730
- const UNSUPPORTED_CHAIN_META$1 = {
4731
- miniMetadata: null,
4732
- extra: null
4733
- };
4734
- const SubNativeTokenConfigSchema = TokenConfigBaseSchema;
4735
5384
  const SubNativeModule = hydrate => {
4736
5385
  const {
4737
5386
  chainConnectors,
@@ -4739,197 +5388,33 @@ const SubNativeModule = hydrate => {
4739
5388
  } = hydrate;
4740
5389
  const chainConnector$1 = chainConnectors.substrate;
4741
5390
  util$1.assert(chainConnector$1, "This module requires a substrate chain connector");
4742
- const queryCache = new QueryCache(chaindataProvider$1, chainConnector$1);
5391
+ const queryCache = new QueryCache(chaindataProvider$1);
4743
5392
  const getModuleTokens = async () => {
4744
- return await chaindataProvider$1.getTokensMapById(moduleType$2);
4745
- };
4746
-
4747
- // subscribeBalances was split by network to prevent all subs to wait for all minimetadatas to be ready.
4748
- // 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
4749
- // TODO refactor this be actually network specific
4750
- // Note: had to extract this function from the result object or this.subscribeBalances wouldn't be typed correctly
4751
- const subscribeChainBalances = (chainId, opts, callback, signal) => {
4752
- const {
4753
- addressesByToken,
4754
- initialBalances
4755
- } = opts;
4756
- // full record of balances for this module
4757
- const subNativeBalances = new rxjs.BehaviorSubject(Object.fromEntries(initialBalances?.map(b => [getBalanceId(b), b]) ?? []));
4758
- // tokens which have a known positive balance
4759
- const positiveBalanceTokens = subNativeBalances.pipe(rxjs.map(balances => Array.from(new Set(Object.values(balances).map(b => b.tokenId)))), rxjs.share());
4760
-
4761
- // tokens that will be subscribed to, simply a slice of the positive balance tokens of size MAX_SUBSCRIPTION_SIZE
4762
- const subscriptionTokens = positiveBalanceTokens.pipe(rxjs.map(tokens => tokens.sort(sortChainsNativeTokensByPriority).slice(0, MAX_SUBSCRIPTION_SIZE)));
4763
-
4764
- // an initialised balance is one where we have received a response for any type of 'subsource',
4765
- // until then they are initialising. We only need to maintain one map of tokens to addresses for this
4766
- const initialisingBalances = Object.entries(addressesByToken).reduce((acc, [tokenId, addresses]) => {
4767
- acc.set(tokenId, new Set(addresses));
4768
- return acc;
4769
- }, new Map());
4770
-
4771
- // after thirty seconds, we need to kill the initialising balances
4772
- const initBalancesTimeout = setTimeout(() => {
4773
- initialisingBalances.clear();
4774
- // manually call the callback to ensure the caller gets the correct status
4775
- callback(null, {
4776
- status: "live",
4777
- data: Object.values(subNativeBalances.getValue())
4778
- });
4779
- }, 30_000);
4780
- const _callbackSub = subNativeBalances.pipe(rxjs.debounceTime(100)).subscribe({
4781
- next: balances => {
4782
- callback(null, {
4783
- status: initialisingBalances.size > 0 ? "initialising" : "live",
4784
- data: Object.values(balances)
4785
- });
4786
- },
4787
- error: error => callback(error),
4788
- complete: () => {
4789
- initialisingBalances.clear();
4790
- clearTimeout(initBalancesTimeout);
4791
- }
4792
- });
4793
- const unsubDeferred = util.Deferred();
4794
- // we return this to the caller so that they can let us know when they're no longer interested in this subscription
4795
- const callerUnsubscribe = () => {
4796
- subNativeBalances.complete();
4797
- _callbackSub.unsubscribe();
4798
- return unsubDeferred.reject(new Error(`Caller unsubscribed`));
4799
- };
4800
- // we queue up our work to clean up our subscription when this promise rejects
4801
- const callerUnsubscribed = unsubDeferred.promise;
4802
-
4803
- // The update handler is to allow us to merge balances with the same id, and manage initialising and positive balances state for each
4804
- // balance type and network
4805
- const handleUpdateForSource = source => (error, result) => {
4806
- if (result) {
4807
- const currentBalances = subNativeBalances.getValue();
4808
-
4809
- // first merge any balances with the same id within the result
4810
- const accumulatedUpdates = result.filter(b => b.values.length > 0).reduce((acc, b) => {
4811
- const bId = getBalanceId(b);
4812
- acc[bId] = mergeBalances(acc[bId], b, source, false);
4813
- return acc;
4814
- }, {});
4815
-
4816
- // then merge these with the current balances
4817
- const mergedBalances = {};
4818
- Object.entries(accumulatedUpdates).forEach(([bId, b]) => {
4819
- // merge the values from the new balance into the existing balance, if there is one
4820
- mergedBalances[bId] = mergeBalances(currentBalances[bId], b, source, true);
4821
-
4822
- // update initialisingBalances to remove balances which have been updated
4823
- const intialisingForToken = initialisingBalances.get(b.tokenId);
4824
- if (intialisingForToken) {
4825
- intialisingForToken.delete(b.address);
4826
- if (intialisingForToken.size === 0) initialisingBalances.delete(b.tokenId);else initialisingBalances.set(b.tokenId, intialisingForToken);
4827
- }
4828
- });
4829
- subNativeBalances.next({
4830
- ...currentBalances,
4831
- ...mergedBalances
4832
- });
4833
- }
4834
- if (error) {
4835
- if (error instanceof SubNativeBalanceError) {
4836
- // this type of error doesn't need to be handled by the caller
4837
- initialisingBalances.delete(error.tokenId);
4838
- } else return callback(error);
4839
- }
4840
- };
4841
-
4842
- // subscribe to addresses and tokens for which we have a known positive balance
4843
- const positiveSub = subscriptionTokens.pipe(rxjs.debounceTime(1000), rxjs.takeUntil(callerUnsubscribed), rxjs.map(tokenIds => tokenIds.reduce((acc, tokenId) => {
4844
- acc[tokenId] = addressesByToken[tokenId];
4845
- return acc;
4846
- }, {})), rxjs.distinctUntilChanged(isEqual__default.default), rxjs.switchMap(newAddressesByToken => {
4847
- return rxjs.from(queryCache.getQueries(newAddressesByToken)).pipe(rxjs.switchMap(baseQueries => {
4848
- return new rxjs.Observable(subscriber => {
4849
- if (!chainConnectors.substrate) return;
4850
- const unsubSubtensorStaking = subscribeSubtensorStaking(chaindataProvider$1, chainConnectors.substrate, newAddressesByToken, handleUpdateForSource("subtensor-staking"), signal);
4851
- const unsubNompoolStaking = subscribeNompoolStaking(chaindataProvider$1, chainConnectors.substrate, newAddressesByToken, handleUpdateForSource("nompools-staking"), signal);
4852
- const unsubBase = subscribeBase(baseQueries, chainConnectors.substrate, handleUpdateForSource("base"));
4853
- subscriber.add(async () => (await unsubSubtensorStaking)());
4854
- subscriber.add(async () => (await unsubNompoolStaking)());
4855
- subscriber.add(async () => (await unsubBase)());
4856
- });
4857
- }));
4858
- })).subscribe();
4859
-
4860
- // for chains where we don't have a known positive balance, poll rather than subscribe
4861
- const poll = async (addressesByToken = {}) => {
4862
- const handleUpdate = handleUpdateForSource("base");
4863
- try {
4864
- const balances = await fetchBalances(addressesByToken);
4865
- handleUpdate(null, Object.values(balances.toJSON()));
4866
- } catch (error) {
4867
- if (error instanceof chainConnector.ChainConnectionError) {
4868
- // coerce ChainConnection errors into SubNativeBalance errors
4869
- const errorChainId = error.chainId;
4870
- Object.entries(await getModuleTokens()).filter(([, token]) => token.networkId === errorChainId).forEach(([tokenId]) => {
4871
- const wrappedError = new SubNativeBalanceError(tokenId, error.message);
4872
- handleUpdate(wrappedError);
4873
- });
4874
- } else {
4875
- log.error("unknown substrate native balance error", error);
4876
- handleUpdate(error);
4877
- }
4878
- }
4879
- };
4880
- // do one poll to get things started
4881
- const currentBalances = subNativeBalances.getValue();
4882
- const currentTokens = new Set(Object.values(currentBalances).map(b => b.tokenId));
4883
- const nonCurrentTokens = Object.keys(addressesByToken).filter(tokenId => !currentTokens.has(tokenId)).sort(sortChainsNativeTokensByPriority);
4884
-
4885
- // break nonCurrentTokens into chunks of POLLING_WINDOW_SIZE
4886
- const pool = new PQueue__default.default({
4887
- concurrency: POLLING_WINDOW_SIZE
4888
- });
4889
- nonCurrentTokens.forEach(nonCurrentTokenId => pool.add(() => poll({
4890
- [nonCurrentTokenId]: addressesByToken[nonCurrentTokenId]
4891
- }), {
4892
- signal
4893
- }));
4894
-
4895
- // now poll every 30s on chains which are not subscriptionTokens
4896
- // we chunk this observable into batches of positive token ids, to prevent eating all the websocket connections
4897
- const pollingSub = rxjs.interval(30_000) // emit values every 30 seconds
4898
- .pipe(rxjs.takeUntil(callerUnsubscribed), rxjs.withLatestFrom(subscriptionTokens),
4899
- // Combine latest value from subscriptionTokens with each interval tick
4900
- rxjs.map(([, subscribedTokenIds]) =>
4901
- // Filter out tokens that are not subscribed
4902
- Object.keys(addressesByToken).filter(tokenId => !subscribedTokenIds.includes(tokenId))), rxjs.exhaustMap(tokenIds => rxjs.from(util$1.arrayChunk(tokenIds, POLLING_WINDOW_SIZE)).pipe(rxjs.concatMap(async tokenChunk => {
4903
- // tokenChunk is a chunk of tokenIds with size POLLING_WINDOW_SIZE
4904
- const pollingTokenAddresses = Object.fromEntries(tokenChunk.map(tokenId => [tokenId, addressesByToken[tokenId]]));
4905
- await pool.add(() => poll(pollingTokenAddresses), {
4906
- signal
4907
- });
4908
- return true;
4909
- })))).subscribe();
4910
- return () => {
4911
- callerUnsubscribe();
4912
- positiveSub.unsubscribe();
4913
- pollingSub.unsubscribe();
4914
- };
4915
- };
4916
- const fetchBalances = async addressesByToken => {
4917
- util$1.assert(chainConnectors.substrate, "This module requires a substrate chain connector");
4918
- const queries = await queryCache.getQueries(addressesByToken);
4919
- util$1.assert(chainConnectors.substrate, "This module requires a substrate chain connector");
4920
- const result = await new RpcStateQueryHelper(chainConnectors.substrate, queries).fetch();
4921
- return new Balances(result ?? []);
5393
+ return await chaindataProvider$1.tokensByIdForType(moduleType$2);
4922
5394
  };
4923
5395
  return {
4924
5396
  ...DefaultBalanceModule(moduleType$2),
4925
- async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) {
4926
- if (!metadataRpc) return UNSUPPORTED_CHAIN_META$1;
5397
+ async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc, systemProperties) {
5398
+ const isTestnet = (await chaindataProvider$1.chainById(chainId))?.isTestnet || false;
5399
+ if (moduleConfig?.disable === true || metadataRpc === undefined) return {
5400
+ isTestnet
5401
+ };
5402
+
5403
+ //
5404
+ // extract system_properties
5405
+ //
5406
+
5407
+ const {
5408
+ tokenSymbol,
5409
+ tokenDecimals
5410
+ } = systemProperties ?? {};
5411
+ const symbol = (Array.isArray(tokenSymbol) ? tokenSymbol[0] : tokenSymbol) ?? DEFAULT_SYMBOL;
5412
+ const decimals = (Array.isArray(tokenDecimals) ? tokenDecimals[0] : tokenDecimals) ?? DEFAULT_DECIMALS;
4927
5413
 
4928
5414
  //
4929
5415
  // process metadata into SCALE encoders/decoders
4930
5416
  //
4931
5417
  const metadataVersion = scale.getMetadataVersion(metadataRpc);
4932
- if (metadataVersion < 14) return UNSUPPORTED_CHAIN_META$1;
4933
5418
  const metadata = scale.decAnyMetadata(metadataRpc);
4934
5419
  const unifiedMetadata = scale.unifyMetadata(metadata);
4935
5420
 
@@ -4949,6 +5434,7 @@ const SubNativeModule = hydrate => {
4949
5434
  };
4950
5435
  const existentialDeposit = getConstantValue("Balances", "ExistentialDeposit")?.toString();
4951
5436
  const nominationPoolsPalletId = getConstantValue("NominationPools", "PalletId")?.asText();
5437
+ const crowdloanPalletId = getConstantValue("Crowdloan", "PalletId")?.asText();
4952
5438
  const hasSubtensorPallet = getConstantValue("SubtensorModule", "KeySwapCost") !== undefined;
4953
5439
 
4954
5440
  //
@@ -4968,10 +5454,15 @@ const SubNativeModule = hydrate => {
4968
5454
  }, {
4969
5455
  pallet: "Staking",
4970
5456
  items: ["Ledger"]
5457
+ }, {
5458
+ pallet: "Crowdloan",
5459
+ items: ["Funds"]
5460
+ }, {
5461
+ pallet: "Paras",
5462
+ items: ["Parachains"]
4971
5463
  },
4972
5464
  // TotalColdkeyStake is used until v.2.2.1, then it is replaced by StakingHotkeys+Stake
4973
5465
  // Need to keep TotalColdkeyStake for a while so chaindata keeps including it in miniMetadatas, so it doesnt break old versions of the wallet
4974
- // TODO: Since chaindata v4 this is safe to now delete
4975
5466
  {
4976
5467
  pallet: "SubtensorModule",
4977
5468
  items: ["TotalColdkeyStake", "StakingHotkeys", "Stake"]
@@ -4990,40 +5481,47 @@ const SubNativeModule = hydrate => {
4990
5481
  }) => name === "Freezes"));
4991
5482
  const useLegacyTransferableCalculation = !hasFreezesItem;
4992
5483
  const chainMeta = {
5484
+ isTestnet,
5485
+ useLegacyTransferableCalculation,
5486
+ symbol,
5487
+ decimals,
5488
+ existentialDeposit,
5489
+ nominationPoolsPalletId,
5490
+ crowdloanPalletId,
5491
+ hasSubtensorPallet,
4993
5492
  miniMetadata,
4994
- extra: {
4995
- useLegacyTransferableCalculation,
4996
- existentialDeposit,
4997
- nominationPoolsPalletId,
4998
- hasSubtensorPallet
4999
- }
5493
+ metadataVersion
5000
5494
  };
5001
- if (!useLegacyTransferableCalculation) delete chainMeta.extra?.useLegacyTransferableCalculation;
5002
- if (!hasSubtensorPallet) delete chainMeta.extra?.hasSubtensorPallet;
5495
+ if (!useLegacyTransferableCalculation) delete chainMeta.useLegacyTransferableCalculation;
5496
+ if (!hasSubtensorPallet) delete chainMeta.hasSubtensorPallet;
5003
5497
  return chainMeta;
5004
5498
  },
5005
5499
  async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) {
5006
5500
  if (moduleConfig?.disable === true) return {};
5007
5501
  const {
5008
- tokenSymbol: symbol,
5009
- tokenDecimals: decimals
5010
- } = await getChainProperties(chainConnector$1, chainId);
5011
- const {
5502
+ isTestnet,
5503
+ symbol,
5504
+ decimals,
5012
5505
  existentialDeposit
5013
- } = chainMeta.extra ?? {};
5014
- if (existentialDeposit === undefined) log.warn("Substrate native module: existentialDeposit is undefined for %s, using 0", chainId);
5015
- const id = chaindataProvider.subNativeTokenId(chainId);
5506
+ } = chainMeta;
5507
+ const id = subNativeTokenId(chainId);
5016
5508
  const nativeToken = {
5017
5509
  id,
5018
5510
  type: "substrate-native",
5019
- platform: "polkadot",
5020
- isDefault: true,
5021
- symbol: symbol,
5022
- name: symbol,
5023
- decimals: decimals,
5511
+ isTestnet,
5512
+ isDefault: moduleConfig?.isDefault ?? true,
5513
+ symbol: symbol ?? DEFAULT_SYMBOL,
5514
+ decimals: decimals ?? DEFAULT_DECIMALS,
5515
+ logo: moduleConfig?.logo || chaindataProvider.githubTokenLogoUrl(id),
5024
5516
  existentialDeposit: existentialDeposit ?? "0",
5025
- networkId: chainId
5517
+ chain: {
5518
+ id: chainId
5519
+ }
5026
5520
  };
5521
+ if (moduleConfig?.symbol) nativeToken.symbol = moduleConfig?.symbol;
5522
+ if (moduleConfig?.coingeckoId) nativeToken.coingeckoId = moduleConfig?.coingeckoId;
5523
+ if (moduleConfig?.dcentName) nativeToken.dcentName = moduleConfig?.dcentName;
5524
+ if (moduleConfig?.mirrorOf) nativeToken.mirrorOf = moduleConfig?.mirrorOf;
5027
5525
  return {
5028
5526
  [nativeToken.id]: nativeToken
5029
5527
  };
@@ -5033,36 +5531,169 @@ const SubNativeModule = hydrate => {
5033
5531
  initialBalances
5034
5532
  }, callback) {
5035
5533
  util$1.assert(chainConnectors.substrate, "This module requires a substrate chain connector");
5036
- const addressesByTokenByNetwork = getAddresssesByTokenByNetwork(addressesByToken);
5037
- const initialBalancesByNetwork = lodash.groupBy(initialBalances ?? [], "networkId");
5038
- const controller = new AbortController();
5039
- const safeCallback = (error, result) => {
5040
- if (controller.signal.aborted) return;
5041
- if (util.isAbortError(error)) return;
5042
- // typescript isnt happy with fowarding parameters as is
5043
- return error ? callback(error, undefined) : callback(error, result);
5534
+
5535
+ // full record of balances for this module
5536
+ const subNativeBalances = new rxjs.BehaviorSubject(Object.fromEntries(initialBalances?.map(b => [getBalanceId(b), b]) ?? []));
5537
+ // tokens which have a known positive balance
5538
+ const positiveBalanceTokens = subNativeBalances.pipe(rxjs.map(balances => Array.from(new Set(Object.values(balances).map(b => b.tokenId)))), rxjs.share());
5539
+
5540
+ // tokens that will be subscribed to, simply a slice of the positive balance tokens of size MAX_SUBSCRIPTION_SIZE
5541
+ const subscriptionTokens = positiveBalanceTokens.pipe(rxjs.map(tokens => tokens.sort(sortChains).slice(0, MAX_SUBSCRIPTION_SIZE)));
5542
+
5543
+ // an initialised balance is one where we have received a response for any type of 'subsource',
5544
+ // until then they are initialising. We only need to maintain one map of tokens to addresses for this
5545
+ const initialisingBalances = Object.entries(addressesByToken).reduce((acc, [tokenId, addresses]) => {
5546
+ acc.set(tokenId, new Set(addresses));
5547
+ return acc;
5548
+ }, new Map());
5549
+
5550
+ // after thirty seconds, we need to kill the initialising balances
5551
+ const initBalancesTimeout = setTimeout(() => {
5552
+ initialisingBalances.clear();
5553
+ // manually call the callback to ensure the caller gets the correct status
5554
+ callback(null, {
5555
+ status: "live",
5556
+ data: Object.values(subNativeBalances.getValue())
5557
+ });
5558
+ }, 30_000);
5559
+ const _callbackSub = subNativeBalances.pipe(rxjs.debounceTime(100)).subscribe({
5560
+ next: balances => {
5561
+ callback(null, {
5562
+ status: initialisingBalances.size > 0 ? "initialising" : "live",
5563
+ data: Object.values(balances)
5564
+ });
5565
+ },
5566
+ error: error => callback(error),
5567
+ complete: () => {
5568
+ initialisingBalances.clear();
5569
+ clearTimeout(initBalancesTimeout);
5570
+ }
5571
+ });
5572
+ const unsubDeferred = util.Deferred();
5573
+ // we return this to the caller so that they can let us know when they're no longer interested in this subscription
5574
+ const callerUnsubscribe = () => {
5575
+ subNativeBalances.complete();
5576
+ _callbackSub.unsubscribe();
5577
+ return unsubDeferred.reject(new Error(`Caller unsubscribed`));
5044
5578
  };
5045
- const unsubsribeFns = Promise.all(lodash.keys(addressesByTokenByNetwork).map(async networkId => {
5579
+ // we queue up our work to clean up our subscription when this promise rejects
5580
+ const callerUnsubscribed = unsubDeferred.promise;
5581
+
5582
+ // The update handler is to allow us to merge balances with the same id, and manage initialising and positive balances state for each
5583
+ // balance type and network
5584
+ const handleUpdateForSource = source => (error, result) => {
5585
+ if (result) {
5586
+ const currentBalances = subNativeBalances.getValue();
5587
+
5588
+ // first merge any balances with the same id within the result
5589
+ const accumulatedUpdates = result.filter(b => b.values.length > 0).reduce((acc, b) => {
5590
+ const bId = getBalanceId(b);
5591
+ acc[bId] = mergeBalances(acc[bId], b, source, false);
5592
+ return acc;
5593
+ }, {});
5594
+
5595
+ // then merge these with the current balances
5596
+ const mergedBalances = {};
5597
+ Object.entries(accumulatedUpdates).forEach(([bId, b]) => {
5598
+ // merge the values from the new balance into the existing balance, if there is one
5599
+ mergedBalances[bId] = mergeBalances(currentBalances[bId], b, source, true);
5600
+
5601
+ // update initialisingBalances to remove balances which have been updated
5602
+ const intialisingForToken = initialisingBalances.get(b.tokenId);
5603
+ if (intialisingForToken) {
5604
+ intialisingForToken.delete(b.address);
5605
+ if (intialisingForToken.size === 0) initialisingBalances.delete(b.tokenId);else initialisingBalances.set(b.tokenId, intialisingForToken);
5606
+ }
5607
+ });
5608
+ subNativeBalances.next({
5609
+ ...currentBalances,
5610
+ ...mergedBalances
5611
+ });
5612
+ }
5613
+ if (error) {
5614
+ if (error instanceof SubNativeBalanceError) {
5615
+ // this type of error doesn't need to be handled by the caller
5616
+ initialisingBalances.delete(error.tokenId);
5617
+ } else return callback(error);
5618
+ }
5619
+ };
5620
+
5621
+ // subscribe to addresses and tokens for which we have a known positive balance
5622
+ const positiveSub = subscriptionTokens.pipe(rxjs.debounceTime(1000), rxjs.takeUntil(callerUnsubscribed), rxjs.map(tokenIds => tokenIds.reduce((acc, tokenId) => {
5623
+ acc[tokenId] = addressesByToken[tokenId];
5624
+ return acc;
5625
+ }, {})), rxjs.distinctUntilChanged(isEqual__default.default), rxjs.switchMap(newAddressesByToken => {
5626
+ return rxjs.from(queryCache.getQueries(newAddressesByToken)).pipe(rxjs.switchMap(baseQueries => {
5627
+ return new rxjs.Observable(subscriber => {
5628
+ if (!chainConnectors.substrate) return;
5629
+ const unsubSubtensorStaking = subscribeSubtensorStaking(chaindataProvider$1, chainConnectors.substrate, newAddressesByToken, handleUpdateForSource("subtensor-staking"));
5630
+ const unsubNompoolStaking = subscribeNompoolStaking(chaindataProvider$1, chainConnectors.substrate, newAddressesByToken, handleUpdateForSource("nompools-staking"));
5631
+ const unsubCrowdloans = subscribeCrowdloans(chaindataProvider$1, chainConnectors.substrate, newAddressesByToken, handleUpdateForSource("crowdloan"));
5632
+ const unsubBase = subscribeBase(baseQueries, chainConnectors.substrate, handleUpdateForSource("base"));
5633
+ subscriber.add(async () => (await unsubSubtensorStaking)());
5634
+ subscriber.add(async () => (await unsubNompoolStaking)());
5635
+ subscriber.add(async () => (await unsubCrowdloans)());
5636
+ subscriber.add(async () => (await unsubBase)());
5637
+ });
5638
+ }));
5639
+ })).subscribe();
5640
+
5641
+ // for chains where we don't have a known positive balance, poll rather than subscribe
5642
+ const poll = async (addressesByToken = {}) => {
5643
+ const handleUpdate = handleUpdateForSource("base");
5046
5644
  try {
5047
- // this is what we want to be done separately for each network
5048
- // this will update the DB so minimetadata will be available when it's used, everywhere else down the tree of subscribeChainBalances
5049
- await getMiniMetadata(chaindataProvider$1, chainConnector$1, networkId, moduleType$2, controller.signal);
5050
- } catch (err) {
5051
- if (!util.isAbortError(err)) log.warn("Failed to get native token miniMetadata for network", networkId, err);
5052
- return () => {};
5645
+ const balances = await this.fetchBalances(addressesByToken);
5646
+ handleUpdate(null, Object.values(balances.toJSON()));
5647
+ } catch (error) {
5648
+ if (error instanceof chainConnector.ChainConnectionError) {
5649
+ // coerce ChainConnection errors into SubNativeBalance errors
5650
+ const errorChainId = error.chainId;
5651
+ Object.entries(await getModuleTokens()).filter(([, token]) => token.chain?.id === errorChainId).forEach(([tokenId]) => {
5652
+ const wrappedError = new SubNativeBalanceError(tokenId, error.message);
5653
+ handleUpdate(wrappedError);
5654
+ });
5655
+ } else {
5656
+ log.error("unknown substrate native balance error", error);
5657
+ handleUpdate(error);
5658
+ }
5053
5659
  }
5054
- if (controller.signal.aborted) return () => {};
5055
- return subscribeChainBalances(networkId, {
5056
- addressesByToken: addressesByTokenByNetwork[networkId] ?? {},
5057
- initialBalances: initialBalancesByNetwork[networkId] ?? []
5058
- }, safeCallback, controller.signal);
5660
+ };
5661
+ // do one poll to get things started
5662
+ const currentBalances = subNativeBalances.getValue();
5663
+ const currentTokens = new Set(Object.values(currentBalances).map(b => b.tokenId));
5664
+ const nonCurrentTokens = Object.keys(addressesByToken).filter(tokenId => !currentTokens.has(tokenId)).sort(sortChains);
5665
+
5666
+ // break nonCurrentTokens into chunks of POLLING_WINDOW_SIZE
5667
+ await PromisePool__default.default.withConcurrency(POLLING_WINDOW_SIZE).for(nonCurrentTokens).process(async nonCurrentTokenId => await poll({
5668
+ [nonCurrentTokenId]: addressesByToken[nonCurrentTokenId]
5059
5669
  }));
5670
+
5671
+ // now poll every 30s on chains which are not subscriptionTokens
5672
+ // we chunk this observable into batches of positive token ids, to prevent eating all the websocket connections
5673
+ const pollingSub = rxjs.interval(30_000) // emit values every 30 seconds
5674
+ .pipe(rxjs.takeUntil(callerUnsubscribed), rxjs.withLatestFrom(subscriptionTokens),
5675
+ // Combine latest value from subscriptionTokens with each interval tick
5676
+ rxjs.map(([, subscribedTokenIds]) =>
5677
+ // Filter out tokens that are not subscribed
5678
+ Object.keys(addressesByToken).filter(tokenId => !subscribedTokenIds.includes(tokenId))), rxjs.exhaustMap(tokenIds => rxjs.from(util$1.arrayChunk(tokenIds, POLLING_WINDOW_SIZE)).pipe(rxjs.concatMap(async tokenChunk => {
5679
+ // tokenChunk is a chunk of tokenIds with size POLLING_WINDOW_SIZE
5680
+ const pollingTokenAddresses = Object.fromEntries(tokenChunk.map(tokenId => [tokenId, addressesByToken[tokenId]]));
5681
+ await poll(pollingTokenAddresses);
5682
+ return true;
5683
+ })))).subscribe();
5060
5684
  return () => {
5061
- unsubsribeFns.then(fns => fns.forEach(unsubscribe => unsubscribe()));
5062
- controller.abort();
5685
+ callerUnsubscribe();
5686
+ positiveSub.unsubscribe();
5687
+ pollingSub.unsubscribe();
5063
5688
  };
5064
5689
  },
5065
- fetchBalances,
5690
+ async fetchBalances(addressesByToken) {
5691
+ util$1.assert(chainConnectors.substrate, "This module requires a substrate chain connector");
5692
+ const queries = await queryCache.getQueries(addressesByToken);
5693
+ util$1.assert(chainConnectors.substrate, "This module requires a substrate chain connector");
5694
+ const result = await new RpcStateQueryHelper(chainConnectors.substrate, queries).fetch();
5695
+ return new Balances(result ?? []);
5696
+ },
5066
5697
  async transferToken({
5067
5698
  tokenId,
5068
5699
  from,
@@ -5079,10 +5710,11 @@ const SubNativeModule = hydrate => {
5079
5710
  transferMethod,
5080
5711
  userExtensions
5081
5712
  }) {
5082
- const token = await chaindataProvider$1.getTokenById(tokenId, "substrate-native");
5713
+ const token = await chaindataProvider$1.tokenById(tokenId);
5083
5714
  util$1.assert(token, `Token ${tokenId} not found in store`);
5084
- const chainId = token.networkId;
5085
- const chain = await chaindataProvider$1.getNetworkById(chainId, "polkadot");
5715
+ if (token.type !== "substrate-native") throw new Error(`This module doesn't handle tokens of type ${token.type}`);
5716
+ const chainId = token.chain.id;
5717
+ const chain = await chaindataProvider$1.chainById(chainId);
5086
5718
  util$1.assert(chain?.genesisHash, `Chain ${chainId} not found in store`);
5087
5719
  const {
5088
5720
  genesisHash
@@ -6276,10 +6908,7 @@ var psp22Abi = {
6276
6908
  };
6277
6909
 
6278
6910
  const moduleType$1 = "substrate-psp22";
6279
- const SubPsp22TokenConfigSchema = z__default.default.strictObject({
6280
- contractAddress: chaindataProvider.SubPsp22TokenSchema.shape.contractAddress,
6281
- ...TokenConfigBaseSchema.shape
6282
- });
6911
+ const subPsp22TokenId = (chainId, tokenSymbol) => `${chainId}-substrate-psp22-${tokenSymbol}`.toLowerCase().replace(/ /g, "-");
6283
6912
  const SubPsp22Module = hydrate => {
6284
6913
  const {
6285
6914
  chainConnectors,
@@ -6289,15 +6918,16 @@ const SubPsp22Module = hydrate => {
6289
6918
  util$1.assert(chainConnector, "This module requires a substrate chain connector");
6290
6919
  return {
6291
6920
  ...DefaultBalanceModule(moduleType$1),
6292
- async fetchSubstrateChainMeta(_chainId) {
6293
- // we dont need anything
6921
+ async fetchSubstrateChainMeta(chainId) {
6922
+ const isTestnet = (await chaindataProvider$1.chainById(chainId))?.isTestnet || false;
6294
6923
  return {
6295
- miniMetadata: null,
6296
- extra: null
6924
+ isTestnet
6297
6925
  };
6298
6926
  },
6299
- async fetchSubstrateChainTokens(chainId, _chainMeta, moduleConfig, tokens) {
6300
- if (!tokens?.length) return {};
6927
+ async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) {
6928
+ const {
6929
+ isTestnet
6930
+ } = chainMeta;
6301
6931
  const registry = new types.TypeRegistry();
6302
6932
  const Psp22Abi = new apiContract.Abi(psp22Abi);
6303
6933
 
@@ -6307,11 +6937,12 @@ const SubPsp22Module = hydrate => {
6307
6937
  chainId,
6308
6938
  registry
6309
6939
  });
6310
- const tokenList = {};
6311
- for (const tokenConfig of tokens ?? []) {
6940
+ const tokens = {};
6941
+ for (const tokenConfig of moduleConfig?.tokens ?? []) {
6312
6942
  try {
6313
6943
  let symbol = tokenConfig?.symbol ?? "Unit";
6314
6944
  let decimals = tokenConfig?.decimals ?? 0;
6945
+ const existentialDeposit = tokenConfig?.ed ?? "0";
6315
6946
  const contractAddress = tokenConfig?.contractAddress ?? undefined;
6316
6947
  if (contractAddress === undefined) continue;
6317
6948
  await (async () => {
@@ -6327,28 +6958,35 @@ const SubPsp22Module = hydrate => {
6327
6958
  const decimalsData = decimalsResult.toJSON()?.result?.ok?.data;
6328
6959
  decimals = typeof decimalsData === "string" && decimalsData.startsWith("0x") ? util$1.hexToNumber(decimalsData) : decimals;
6329
6960
  })();
6330
- const id = chaindataProvider.subPsp22TokenId(chainId, contractAddress);
6961
+ const id = subPsp22TokenId(chainId, symbol);
6331
6962
  const token = {
6332
6963
  id,
6333
6964
  type: "substrate-psp22",
6334
- platform: "polkadot",
6965
+ isTestnet,
6335
6966
  isDefault: tokenConfig.isDefault ?? true,
6336
6967
  symbol,
6337
6968
  decimals,
6338
- name: tokenConfig?.name || symbol,
6339
- logo: tokenConfig?.logo,
6969
+ logo: tokenConfig?.logo || chaindataProvider.githubTokenLogoUrl(id),
6970
+ existentialDeposit,
6340
6971
  contractAddress,
6341
- networkId: chainId
6972
+ chain: {
6973
+ id: chainId
6974
+ }
6342
6975
  };
6976
+ if (tokenConfig?.symbol) {
6977
+ token.symbol = tokenConfig?.symbol;
6978
+ token.id = subPsp22TokenId(chainId, token.symbol);
6979
+ }
6343
6980
  if (tokenConfig?.coingeckoId) token.coingeckoId = tokenConfig?.coingeckoId;
6981
+ if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName;
6344
6982
  if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf;
6345
- tokenList[token.id] = token;
6983
+ tokens[token.id] = token;
6346
6984
  } catch (error) {
6347
6985
  log.error(`Failed to build substrate-psp22 token ${tokenConfig.contractAddress} (${tokenConfig.symbol}) on ${chainId}`, error?.message ?? error);
6348
6986
  continue;
6349
6987
  }
6350
6988
  }
6351
- return tokenList;
6989
+ return tokens;
6352
6990
  },
6353
6991
  // TODO: Don't create empty subscriptions
6354
6992
  async subscribeBalances({
@@ -6358,7 +6996,7 @@ const SubPsp22Module = hydrate => {
6358
6996
  const subscriptionInterval = 12_000; // 12_000ms == 12 seconds
6359
6997
  const initDelay = 3_000; // 3000ms == 3 seconds
6360
6998
  const cache = new Map();
6361
- const tokens = await chaindataProvider$1.getTokensMapById();
6999
+ const tokens = await chaindataProvider$1.tokensById();
6362
7000
  const poll = async () => {
6363
7001
  if (!subscriptionActive) return;
6364
7002
  try {
@@ -6385,7 +7023,7 @@ const SubPsp22Module = hydrate => {
6385
7023
  },
6386
7024
  async fetchBalances(addressesByToken) {
6387
7025
  util$1.assert(chainConnectors.substrate, "This module requires a substrate chain connector");
6388
- const tokens = await chaindataProvider$1.getTokensMapById();
7026
+ const tokens = await chaindataProvider$1.tokensById();
6389
7027
  return fetchBalances(chainConnectors.substrate, tokens, addressesByToken);
6390
7028
  },
6391
7029
  async transferToken({
@@ -6403,11 +7041,11 @@ const SubPsp22Module = hydrate => {
6403
7041
  tip,
6404
7042
  userExtensions
6405
7043
  }) {
6406
- const token = await chaindataProvider$1.getTokenById(tokenId, "substrate-psp22");
7044
+ const token = await chaindataProvider$1.tokenById(tokenId);
6407
7045
  util$1.assert(token, `Token ${tokenId} not found in store`);
6408
7046
  if (token.type !== "substrate-psp22") throw new Error(`This module doesn't handle tokens of type ${token.type}`);
6409
- const chainId = token.networkId;
6410
- const chain = await chaindataProvider$1.getNetworkById(chainId, "polkadot");
7047
+ const chainId = token.chain.id;
7048
+ const chain = await chaindataProvider$1.chainById(chainId);
6411
7049
  util$1.assert(chain?.genesisHash, `Chain ${chainId} not found in store`);
6412
7050
  const {
6413
7051
  genesisHash
@@ -6483,7 +7121,7 @@ const fetchBalances = async (chainConnector, tokens, addressesByToken) => {
6483
7121
  // TODO: Use `decodeOutput` from `./util/decodeOutput`
6484
7122
  const contractCall = makeContractCaller({
6485
7123
  chainConnector,
6486
- chainId: token.networkId,
7124
+ chainId: token.chain.id,
6487
7125
  registry
6488
7126
  });
6489
7127
  if (token.contractAddress === undefined) {
@@ -6500,7 +7138,10 @@ const fetchBalances = async (chainConnector, tokens, addressesByToken) => {
6500
7138
  source: "substrate-psp22",
6501
7139
  status: "live",
6502
7140
  address,
6503
- networkId: token.networkId,
7141
+ multiChainId: {
7142
+ subChainId: token.chain.id
7143
+ },
7144
+ chainId: token.chain.id,
6504
7145
  tokenId,
6505
7146
  value: balance
6506
7147
  };
@@ -6523,16 +7164,8 @@ const fetchBalances = async (chainConnector, tokens, addressesByToken) => {
6523
7164
  };
6524
7165
 
6525
7166
  const moduleType = "substrate-tokens";
6526
- const SubTokensTokenConfigSchema = z__default.default.strictObject({
6527
- onChainId: chaindataProvider.SubTokensTokenSchema.shape.onChainId,
6528
- ...TokenConfigBaseSchema.shape,
6529
- existentialDeposit: chaindataProvider.SubTokensTokenSchema.shape.existentialDeposit.optional()
6530
- });
6531
7167
  const defaultPalletId = "Tokens";
6532
- const UNSUPPORTED_CHAIN_META = {
6533
- miniMetadata: null,
6534
- extra: {}
6535
- };
7168
+ const subTokensTokenId = (chainId, onChainId) => `${chainId}-substrate-tokens-${lzString.compressToEncodedURIComponent(String(onChainId))}`;
6536
7169
  const SubTokensModule = hydrate => {
6537
7170
  const {
6538
7171
  chainConnectors,
@@ -6543,7 +7176,14 @@ const SubTokensModule = hydrate => {
6543
7176
  return {
6544
7177
  ...DefaultBalanceModule(moduleType),
6545
7178
  async fetchSubstrateChainMeta(chainId, moduleConfig, metadataRpc) {
6546
- if (metadataRpc === undefined) return UNSUPPORTED_CHAIN_META;
7179
+ const isTestnet = (await chaindataProvider$1.chainById(chainId))?.isTestnet || false;
7180
+ if (metadataRpc === undefined) return {
7181
+ isTestnet
7182
+ };
7183
+ if ((moduleConfig?.tokens ?? []).length < 1) return {
7184
+ isTestnet
7185
+ };
7186
+ const metadataVersion = scale.getMetadataVersion(metadataRpc);
6547
7187
  const metadata = scale.decAnyMetadata(metadataRpc);
6548
7188
  const palletId = moduleConfig?.palletId ?? defaultPalletId;
6549
7189
  scale.compactMetadata(metadata, [{
@@ -6551,83 +7191,74 @@ const SubTokensModule = hydrate => {
6551
7191
  items: ["Accounts"]
6552
7192
  }]);
6553
7193
  const miniMetadata = scale.encodeMetadata(metadata);
6554
- return {
7194
+ return palletId === defaultPalletId ? {
7195
+ isTestnet,
6555
7196
  miniMetadata,
6556
- extra: {
6557
- palletId
6558
- }
7197
+ metadataVersion
7198
+ } : {
7199
+ isTestnet,
7200
+ palletId,
7201
+ miniMetadata,
7202
+ metadataVersion
6559
7203
  };
6560
7204
  },
6561
- async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig, tokens) {
6562
- const tokenList = {};
6563
- for (const tokenConfig of tokens ?? []) {
7205
+ async fetchSubstrateChainTokens(chainId, chainMeta, moduleConfig) {
7206
+ const {
7207
+ isTestnet
7208
+ } = chainMeta;
7209
+ const tokens = {};
7210
+ for (const tokenConfig of moduleConfig?.tokens ?? []) {
6564
7211
  try {
6565
- // TODO fetch metadata from chain, like we do for assets
6566
7212
  const symbol = tokenConfig?.symbol ?? "Unit";
6567
7213
  const decimals = tokenConfig?.decimals ?? 0;
6568
- const existentialDeposit = tokenConfig?.existentialDeposit ?? "0";
7214
+ const existentialDeposit = tokenConfig?.ed ?? "0";
6569
7215
  const onChainId = tokenConfig?.onChainId ?? undefined;
6570
7216
  if (onChainId === undefined) continue;
6571
- const id = chaindataProvider.subTokensTokenId(chainId, onChainId);
7217
+ const id = subTokensTokenId(chainId, onChainId);
6572
7218
  const token = {
6573
7219
  id,
6574
7220
  type: "substrate-tokens",
6575
- platform: "polkadot",
7221
+ isTestnet,
6576
7222
  isDefault: tokenConfig.isDefault ?? true,
6577
7223
  symbol,
6578
7224
  decimals,
6579
- name: tokenConfig?.name ?? symbol,
6580
- logo: tokenConfig?.logo,
7225
+ logo: tokenConfig?.logo || chaindataProvider.githubTokenLogoUrl(id),
6581
7226
  existentialDeposit,
6582
7227
  onChainId,
6583
- networkId: chainId
7228
+ chain: {
7229
+ id: chainId
7230
+ }
6584
7231
  };
7232
+ if (tokenConfig?.symbol) {
7233
+ token.symbol = tokenConfig?.symbol;
7234
+ token.id = subTokensTokenId(chainId, token.onChainId);
7235
+ }
6585
7236
  if (tokenConfig?.coingeckoId) token.coingeckoId = tokenConfig?.coingeckoId;
7237
+ if (tokenConfig?.dcentName) token.dcentName = tokenConfig?.dcentName;
6586
7238
  if (tokenConfig?.mirrorOf) token.mirrorOf = tokenConfig?.mirrorOf;
6587
- tokenList[token.id] = token;
7239
+ tokens[token.id] = token;
6588
7240
  } catch (error) {
6589
7241
  log.error(`Failed to build substrate-tokens token ${tokenConfig.onChainId} (${tokenConfig.symbol}) on ${chainId}`, error?.message ?? error);
6590
7242
  continue;
6591
7243
  }
6592
7244
  }
6593
- return tokenList;
7245
+ return tokens;
6594
7246
  },
6595
7247
  // TODO: Don't create empty subscriptions
6596
7248
  async subscribeBalances({
6597
7249
  addressesByToken
6598
7250
  }, callback) {
6599
- const byNetwork = lodash.keys(addressesByToken).reduce((acc, tokenId) => {
6600
- const networkId = chaindataProvider.parseSubTokensTokenId(tokenId).networkId;
6601
- if (!acc[networkId]) acc[networkId] = {};
6602
- acc[networkId][tokenId] = addressesByToken[tokenId];
6603
- return acc;
6604
- }, {});
6605
- const controller = new AbortController();
6606
- const pUnsubs = Promise.all(lodash.toPairs(byNetwork).map(async ([networkId, addressesByToken]) => {
6607
- try {
6608
- const queries = await buildNetworkQueries(networkId, chainConnector, chaindataProvider$1, addressesByToken, controller.signal);
6609
- if (controller.signal.aborted) return () => {};
6610
- const stateHelper = new RpcStateQueryHelper(chainConnector, queries);
6611
- return await stateHelper.subscribe((error, result) => {
6612
- if (error) return callback(error);
6613
- const balances = result?.filter(b => b !== null) ?? [];
6614
- if (balances.length > 0) callback(null, new Balances(balances));
6615
- });
6616
- } catch (err) {
6617
- if (!util.isAbortError(err)) log.error(`Failed to subscribe balances for network ${networkId}`, err);
6618
- return () => {};
6619
- }
6620
- }));
6621
- return () => {
6622
- pUnsubs.then(unsubs => {
6623
- unsubs.forEach(unsubscribe => unsubscribe());
6624
- });
6625
- controller.abort();
6626
- };
7251
+ const queries = await buildQueries(chaindataProvider$1, addressesByToken);
7252
+ const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe((error, result) => {
7253
+ if (error) return callback(error);
7254
+ const balances = result?.filter(b => b !== null) ?? [];
7255
+ if (balances.length > 0) callback(null, new Balances(balances));
7256
+ });
7257
+ return unsubscribe;
6627
7258
  },
6628
7259
  async fetchBalances(addressesByToken) {
6629
7260
  util$1.assert(chainConnectors.substrate, "This module requires a substrate chain connector");
6630
- const queries = await buildQueries(chainConnector, chaindataProvider$1, addressesByToken);
7261
+ const queries = await buildQueries(chaindataProvider$1, addressesByToken);
6631
7262
  const result = await new RpcStateQueryHelper(chainConnectors.substrate, queries).fetch();
6632
7263
  const balances = result?.filter(b => b !== null) ?? [];
6633
7264
  return new Balances(balances);
@@ -6639,13 +7270,15 @@ const SubTokensModule = hydrate => {
6639
7270
  transferMethod,
6640
7271
  metadataRpc
6641
7272
  }) {
6642
- const token = await chaindataProvider$1.getTokenById(tokenId, "substrate-tokens");
7273
+ const token = await chaindataProvider$1.tokenById(tokenId);
6643
7274
  util$1.assert(token, `Token ${tokenId} not found in store`);
6644
- const chainId = token.networkId;
6645
- const chain = await chaindataProvider$1.getNetworkById(chainId, "polkadot");
7275
+ if (token.type !== "substrate-tokens") throw new Error(`This module doesn't handle tokens of type ${token.type}`);
7276
+ const chainId = token.chain.id;
7277
+ const chain = await chaindataProvider$1.chainById(chainId);
6646
7278
  util$1.assert(chain?.genesisHash, `Chain ${chainId} not found in store`);
6647
- const miniMetadata = await getMiniMetadata(chaindataProvider$1, chainConnector, chainId, moduleType);
6648
- const tokensPallet = miniMetadata?.extra?.palletId ?? defaultPalletId;
7279
+ const miniMetadatas = new Map((await db.miniMetadatas.toArray()).map(miniMetadata => [miniMetadata.id, miniMetadata]));
7280
+ const [chainMeta] = findChainMeta(miniMetadatas, moduleType, chain);
7281
+ const tokensPallet = chainMeta?.palletId ?? defaultPalletId;
6649
7282
  const onChainId = (() => {
6650
7283
  try {
6651
7284
  return scale.papiParse(token.onChainId);
@@ -6744,15 +7377,23 @@ const SubTokensModule = hydrate => {
6744
7377
  }
6745
7378
  };
6746
7379
  };
6747
- async function buildNetworkQueries(networkId, chainConnector, chaindataProvider, addressesByToken, signal) {
6748
- const miniMetadata = await getMiniMetadata(chaindataProvider, chainConnector, networkId, moduleType, signal);
6749
- const chain = await chaindataProvider.getNetworkById(networkId, "polkadot");
6750
- const tokens = await chaindataProvider.getTokensMapById();
6751
- if (!chain) return [];
6752
- signal?.throwIfAborted();
6753
- const palletId = miniMetadata.extra.palletId ?? defaultPalletId;
6754
- const networkStorageCoders = buildNetworkStorageCoders(networkId, miniMetadata, {
6755
- storage: [palletId, "Accounts"]
7380
+ async function buildQueries(chaindataProvider, addressesByToken) {
7381
+ const allChains = await chaindataProvider.chainsById();
7382
+ const tokens = await chaindataProvider.tokensById();
7383
+ const miniMetadatas = new Map((await db.miniMetadatas.toArray()).map(miniMetadata => [miniMetadata.id, miniMetadata]));
7384
+ const tokensPalletByChain = new Map(Object.values(allChains).map(chain => [chain.id, findChainMeta(miniMetadatas, moduleType, chain)[0]?.palletId]));
7385
+ const uniqueChainIds = getUniqueChainIds(addressesByToken, tokens);
7386
+ const chains = Object.fromEntries(uniqueChainIds.map(chainId => [chainId, allChains[chainId]]));
7387
+ const chainStorageCoders = buildStorageCoders({
7388
+ chainIds: uniqueChainIds,
7389
+ chains,
7390
+ miniMetadatas,
7391
+ moduleType: "substrate-tokens",
7392
+ coders: {
7393
+ storage: ({
7394
+ chainId
7395
+ }) => [tokensPalletByChain.get(chainId) ?? defaultPalletId, "Accounts"]
7396
+ }
6756
7397
  });
6757
7398
  return Object.entries(addressesByToken).flatMap(([tokenId, addresses]) => {
6758
7399
  const token = tokens[tokenId];
@@ -6764,8 +7405,18 @@ async function buildNetworkQueries(networkId, chainConnector, chaindataProvider,
6764
7405
  log.debug(`This module doesn't handle tokens of type ${token.type}`);
6765
7406
  return [];
6766
7407
  }
7408
+ const chainId = token.chain?.id;
7409
+ if (!chainId) {
7410
+ log.warn(`Token ${tokenId} has no chain`);
7411
+ return [];
7412
+ }
7413
+ const chain = chains[chainId];
7414
+ if (!chain) {
7415
+ log.warn(`Chain ${chainId} for token ${tokenId} not found`);
7416
+ return [];
7417
+ }
6767
7418
  return addresses.flatMap(address => {
6768
- const scaleCoder = networkStorageCoders?.storage;
7419
+ const scaleCoder = chainStorageCoders.get(chainId)?.storage;
6769
7420
  const onChainId = (() => {
6770
7421
  try {
6771
7422
  return scale.papiParse(token.onChainId);
@@ -6773,12 +7424,12 @@ async function buildNetworkQueries(networkId, chainConnector, chaindataProvider,
6773
7424
  return token.onChainId;
6774
7425
  }
6775
7426
  })();
6776
- const stateKey = scale.encodeStateKey(scaleCoder, `Invalid address / token onChainId in ${networkId} storage query ${address} / ${token.onChainId}`, address, onChainId);
7427
+ const stateKey = scale.encodeStateKey(scaleCoder, `Invalid address / token onChainId in ${chainId} storage query ${address} / ${token.onChainId}`, address, onChainId);
6777
7428
  if (!stateKey) return [];
6778
7429
  const decodeResult = change => {
6779
7430
  /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
6780
7431
 
6781
- const decoded = scale.decodeScale(scaleCoder, change, `Failed to decode substrate-tokens balance on chain ${networkId}`) ?? {
7432
+ const decoded = scale.decodeScale(scaleCoder, change, `Failed to decode substrate-tokens balance on chain ${chainId}`) ?? {
6782
7433
  free: 0n,
6783
7434
  reserved: 0n,
6784
7435
  frozen: 0n
@@ -6803,32 +7454,50 @@ async function buildNetworkQueries(networkId, chainConnector, chaindataProvider,
6803
7454
  source: "substrate-tokens",
6804
7455
  status: "live",
6805
7456
  address,
6806
- networkId,
7457
+ multiChainId: {
7458
+ subChainId: chainId
7459
+ },
7460
+ chainId,
6807
7461
  tokenId: token.id,
6808
7462
  values: balanceValues
6809
7463
  };
6810
7464
  };
6811
7465
  return {
6812
- chainId: networkId,
7466
+ chainId,
6813
7467
  stateKey,
6814
7468
  decodeResult
6815
7469
  };
6816
7470
  });
6817
7471
  });
6818
7472
  }
6819
- async function buildQueries(chainConnector, chaindataProvider$1, addressesByToken, signal) {
6820
- const byNetwork = lodash.keys(addressesByToken).reduce((acc, tokenId) => {
6821
- const networkId = chaindataProvider.parseSubTokensTokenId(tokenId).networkId;
6822
- if (!acc[networkId]) acc[networkId] = {};
6823
- acc[networkId][tokenId] = addressesByToken[tokenId];
6824
- return acc;
6825
- }, {});
6826
- return (await Promise.all(lodash.toPairs(byNetwork).map(([networkId, addressesByToken]) => {
6827
- return buildNetworkQueries(networkId, chainConnector, chaindataProvider$1, addressesByToken, signal);
6828
- }))).flat();
6829
- }
6830
7473
 
6831
- const defaultBalanceModules = [EvmErc20Module, EvmNativeModule, EvmUniswapV2Module, SubAssetsModule, SubForeignAssetsModule, SubNativeModule, SubPsp22Module, SubTokensModule];
7474
+ const defaultBalanceModules = [EvmErc20Module, EvmNativeModule, EvmUniswapV2Module, SubAssetsModule, SubEquilibriumModule, SubForeignAssetsModule, SubNativeModule, SubPsp22Module, SubTokensModule];
7475
+
7476
+ /** Pulls the latest chaindata from https://github.com/TalismanSociety/chaindata */
7477
+ const hydrateChaindataAndMiniMetadata = async (chaindataProvider, miniMetadataUpdater) => {
7478
+ // need chains to be provisioned first, or substrate balances won't fetch on first subscription
7479
+ await chaindataProvider.hydrateChains();
7480
+ await Promise.all([miniMetadataUpdater.hydrateFromChaindata(), miniMetadataUpdater.hydrateCustomChains()]);
7481
+ const chains = await chaindataProvider.chains();
7482
+ const {
7483
+ statusesByChain
7484
+ } = await miniMetadataUpdater.statuses(chains);
7485
+ const goodChains = [...statusesByChain.entries()].flatMap(([chainId, status]) => status === "good" ? chainId : []);
7486
+ await chaindataProvider.hydrateSubstrateTokens(goodChains);
7487
+ };
7488
+
7489
+ /** Builds any missing miniMetadatas (e.g. for the user's custom substrate chains) */
7490
+ const updateCustomMiniMetadata = async (chaindataProvider, miniMetadataUpdater) => {
7491
+ const chainIds = await chaindataProvider.chainIds();
7492
+ await miniMetadataUpdater.update(chainIds);
7493
+ };
7494
+
7495
+ /** Fetches any missing Evm Tokens */
7496
+ const updateEvmTokens = async (chaindataProvider, evmTokenFetcher) => {
7497
+ await chaindataProvider.hydrateEvmNetworks();
7498
+ const evmNetworkIds = await chaindataProvider.evmNetworkIds();
7499
+ await evmTokenFetcher.update(evmNetworkIds);
7500
+ };
6832
7501
 
6833
7502
  exports.Balance = Balance;
6834
7503
  exports.BalanceFormatter = BalanceFormatter;
@@ -6837,12 +7506,11 @@ exports.Balances = Balances;
6837
7506
  exports.Change24hCurrencyFormatter = Change24hCurrencyFormatter;
6838
7507
  exports.DefaultBalanceModule = DefaultBalanceModule;
6839
7508
  exports.EvmErc20Module = EvmErc20Module;
6840
- exports.EvmErc20TokenConfigSchema = EvmErc20TokenConfigSchema;
6841
7509
  exports.EvmNativeModule = EvmNativeModule;
6842
- exports.EvmNativeTokenConfigSchema = EvmNativeTokenConfigSchema;
7510
+ exports.EvmTokenFetcher = EvmTokenFetcher;
6843
7511
  exports.EvmUniswapV2Module = EvmUniswapV2Module;
6844
- exports.EvmUniswapV2TokenConfigSchema = EvmUniswapV2TokenConfigSchema;
6845
7512
  exports.FiatSumBalancesFormatter = FiatSumBalancesFormatter;
7513
+ exports.MiniMetadataUpdater = MiniMetadataUpdater;
6846
7514
  exports.ONE_ALPHA_TOKEN = ONE_ALPHA_TOKEN;
6847
7515
  exports.PlanckSumBalancesFormatter = PlanckSumBalancesFormatter;
6848
7516
  exports.RpcStateQueryHelper = RpcStateQueryHelper;
@@ -6850,20 +7518,15 @@ exports.SCALE_FACTOR = SCALE_FACTOR;
6850
7518
  exports.SUBTENSOR_MIN_STAKE_AMOUNT_PLANK = SUBTENSOR_MIN_STAKE_AMOUNT_PLANK;
6851
7519
  exports.SUBTENSOR_ROOT_NETUID = SUBTENSOR_ROOT_NETUID;
6852
7520
  exports.SubAssetsModule = SubAssetsModule;
6853
- exports.SubAssetsTokenConfigSchema = SubAssetsTokenConfigSchema;
7521
+ exports.SubEquilibriumModule = SubEquilibriumModule;
6854
7522
  exports.SubForeignAssetsModule = SubForeignAssetsModule;
6855
- exports.SubForeignAssetsTokenConfigSchema = SubForeignAssetsTokenConfigSchema;
6856
7523
  exports.SubNativeModule = SubNativeModule;
6857
- exports.SubNativeTokenConfigSchema = SubNativeTokenConfigSchema;
6858
7524
  exports.SubPsp22Module = SubPsp22Module;
6859
- exports.SubPsp22TokenConfigSchema = SubPsp22TokenConfigSchema;
6860
7525
  exports.SubTokensModule = SubTokensModule;
6861
- exports.SubTokensTokenConfigSchema = SubTokensTokenConfigSchema;
6862
7526
  exports.SumBalancesFormatter = SumBalancesFormatter;
6863
7527
  exports.TalismanBalancesDatabase = TalismanBalancesDatabase;
6864
7528
  exports.abiMulticall = abiMulticall;
6865
7529
  exports.balances = balances;
6866
- exports.buildNetworkStorageCoders = buildNetworkStorageCoders;
6867
7530
  exports.buildStorageCoders = buildStorageCoders;
6868
7531
  exports.calculateAlphaPrice = calculateAlphaPrice;
6869
7532
  exports.calculateTaoAmountFromAlpha = calculateTaoAmountFromAlpha;
@@ -6878,14 +7541,27 @@ exports.deriveMiniMetadataId = deriveMiniMetadataId;
6878
7541
  exports.detectTransferMethod = detectTransferMethod;
6879
7542
  exports.erc20Abi = erc20Abi;
6880
7543
  exports.erc20BalancesAggregatorAbi = erc20BalancesAggregatorAbi;
7544
+ exports.evmErc20TokenId = evmErc20TokenId;
7545
+ exports.evmNativeTokenId = evmNativeTokenId;
7546
+ exports.evmUniswapV2TokenId = evmUniswapV2TokenId;
6881
7547
  exports.excludeFromFeePayableLocks = excludeFromFeePayableLocks;
6882
7548
  exports.excludeFromTransferableAmount = excludeFromTransferableAmount;
6883
7549
  exports.filterBaseLocks = filterBaseLocks;
6884
7550
  exports.filterMirrorTokens = filterMirrorTokens;
7551
+ exports.findChainMeta = findChainMeta;
6885
7552
  exports.getBalanceId = getBalanceId;
6886
7553
  exports.getLockTitle = getLockTitle;
6887
7554
  exports.getUniqueChainIds = getUniqueChainIds;
6888
7555
  exports.getValueId = getValueId;
7556
+ exports.hydrateChaindataAndMiniMetadata = hydrateChaindataAndMiniMetadata;
6889
7557
  exports.includeInTotalExtraAmount = includeInTotalExtraAmount;
6890
7558
  exports.makeContractCaller = makeContractCaller;
7559
+ exports.subAssetTokenId = subAssetTokenId;
7560
+ exports.subEquilibriumTokenId = subEquilibriumTokenId;
7561
+ exports.subForeignAssetTokenId = subForeignAssetTokenId;
7562
+ exports.subNativeTokenId = subNativeTokenId;
7563
+ exports.subPsp22TokenId = subPsp22TokenId;
7564
+ exports.subTokensTokenId = subTokensTokenId;
6891
7565
  exports.uniswapV2PairAbi = uniswapV2PairAbi;
7566
+ exports.updateCustomMiniMetadata = updateCustomMiniMetadata;
7567
+ exports.updateEvmTokens = updateEvmTokens;