@talismn/balances 0.0.0-pr2043-20250618043535 → 0.0.0-pr2043-20250618090234

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.
@@ -0,0 +1,3 @@
1
+ import { ChainConnector } from "@talismn/chain-connector";
2
+ import { DotNetworkId } from "@talismn/chaindata-provider";
3
+ export declare const getMetadataRpc: (chainConnector: ChainConnector, networkId: DotNetworkId) => Promise<`0x${string}`>;
@@ -0,0 +1,4 @@
1
+ import { ChainConnector } from "@talismn/chain-connector";
2
+ import { ChaindataProvider, DotNetworkId } from "@talismn/chaindata-provider";
3
+ import { MiniMetadata } from "../types";
4
+ export declare const getMiniMetadatas: (chainConnector: ChainConnector, chaindataProvider: ChaindataProvider, networkId: DotNetworkId, specVersion: number) => Promise<MiniMetadata[]>;
@@ -0,0 +1,6 @@
1
+ import { ChainConnector } from "@talismn/chain-connector";
2
+ import { DotNetworkId } from "@talismn/chaindata-provider";
3
+ /**
4
+ * fetches the spec version of a network. current request is cached in case of multiple calls (all balance subs kick in at once)
5
+ */
6
+ export declare const getSpecVersion: (chainConnector: ChainConnector, networkId: DotNetworkId) => Promise<number>;
@@ -0,0 +1,4 @@
1
+ import { ChainConnector } from "@talismn/chain-connector";
2
+ import { ChaindataProvider, DotNetworkId } from "@talismn/chaindata-provider";
3
+ import { MiniMetadata } from "../types";
4
+ export declare const getUpdatedMiniMetadatas: (chainConnector: ChainConnector, chaindataProvider: ChaindataProvider, networkId: DotNetworkId, specVersion: number) => Promise<MiniMetadata[]>;
@@ -0,0 +1,4 @@
1
+ import { ChainConnector } from "@talismn/chain-connector";
2
+ import { ChaindataProvider, NetworkId as DotNetworkId } from "@talismn/chaindata-provider";
3
+ import { MiniMetadata } from "../types";
4
+ export declare const getMiniMetadata: (chaindataProvider: ChaindataProvider, chainConnector: ChainConnector, chainId: DotNetworkId, source: string) => Promise<MiniMetadata>;
@@ -9,7 +9,7 @@ export type SubAssetsChainMeta = {
9
9
  };
10
10
  export type SubAssetsModuleConfig = {
11
11
  tokens?: Array<{
12
- assetId: string | number;
12
+ assetId: number | string;
13
13
  } & BalancesConfigTokenParams>;
14
14
  };
15
15
  export type SubAssetsBalance = NewBalanceType<ModuleType, "complex">;
@@ -1,4 +1,4 @@
1
- import { ChainId, DotNetwork, NetworkId } from "@talismn/chaindata-provider";
1
+ import { DotNetwork, DotNetworkId, NetworkId } from "@talismn/chaindata-provider";
2
2
  import { getDynamicBuilder } from "@talismn/scale";
3
3
  import { MiniMetadata } from "../../types";
4
4
  import { AnyNewBalanceModule, InferModuleType } from "./InferBalanceModuleTypes";
@@ -12,12 +12,12 @@ export declare const buildStorageCoders: <TBalanceModule extends AnyNewBalanceMo
12
12
  chainId: string;
13
13
  }) => [string, string]);
14
14
  }>({ chainIds, chains, miniMetadatas, moduleType, coders, }: {
15
- chainIds: ChainId[];
15
+ chainIds: DotNetworkId[];
16
16
  chains: Record<NetworkId, DotNetwork>;
17
17
  miniMetadatas: Map<string, MiniMetadata>;
18
18
  moduleType: InferModuleType<TBalanceModule>;
19
19
  coders: TCoders;
20
- }) => Map<ChainId, { [Property in keyof TCoders]: {
20
+ }) => Map<DotNetworkId, { [Property in keyof TCoders]: {
21
21
  args: [import("scale-ts").Encoder<any[]>, import("scale-ts").Decoder<any[]>] & {
22
22
  enc: import("scale-ts").Encoder<any[]>;
23
23
  dec: import("scale-ts").Decoder<any[]>;
@@ -14,7 +14,9 @@ var viem = require('viem');
14
14
  var isEqual = require('lodash/isEqual');
15
15
  var txwrapperCore = require('@substrate/txwrapper-core');
16
16
  var scale = require('@talismn/scale');
17
+ var lodash = require('lodash');
17
18
  var camelCase = require('lodash/camelCase');
19
+ var sapi = require('@talismn/sapi');
18
20
  var types = require('@polkadot/types');
19
21
  var groupBy = require('lodash/groupBy');
20
22
  var utils = require('@polkadot-api/utils');
@@ -22,7 +24,6 @@ var polkadotApi = require('polkadot-api');
22
24
  var PromisePool = require('@supercharge/promise-pool');
23
25
  var chainConnector = require('@talismn/chain-connector');
24
26
  var scaleTs = require('scale-ts');
25
- var sapi = require('@talismn/sapi');
26
27
  var upperFirst = require('lodash/upperFirst');
27
28
  var apiContract = require('@polkadot/api-contract');
28
29
 
@@ -121,7 +122,7 @@ class EvmTokenFetcher {
121
122
 
122
123
  var packageJson = {
123
124
  name: "@talismn/balances",
124
- version: "0.0.0-pr2043-20250618043535"};
125
+ version: "0.0.0-pr2043-20250618090234"};
125
126
 
126
127
  const libVersion = packageJson.version;
127
128
 
@@ -2952,6 +2953,139 @@ async function getPoolBalance(publicClient, contractAddress, accountAddress) {
2952
2953
  }
2953
2954
  }
2954
2955
 
2956
+ // cache the promise so it can be shared across multiple calls
2957
+ const CACHE_GET_SPEC_VERSION = new Map();
2958
+ const fetchSpecVersion = async (chainConnector, networkId) => {
2959
+ const {
2960
+ specVersion
2961
+ } = await chainConnector.send(networkId, "state_getRuntimeVersion", [true]);
2962
+ return specVersion;
2963
+ };
2964
+
2965
+ /**
2966
+ * fetches the spec version of a network. current request is cached in case of multiple calls (all balance subs kick in at once)
2967
+ */
2968
+ const getSpecVersion = async (chainConnector, networkId) => {
2969
+ if (CACHE_GET_SPEC_VERSION.has(networkId)) return CACHE_GET_SPEC_VERSION.get(networkId);
2970
+ const pResult = fetchSpecVersion(chainConnector, networkId);
2971
+ CACHE_GET_SPEC_VERSION.set(networkId, pResult);
2972
+ try {
2973
+ return await pResult;
2974
+ } catch (cause) {
2975
+ throw new Error(`Failed to fetch specVersion for network ${networkId}`, {
2976
+ cause
2977
+ });
2978
+ } finally {
2979
+ CACHE_GET_SPEC_VERSION.delete(networkId);
2980
+ }
2981
+ };
2982
+
2983
+ // share requests as all modules will call this at once
2984
+ const CACHE$1 = new Map();
2985
+ const getMetadataRpc = async (chainConnector, networkId) => {
2986
+ if (CACHE$1.has(networkId)) return CACHE$1.get(networkId);
2987
+ const pResult = sapi.fetchBestMetadata((...args) => chainConnector.send(networkId, ...args), true // allow fallback to 14 as modules dont use any v15 or v16 specifics yet
2988
+ );
2989
+ CACHE$1.set(networkId, pResult);
2990
+ try {
2991
+ return await pResult;
2992
+ } catch (cause) {
2993
+ throw new Error(`Failed to fetch metadataRpc for network ${networkId}`, {
2994
+ cause
2995
+ });
2996
+ } finally {
2997
+ CACHE$1.delete(networkId);
2998
+ }
2999
+ };
3000
+
3001
+ // share requests as all modules will call this at once
3002
+ const CACHE = new Map();
3003
+ const getMiniMetadatas = async (chainConnector, chaindataProvider, networkId, specVersion) => {
3004
+ if (CACHE.has(networkId)) return CACHE.get(networkId);
3005
+ const pResult = fetchMiniMetadatas(chainConnector, chaindataProvider, networkId, specVersion);
3006
+ CACHE.set(networkId, pResult);
3007
+ try {
3008
+ return await pResult;
3009
+ } catch (cause) {
3010
+ throw new Error(`Failed to fetch metadataRpc for network ${networkId}`, {
3011
+ cause
3012
+ });
3013
+ } finally {
3014
+ CACHE.delete(networkId);
3015
+ }
3016
+ };
3017
+ const fetchMiniMetadatas = async (chainConnector, chaindataProvider, chainId, specVersion) => {
3018
+ const metadataRpc = await getMetadataRpc(chainConnector, chainId);
3019
+ const chainConnectors = {
3020
+ substrate: chainConnector
3021
+ };
3022
+ const modules = defaultBalanceModules.map(mod => mod({
3023
+ chainConnectors,
3024
+ chaindataProvider
3025
+ })).filter(mod => mod.type.startsWith("substrate-"));
3026
+ return Promise.all(modules.map(async mod => {
3027
+ const source = mod.type;
3028
+ const chainMeta = await mod.fetchSubstrateChainMeta(chainId, {}, metadataRpc, {});
3029
+ return {
3030
+ id: deriveMiniMetadataId({
3031
+ source,
3032
+ chainId,
3033
+ specVersion,
3034
+ libVersion
3035
+ }),
3036
+ source,
3037
+ chainId,
3038
+ specVersion,
3039
+ libVersion,
3040
+ data: chainMeta?.miniMetadata ?? null
3041
+ };
3042
+ }));
3043
+ };
3044
+
3045
+ const getUpdatedMiniMetadatas = async (chainConnector, chaindataProvider, networkId, specVersion) => {
3046
+ const miniMetadatas = await getMiniMetadatas(chainConnector, chaindataProvider, networkId, specVersion);
3047
+ await db.transaction("readwrite", "miniMetadatas", async tx => {
3048
+ await tx.miniMetadatas.where({
3049
+ networkId
3050
+ }).delete();
3051
+ await tx.miniMetadatas.bulkPut(miniMetadatas);
3052
+ });
3053
+ return miniMetadatas;
3054
+ };
3055
+
3056
+ const getMiniMetadata = async (chaindataProvider, chainConnector, chainId, source) => {
3057
+ const specVersion = await getSpecVersion(chainConnector, chainId);
3058
+
3059
+ // TODO when working a chaindata branch, need a way to pass the libVersion used to derive the miniMetadataId got github
3060
+ const miniMetadataId = deriveMiniMetadataId({
3061
+ source,
3062
+ chainId,
3063
+ specVersion,
3064
+ libVersion
3065
+ });
3066
+
3067
+ // lookup local ones
3068
+ const [dbMiniMetadata, ghMiniMetadata] = await Promise.all([db.miniMetadatas.get(miniMetadataId), chaindataProvider.miniMetadataById(miniMetadataId)]);
3069
+ const miniMetadata = dbMiniMetadata ?? ghMiniMetadata;
3070
+ if (miniMetadata) return miniMetadata;
3071
+
3072
+ // update from live chain metadata and persist locally
3073
+ const miniMetadatas = await getUpdatedMiniMetadatas(chainConnector, chaindataProvider, chainId, specVersion);
3074
+ const found = miniMetadatas.find(m => m.id === miniMetadataId);
3075
+ if (!found) {
3076
+ log.warn("MiniMetadata not found in updated miniMetadatas", {
3077
+ source,
3078
+ chainId,
3079
+ specVersion,
3080
+ libVersion,
3081
+ miniMetadataId,
3082
+ miniMetadatas
3083
+ });
3084
+ throw new Error(`MiniMetadata not found for ${source} on ${chainId}`);
3085
+ }
3086
+ return found;
3087
+ };
3088
+
2955
3089
  /**
2956
3090
  * Wraps a BalanceModule's fetch/subscribe methods with a single `balances` method.
2957
3091
  * This `balances` method will subscribe if a callback parameter is provided, or otherwise fetch.
@@ -3257,7 +3391,7 @@ const SubAssetsModule = hydrate => {
3257
3391
  const tokens = {};
3258
3392
  for (const tokenConfig of moduleConfig?.tokens ?? []) {
3259
3393
  try {
3260
- const assetId = typeof tokenConfig.assetId === "number" ? tokenConfig.assetId.toString() : tokenConfig.assetId;
3394
+ const assetId = String(tokenConfig.assetId);
3261
3395
  const assetStateKey = tryEncode(assetCoder, BigInt(assetId)) ?? tryEncode(assetCoder, assetId);
3262
3396
  const metadataStateKey = tryEncode(metadataCoder, BigInt(assetId)) ?? tryEncode(metadataCoder, assetId);
3263
3397
  if (assetStateKey === null || metadataStateKey === null) throw new Error(`Failed to encode stateKey for asset ${assetId} on chain ${chainId}`);
@@ -3299,13 +3433,47 @@ const SubAssetsModule = hydrate => {
3299
3433
  async subscribeBalances({
3300
3434
  addressesByToken
3301
3435
  }, callback) {
3302
- const queries = await buildQueries$3(chaindataProvider$1, addressesByToken);
3303
- const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe((error, result) => {
3304
- if (error) return callback(error);
3305
- const balances = result?.filter(b => b !== null) ?? [];
3306
- if (balances.length > 0) callback(null, new Balances(balances));
3307
- });
3308
- return unsubscribe;
3436
+ const byNetwork = lodash.keys(addressesByToken).reduce((acc, tokenId) => {
3437
+ const networkId = chaindataProvider.parseSubAssetTokenId(tokenId).networkId;
3438
+ if (!acc[networkId]) acc[networkId] = {};
3439
+ acc[networkId][tokenId] = addressesByToken[tokenId];
3440
+ return acc;
3441
+ }, {});
3442
+ const controller = new AbortController();
3443
+ await Promise.all(lodash.toPairs(byNetwork).map(async ([networkId, addressesByToken]) => {
3444
+ const queries = await buildNetworkQueries(networkId, chainConnector, chaindataProvider$1, addressesByToken);
3445
+ if (controller.signal.aborted) return;
3446
+ const stateHelper = new RpcStateQueryHelper(chainConnector, queries);
3447
+ const unsubscribe = await stateHelper.subscribe((error, result) => {
3448
+ // console.log("SubstrateAssetsModule.callback", { error, result })
3449
+ if (error) return callback(error);
3450
+ const balances = result?.filter(b => b !== null) ?? [];
3451
+ if (balances.length > 0) callback(null, new Balances(balances));
3452
+ });
3453
+ controller.signal.addEventListener("abort", () => {
3454
+ log.debug("TMP subscribeBalances aborted, unsubscribing from network", networkId);
3455
+ unsubscribe();
3456
+ });
3457
+ }));
3458
+
3459
+ // const networkIds = uniq(uniq(keys(addressesByToken)).map((tokenId) => parseSubAssetTokenId(tokenId).networkId))
3460
+ // const
3461
+
3462
+ //console.log("SubstrateAssetsModule.subscribeBalances 1", { addressesByToken })
3463
+ // const queries = await buildQueries(chaindataProvider, addressesByToken)
3464
+ // //console.log("SubstrateAssetsModule.subscribeBalances 2", { queries, addressesByToken })
3465
+ // const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe(
3466
+ // (error, result) => {
3467
+ // // console.log("SubstrateAssetsModule.callback", { error, result })
3468
+ // if (error) return callback(error)
3469
+ // const balances = result?.filter((b): b is SubAssetsBalance => b !== null) ?? []
3470
+ // if (balances.length > 0) callback(null, new Balances(balances))
3471
+ // },
3472
+ // )
3473
+
3474
+ return () => {
3475
+ controller.abort();
3476
+ };
3309
3477
  },
3310
3478
  async fetchBalances(addressesByToken) {
3311
3479
  util$1.assert(chainConnectors.substrate, "This module requires a substrate chain connector");
@@ -3379,9 +3547,109 @@ const SubAssetsModule = hydrate => {
3379
3547
  }
3380
3548
  };
3381
3549
  };
3550
+ async function buildNetworkQueries(networkId, chainConnector, chaindataProvider, addressesByToken) {
3551
+ const miniMetadata = await getMiniMetadata(chaindataProvider, chainConnector, networkId, moduleType$4);
3552
+ const network = await chaindataProvider.chainById(networkId);
3553
+ const tokensById = await chaindataProvider.tokensById();
3554
+ const chainIds = [networkId];
3555
+ const chains = network ? {
3556
+ [networkId]: network
3557
+ } : {};
3558
+ const miniMetadatas = new Map([[miniMetadata.id, miniMetadata]]);
3559
+ const chainStorageCoders = buildStorageCoders({
3560
+ chainIds,
3561
+ chains,
3562
+ miniMetadatas,
3563
+ moduleType: moduleType$4,
3564
+ coders: {
3565
+ storage: ["Assets", "Account"]
3566
+ }
3567
+ });
3568
+ return Object.entries(addressesByToken).flatMap(([tokenId, addresses]) => {
3569
+ const token = tokensById[tokenId];
3570
+ if (!token) {
3571
+ log.warn(`Token ${tokenId} not found`);
3572
+ return [];
3573
+ }
3574
+ if (token.type !== "substrate-assets") {
3575
+ log.debug(`This module doesn't handle tokens of type ${token.type}`);
3576
+ return [];
3577
+ }
3578
+ const networkId = token.networkId;
3579
+ if (!networkId) {
3580
+ log.warn(`Token ${tokenId} has no chain`);
3581
+ return [];
3582
+ }
3583
+ const chain = chains[networkId];
3584
+ if (!chain) {
3585
+ log.warn(`Chain ${networkId} for token ${tokenId} not found`);
3586
+ return [];
3587
+ }
3588
+ return addresses.flatMap(address => {
3589
+ const scaleCoder = chainStorageCoders.get(networkId)?.storage;
3590
+ const stateKey = tryEncode(scaleCoder, BigInt(token.assetId), address) ?? tryEncode(scaleCoder, token.assetId, address);
3591
+ if (!stateKey) {
3592
+ log.warn(`Invalid assetId / address in ${networkId} storage query ${token.assetId} / ${address}`);
3593
+ return [];
3594
+ }
3595
+ const decodeResult = change => {
3596
+ /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
3597
+
3598
+ const decoded = scale.decodeScale(scaleCoder, change, `Failed to decode substrate-assets balance on chain ${networkId}`) ?? {
3599
+ balance: 0n,
3600
+ status: {
3601
+ type: "Liquid"
3602
+ }};
3603
+ const isFrozen = decoded?.status?.type === "Frozen";
3604
+ const amount = (decoded?.balance ?? 0n).toString();
3605
+
3606
+ // due to the following balance calculations, which are made in the `Balance` type:
3607
+ //
3608
+ // total balance = (free balance) + (reserved balance)
3609
+ // transferable balance = (free balance) - (frozen balance)
3610
+ //
3611
+ // when `isFrozen` is true we need to set **both** the `free` and `frozen` amounts
3612
+ // of this balance to the value we received from the RPC.
3613
+ //
3614
+ // if we only set the `frozen` amount, then the `total` calculation will be incorrect!
3615
+ const free = amount;
3616
+ const frozen = token.isFrozen || isFrozen ? amount : "0";
3617
+
3618
+ // include balance values even if zero, so that newly-zero values overwrite old values
3619
+ const balanceValues = [{
3620
+ type: "free",
3621
+ label: "free",
3622
+ amount: free.toString()
3623
+ }, {
3624
+ type: "locked",
3625
+ label: "frozen",
3626
+ amount: frozen.toString()
3627
+ }];
3628
+ return {
3629
+ source: "substrate-assets",
3630
+ status: "live",
3631
+ address,
3632
+ networkId,
3633
+ tokenId: token.id,
3634
+ values: balanceValues
3635
+ };
3636
+ };
3637
+ return {
3638
+ chainId: networkId,
3639
+ stateKey,
3640
+ decodeResult
3641
+ };
3642
+ });
3643
+ });
3644
+ }
3382
3645
  async function buildQueries$3(chaindataProvider, addressesByToken) {
3383
3646
  const allChains = await chaindataProvider.chainsById();
3384
3647
  const tokens = await chaindataProvider.tokensById();
3648
+
3649
+ // const networkIds = Object.keys(addressesByToken)
3650
+
3651
+ // const
3652
+ // const miniMetadatas = await getMiniMetadatas(chainConnector, chaindataProvider, network)
3385
3653
  const miniMetadatas = new Map((await db.miniMetadatas.toArray()).map(miniMetadata => [miniMetadata.id, miniMetadata]));
3386
3654
  const uniqueChainIds = getUniqueChainIds(addressesByToken, tokens);
3387
3655
  const chains = Object.fromEntries(uniqueChainIds.map(chainId => [chainId, allChains[chainId]]));
@@ -14,7 +14,9 @@ var viem = require('viem');
14
14
  var isEqual = require('lodash/isEqual');
15
15
  var txwrapperCore = require('@substrate/txwrapper-core');
16
16
  var scale = require('@talismn/scale');
17
+ var lodash = require('lodash');
17
18
  var camelCase = require('lodash/camelCase');
19
+ var sapi = require('@talismn/sapi');
18
20
  var types = require('@polkadot/types');
19
21
  var groupBy = require('lodash/groupBy');
20
22
  var utils = require('@polkadot-api/utils');
@@ -22,7 +24,6 @@ var polkadotApi = require('polkadot-api');
22
24
  var PromisePool = require('@supercharge/promise-pool');
23
25
  var chainConnector = require('@talismn/chain-connector');
24
26
  var scaleTs = require('scale-ts');
25
- var sapi = require('@talismn/sapi');
26
27
  var upperFirst = require('lodash/upperFirst');
27
28
  var apiContract = require('@polkadot/api-contract');
28
29
 
@@ -121,7 +122,7 @@ class EvmTokenFetcher {
121
122
 
122
123
  var packageJson = {
123
124
  name: "@talismn/balances",
124
- version: "0.0.0-pr2043-20250618043535"};
125
+ version: "0.0.0-pr2043-20250618090234"};
125
126
 
126
127
  const libVersion = packageJson.version;
127
128
 
@@ -2952,6 +2953,139 @@ async function getPoolBalance(publicClient, contractAddress, accountAddress) {
2952
2953
  }
2953
2954
  }
2954
2955
 
2956
+ // cache the promise so it can be shared across multiple calls
2957
+ const CACHE_GET_SPEC_VERSION = new Map();
2958
+ const fetchSpecVersion = async (chainConnector, networkId) => {
2959
+ const {
2960
+ specVersion
2961
+ } = await chainConnector.send(networkId, "state_getRuntimeVersion", [true]);
2962
+ return specVersion;
2963
+ };
2964
+
2965
+ /**
2966
+ * fetches the spec version of a network. current request is cached in case of multiple calls (all balance subs kick in at once)
2967
+ */
2968
+ const getSpecVersion = async (chainConnector, networkId) => {
2969
+ if (CACHE_GET_SPEC_VERSION.has(networkId)) return CACHE_GET_SPEC_VERSION.get(networkId);
2970
+ const pResult = fetchSpecVersion(chainConnector, networkId);
2971
+ CACHE_GET_SPEC_VERSION.set(networkId, pResult);
2972
+ try {
2973
+ return await pResult;
2974
+ } catch (cause) {
2975
+ throw new Error(`Failed to fetch specVersion for network ${networkId}`, {
2976
+ cause
2977
+ });
2978
+ } finally {
2979
+ CACHE_GET_SPEC_VERSION.delete(networkId);
2980
+ }
2981
+ };
2982
+
2983
+ // share requests as all modules will call this at once
2984
+ const CACHE$1 = new Map();
2985
+ const getMetadataRpc = async (chainConnector, networkId) => {
2986
+ if (CACHE$1.has(networkId)) return CACHE$1.get(networkId);
2987
+ const pResult = sapi.fetchBestMetadata((...args) => chainConnector.send(networkId, ...args), true // allow fallback to 14 as modules dont use any v15 or v16 specifics yet
2988
+ );
2989
+ CACHE$1.set(networkId, pResult);
2990
+ try {
2991
+ return await pResult;
2992
+ } catch (cause) {
2993
+ throw new Error(`Failed to fetch metadataRpc for network ${networkId}`, {
2994
+ cause
2995
+ });
2996
+ } finally {
2997
+ CACHE$1.delete(networkId);
2998
+ }
2999
+ };
3000
+
3001
+ // share requests as all modules will call this at once
3002
+ const CACHE = new Map();
3003
+ const getMiniMetadatas = async (chainConnector, chaindataProvider, networkId, specVersion) => {
3004
+ if (CACHE.has(networkId)) return CACHE.get(networkId);
3005
+ const pResult = fetchMiniMetadatas(chainConnector, chaindataProvider, networkId, specVersion);
3006
+ CACHE.set(networkId, pResult);
3007
+ try {
3008
+ return await pResult;
3009
+ } catch (cause) {
3010
+ throw new Error(`Failed to fetch metadataRpc for network ${networkId}`, {
3011
+ cause
3012
+ });
3013
+ } finally {
3014
+ CACHE.delete(networkId);
3015
+ }
3016
+ };
3017
+ const fetchMiniMetadatas = async (chainConnector, chaindataProvider, chainId, specVersion) => {
3018
+ const metadataRpc = await getMetadataRpc(chainConnector, chainId);
3019
+ const chainConnectors = {
3020
+ substrate: chainConnector
3021
+ };
3022
+ const modules = defaultBalanceModules.map(mod => mod({
3023
+ chainConnectors,
3024
+ chaindataProvider
3025
+ })).filter(mod => mod.type.startsWith("substrate-"));
3026
+ return Promise.all(modules.map(async mod => {
3027
+ const source = mod.type;
3028
+ const chainMeta = await mod.fetchSubstrateChainMeta(chainId, {}, metadataRpc, {});
3029
+ return {
3030
+ id: deriveMiniMetadataId({
3031
+ source,
3032
+ chainId,
3033
+ specVersion,
3034
+ libVersion
3035
+ }),
3036
+ source,
3037
+ chainId,
3038
+ specVersion,
3039
+ libVersion,
3040
+ data: chainMeta?.miniMetadata ?? null
3041
+ };
3042
+ }));
3043
+ };
3044
+
3045
+ const getUpdatedMiniMetadatas = async (chainConnector, chaindataProvider, networkId, specVersion) => {
3046
+ const miniMetadatas = await getMiniMetadatas(chainConnector, chaindataProvider, networkId, specVersion);
3047
+ await db.transaction("readwrite", "miniMetadatas", async tx => {
3048
+ await tx.miniMetadatas.where({
3049
+ networkId
3050
+ }).delete();
3051
+ await tx.miniMetadatas.bulkPut(miniMetadatas);
3052
+ });
3053
+ return miniMetadatas;
3054
+ };
3055
+
3056
+ const getMiniMetadata = async (chaindataProvider, chainConnector, chainId, source) => {
3057
+ const specVersion = await getSpecVersion(chainConnector, chainId);
3058
+
3059
+ // TODO when working a chaindata branch, need a way to pass the libVersion used to derive the miniMetadataId got github
3060
+ const miniMetadataId = deriveMiniMetadataId({
3061
+ source,
3062
+ chainId,
3063
+ specVersion,
3064
+ libVersion
3065
+ });
3066
+
3067
+ // lookup local ones
3068
+ const [dbMiniMetadata, ghMiniMetadata] = await Promise.all([db.miniMetadatas.get(miniMetadataId), chaindataProvider.miniMetadataById(miniMetadataId)]);
3069
+ const miniMetadata = dbMiniMetadata ?? ghMiniMetadata;
3070
+ if (miniMetadata) return miniMetadata;
3071
+
3072
+ // update from live chain metadata and persist locally
3073
+ const miniMetadatas = await getUpdatedMiniMetadatas(chainConnector, chaindataProvider, chainId, specVersion);
3074
+ const found = miniMetadatas.find(m => m.id === miniMetadataId);
3075
+ if (!found) {
3076
+ log.warn("MiniMetadata not found in updated miniMetadatas", {
3077
+ source,
3078
+ chainId,
3079
+ specVersion,
3080
+ libVersion,
3081
+ miniMetadataId,
3082
+ miniMetadatas
3083
+ });
3084
+ throw new Error(`MiniMetadata not found for ${source} on ${chainId}`);
3085
+ }
3086
+ return found;
3087
+ };
3088
+
2955
3089
  /**
2956
3090
  * Wraps a BalanceModule's fetch/subscribe methods with a single `balances` method.
2957
3091
  * This `balances` method will subscribe if a callback parameter is provided, or otherwise fetch.
@@ -3257,7 +3391,7 @@ const SubAssetsModule = hydrate => {
3257
3391
  const tokens = {};
3258
3392
  for (const tokenConfig of moduleConfig?.tokens ?? []) {
3259
3393
  try {
3260
- const assetId = typeof tokenConfig.assetId === "number" ? tokenConfig.assetId.toString() : tokenConfig.assetId;
3394
+ const assetId = String(tokenConfig.assetId);
3261
3395
  const assetStateKey = tryEncode(assetCoder, BigInt(assetId)) ?? tryEncode(assetCoder, assetId);
3262
3396
  const metadataStateKey = tryEncode(metadataCoder, BigInt(assetId)) ?? tryEncode(metadataCoder, assetId);
3263
3397
  if (assetStateKey === null || metadataStateKey === null) throw new Error(`Failed to encode stateKey for asset ${assetId} on chain ${chainId}`);
@@ -3299,13 +3433,47 @@ const SubAssetsModule = hydrate => {
3299
3433
  async subscribeBalances({
3300
3434
  addressesByToken
3301
3435
  }, callback) {
3302
- const queries = await buildQueries$3(chaindataProvider$1, addressesByToken);
3303
- const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe((error, result) => {
3304
- if (error) return callback(error);
3305
- const balances = result?.filter(b => b !== null) ?? [];
3306
- if (balances.length > 0) callback(null, new Balances(balances));
3307
- });
3308
- return unsubscribe;
3436
+ const byNetwork = lodash.keys(addressesByToken).reduce((acc, tokenId) => {
3437
+ const networkId = chaindataProvider.parseSubAssetTokenId(tokenId).networkId;
3438
+ if (!acc[networkId]) acc[networkId] = {};
3439
+ acc[networkId][tokenId] = addressesByToken[tokenId];
3440
+ return acc;
3441
+ }, {});
3442
+ const controller = new AbortController();
3443
+ await Promise.all(lodash.toPairs(byNetwork).map(async ([networkId, addressesByToken]) => {
3444
+ const queries = await buildNetworkQueries(networkId, chainConnector, chaindataProvider$1, addressesByToken);
3445
+ if (controller.signal.aborted) return;
3446
+ const stateHelper = new RpcStateQueryHelper(chainConnector, queries);
3447
+ const unsubscribe = await stateHelper.subscribe((error, result) => {
3448
+ // console.log("SubstrateAssetsModule.callback", { error, result })
3449
+ if (error) return callback(error);
3450
+ const balances = result?.filter(b => b !== null) ?? [];
3451
+ if (balances.length > 0) callback(null, new Balances(balances));
3452
+ });
3453
+ controller.signal.addEventListener("abort", () => {
3454
+ log.debug("TMP subscribeBalances aborted, unsubscribing from network", networkId);
3455
+ unsubscribe();
3456
+ });
3457
+ }));
3458
+
3459
+ // const networkIds = uniq(uniq(keys(addressesByToken)).map((tokenId) => parseSubAssetTokenId(tokenId).networkId))
3460
+ // const
3461
+
3462
+ //console.log("SubstrateAssetsModule.subscribeBalances 1", { addressesByToken })
3463
+ // const queries = await buildQueries(chaindataProvider, addressesByToken)
3464
+ // //console.log("SubstrateAssetsModule.subscribeBalances 2", { queries, addressesByToken })
3465
+ // const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe(
3466
+ // (error, result) => {
3467
+ // // console.log("SubstrateAssetsModule.callback", { error, result })
3468
+ // if (error) return callback(error)
3469
+ // const balances = result?.filter((b): b is SubAssetsBalance => b !== null) ?? []
3470
+ // if (balances.length > 0) callback(null, new Balances(balances))
3471
+ // },
3472
+ // )
3473
+
3474
+ return () => {
3475
+ controller.abort();
3476
+ };
3309
3477
  },
3310
3478
  async fetchBalances(addressesByToken) {
3311
3479
  util$1.assert(chainConnectors.substrate, "This module requires a substrate chain connector");
@@ -3379,9 +3547,109 @@ const SubAssetsModule = hydrate => {
3379
3547
  }
3380
3548
  };
3381
3549
  };
3550
+ async function buildNetworkQueries(networkId, chainConnector, chaindataProvider, addressesByToken) {
3551
+ const miniMetadata = await getMiniMetadata(chaindataProvider, chainConnector, networkId, moduleType$4);
3552
+ const network = await chaindataProvider.chainById(networkId);
3553
+ const tokensById = await chaindataProvider.tokensById();
3554
+ const chainIds = [networkId];
3555
+ const chains = network ? {
3556
+ [networkId]: network
3557
+ } : {};
3558
+ const miniMetadatas = new Map([[miniMetadata.id, miniMetadata]]);
3559
+ const chainStorageCoders = buildStorageCoders({
3560
+ chainIds,
3561
+ chains,
3562
+ miniMetadatas,
3563
+ moduleType: moduleType$4,
3564
+ coders: {
3565
+ storage: ["Assets", "Account"]
3566
+ }
3567
+ });
3568
+ return Object.entries(addressesByToken).flatMap(([tokenId, addresses]) => {
3569
+ const token = tokensById[tokenId];
3570
+ if (!token) {
3571
+ log.warn(`Token ${tokenId} not found`);
3572
+ return [];
3573
+ }
3574
+ if (token.type !== "substrate-assets") {
3575
+ log.debug(`This module doesn't handle tokens of type ${token.type}`);
3576
+ return [];
3577
+ }
3578
+ const networkId = token.networkId;
3579
+ if (!networkId) {
3580
+ log.warn(`Token ${tokenId} has no chain`);
3581
+ return [];
3582
+ }
3583
+ const chain = chains[networkId];
3584
+ if (!chain) {
3585
+ log.warn(`Chain ${networkId} for token ${tokenId} not found`);
3586
+ return [];
3587
+ }
3588
+ return addresses.flatMap(address => {
3589
+ const scaleCoder = chainStorageCoders.get(networkId)?.storage;
3590
+ const stateKey = tryEncode(scaleCoder, BigInt(token.assetId), address) ?? tryEncode(scaleCoder, token.assetId, address);
3591
+ if (!stateKey) {
3592
+ log.warn(`Invalid assetId / address in ${networkId} storage query ${token.assetId} / ${address}`);
3593
+ return [];
3594
+ }
3595
+ const decodeResult = change => {
3596
+ /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
3597
+
3598
+ const decoded = scale.decodeScale(scaleCoder, change, `Failed to decode substrate-assets balance on chain ${networkId}`) ?? {
3599
+ balance: 0n,
3600
+ status: {
3601
+ type: "Liquid"
3602
+ }};
3603
+ const isFrozen = decoded?.status?.type === "Frozen";
3604
+ const amount = (decoded?.balance ?? 0n).toString();
3605
+
3606
+ // due to the following balance calculations, which are made in the `Balance` type:
3607
+ //
3608
+ // total balance = (free balance) + (reserved balance)
3609
+ // transferable balance = (free balance) - (frozen balance)
3610
+ //
3611
+ // when `isFrozen` is true we need to set **both** the `free` and `frozen` amounts
3612
+ // of this balance to the value we received from the RPC.
3613
+ //
3614
+ // if we only set the `frozen` amount, then the `total` calculation will be incorrect!
3615
+ const free = amount;
3616
+ const frozen = token.isFrozen || isFrozen ? amount : "0";
3617
+
3618
+ // include balance values even if zero, so that newly-zero values overwrite old values
3619
+ const balanceValues = [{
3620
+ type: "free",
3621
+ label: "free",
3622
+ amount: free.toString()
3623
+ }, {
3624
+ type: "locked",
3625
+ label: "frozen",
3626
+ amount: frozen.toString()
3627
+ }];
3628
+ return {
3629
+ source: "substrate-assets",
3630
+ status: "live",
3631
+ address,
3632
+ networkId,
3633
+ tokenId: token.id,
3634
+ values: balanceValues
3635
+ };
3636
+ };
3637
+ return {
3638
+ chainId: networkId,
3639
+ stateKey,
3640
+ decodeResult
3641
+ };
3642
+ });
3643
+ });
3644
+ }
3382
3645
  async function buildQueries$3(chaindataProvider, addressesByToken) {
3383
3646
  const allChains = await chaindataProvider.chainsById();
3384
3647
  const tokens = await chaindataProvider.tokensById();
3648
+
3649
+ // const networkIds = Object.keys(addressesByToken)
3650
+
3651
+ // const
3652
+ // const miniMetadatas = await getMiniMetadatas(chainConnector, chaindataProvider, network)
3385
3653
  const miniMetadatas = new Map((await db.miniMetadatas.toArray()).map(miniMetadata => [miniMetadata.id, miniMetadata]));
3386
3654
  const uniqueChainIds = getUniqueChainIds(addressesByToken, tokens);
3387
3655
  const chains = Object.fromEntries(uniqueChainIds.map(chainId => [chainId, allChains[chainId]]));
@@ -1,4 +1,4 @@
1
- import { fetchInitMiniMetadatas, evmErc20TokenId as evmErc20TokenId$1, EvmErc20TokenSchema, evmNativeTokenId, evmUniswapV2TokenId, githubTokenLogoUrl, subAssetTokenId, subForeignAssetTokenId, subNativeTokenId, subPsp22TokenId, subTokensTokenId } from '@talismn/chaindata-provider';
1
+ import { fetchInitMiniMetadatas, evmErc20TokenId as evmErc20TokenId$1, EvmErc20TokenSchema, evmNativeTokenId, evmUniswapV2TokenId, githubTokenLogoUrl, parseSubAssetTokenId, subAssetTokenId, subForeignAssetTokenId, subNativeTokenId, subPsp22TokenId, subTokensTokenId } from '@talismn/chaindata-provider';
2
2
  import { Dexie, liveQuery } from 'dexie';
3
3
  import { from, Observable, scan, share, map, switchAll, combineLatest, mergeMap, toArray, interval, startWith, exhaustMap, pipe, filter, shareReplay, combineLatestWith, distinctUntilChanged, firstValueFrom, BehaviorSubject, debounceTime, takeUntil, switchMap, withLatestFrom, concatMap } from 'rxjs';
4
4
  import anylogger from 'anylogger';
@@ -12,7 +12,9 @@ import { parseAbi, isHex, hexToBigInt } from 'viem';
12
12
  import isEqual from 'lodash/isEqual';
13
13
  import { defineMethod } from '@substrate/txwrapper-core';
14
14
  import { unifyMetadata, decAnyMetadata, getDynamicBuilder, getLookupFn, getMetadataVersion, compactMetadata, encodeMetadata, decodeScale, papiParse, encodeStateKey } from '@talismn/scale';
15
+ import { keys, toPairs } from 'lodash';
15
16
  import camelCase from 'lodash/camelCase';
17
+ import { fetchBestMetadata, getScaleApi } from '@talismn/sapi';
16
18
  import { Metadata, TypeRegistry } from '@polkadot/types';
17
19
  import groupBy from 'lodash/groupBy';
18
20
  import { mergeUint8, toHex } from '@polkadot-api/utils';
@@ -20,7 +22,6 @@ import { Binary, AccountId } from 'polkadot-api';
20
22
  import PromisePool from '@supercharge/promise-pool';
21
23
  import { ChainConnectionError } from '@talismn/chain-connector';
22
24
  import { u32, u128, Struct } from 'scale-ts';
23
- import { getScaleApi } from '@talismn/sapi';
24
25
  import upperFirst from 'lodash/upperFirst';
25
26
  import { Abi } from '@polkadot/api-contract';
26
27
 
@@ -108,7 +109,7 @@ class EvmTokenFetcher {
108
109
 
109
110
  var packageJson = {
110
111
  name: "@talismn/balances",
111
- version: "0.0.0-pr2043-20250618043535"};
112
+ version: "0.0.0-pr2043-20250618090234"};
112
113
 
113
114
  const libVersion = packageJson.version;
114
115
 
@@ -2939,6 +2940,139 @@ async function getPoolBalance(publicClient, contractAddress, accountAddress) {
2939
2940
  }
2940
2941
  }
2941
2942
 
2943
+ // cache the promise so it can be shared across multiple calls
2944
+ const CACHE_GET_SPEC_VERSION = new Map();
2945
+ const fetchSpecVersion = async (chainConnector, networkId) => {
2946
+ const {
2947
+ specVersion
2948
+ } = await chainConnector.send(networkId, "state_getRuntimeVersion", [true]);
2949
+ return specVersion;
2950
+ };
2951
+
2952
+ /**
2953
+ * fetches the spec version of a network. current request is cached in case of multiple calls (all balance subs kick in at once)
2954
+ */
2955
+ const getSpecVersion = async (chainConnector, networkId) => {
2956
+ if (CACHE_GET_SPEC_VERSION.has(networkId)) return CACHE_GET_SPEC_VERSION.get(networkId);
2957
+ const pResult = fetchSpecVersion(chainConnector, networkId);
2958
+ CACHE_GET_SPEC_VERSION.set(networkId, pResult);
2959
+ try {
2960
+ return await pResult;
2961
+ } catch (cause) {
2962
+ throw new Error(`Failed to fetch specVersion for network ${networkId}`, {
2963
+ cause
2964
+ });
2965
+ } finally {
2966
+ CACHE_GET_SPEC_VERSION.delete(networkId);
2967
+ }
2968
+ };
2969
+
2970
+ // share requests as all modules will call this at once
2971
+ const CACHE$1 = new Map();
2972
+ const getMetadataRpc = async (chainConnector, networkId) => {
2973
+ if (CACHE$1.has(networkId)) return CACHE$1.get(networkId);
2974
+ const pResult = fetchBestMetadata((...args) => chainConnector.send(networkId, ...args), true // allow fallback to 14 as modules dont use any v15 or v16 specifics yet
2975
+ );
2976
+ CACHE$1.set(networkId, pResult);
2977
+ try {
2978
+ return await pResult;
2979
+ } catch (cause) {
2980
+ throw new Error(`Failed to fetch metadataRpc for network ${networkId}`, {
2981
+ cause
2982
+ });
2983
+ } finally {
2984
+ CACHE$1.delete(networkId);
2985
+ }
2986
+ };
2987
+
2988
+ // share requests as all modules will call this at once
2989
+ const CACHE = new Map();
2990
+ const getMiniMetadatas = async (chainConnector, chaindataProvider, networkId, specVersion) => {
2991
+ if (CACHE.has(networkId)) return CACHE.get(networkId);
2992
+ const pResult = fetchMiniMetadatas(chainConnector, chaindataProvider, networkId, specVersion);
2993
+ CACHE.set(networkId, pResult);
2994
+ try {
2995
+ return await pResult;
2996
+ } catch (cause) {
2997
+ throw new Error(`Failed to fetch metadataRpc for network ${networkId}`, {
2998
+ cause
2999
+ });
3000
+ } finally {
3001
+ CACHE.delete(networkId);
3002
+ }
3003
+ };
3004
+ const fetchMiniMetadatas = async (chainConnector, chaindataProvider, chainId, specVersion) => {
3005
+ const metadataRpc = await getMetadataRpc(chainConnector, chainId);
3006
+ const chainConnectors = {
3007
+ substrate: chainConnector
3008
+ };
3009
+ const modules = defaultBalanceModules.map(mod => mod({
3010
+ chainConnectors,
3011
+ chaindataProvider
3012
+ })).filter(mod => mod.type.startsWith("substrate-"));
3013
+ return Promise.all(modules.map(async mod => {
3014
+ const source = mod.type;
3015
+ const chainMeta = await mod.fetchSubstrateChainMeta(chainId, {}, metadataRpc, {});
3016
+ return {
3017
+ id: deriveMiniMetadataId({
3018
+ source,
3019
+ chainId,
3020
+ specVersion,
3021
+ libVersion
3022
+ }),
3023
+ source,
3024
+ chainId,
3025
+ specVersion,
3026
+ libVersion,
3027
+ data: chainMeta?.miniMetadata ?? null
3028
+ };
3029
+ }));
3030
+ };
3031
+
3032
+ const getUpdatedMiniMetadatas = async (chainConnector, chaindataProvider, networkId, specVersion) => {
3033
+ const miniMetadatas = await getMiniMetadatas(chainConnector, chaindataProvider, networkId, specVersion);
3034
+ await db.transaction("readwrite", "miniMetadatas", async tx => {
3035
+ await tx.miniMetadatas.where({
3036
+ networkId
3037
+ }).delete();
3038
+ await tx.miniMetadatas.bulkPut(miniMetadatas);
3039
+ });
3040
+ return miniMetadatas;
3041
+ };
3042
+
3043
+ const getMiniMetadata = async (chaindataProvider, chainConnector, chainId, source) => {
3044
+ const specVersion = await getSpecVersion(chainConnector, chainId);
3045
+
3046
+ // TODO when working a chaindata branch, need a way to pass the libVersion used to derive the miniMetadataId got github
3047
+ const miniMetadataId = deriveMiniMetadataId({
3048
+ source,
3049
+ chainId,
3050
+ specVersion,
3051
+ libVersion
3052
+ });
3053
+
3054
+ // lookup local ones
3055
+ const [dbMiniMetadata, ghMiniMetadata] = await Promise.all([db.miniMetadatas.get(miniMetadataId), chaindataProvider.miniMetadataById(miniMetadataId)]);
3056
+ const miniMetadata = dbMiniMetadata ?? ghMiniMetadata;
3057
+ if (miniMetadata) return miniMetadata;
3058
+
3059
+ // update from live chain metadata and persist locally
3060
+ const miniMetadatas = await getUpdatedMiniMetadatas(chainConnector, chaindataProvider, chainId, specVersion);
3061
+ const found = miniMetadatas.find(m => m.id === miniMetadataId);
3062
+ if (!found) {
3063
+ log.warn("MiniMetadata not found in updated miniMetadatas", {
3064
+ source,
3065
+ chainId,
3066
+ specVersion,
3067
+ libVersion,
3068
+ miniMetadataId,
3069
+ miniMetadatas
3070
+ });
3071
+ throw new Error(`MiniMetadata not found for ${source} on ${chainId}`);
3072
+ }
3073
+ return found;
3074
+ };
3075
+
2942
3076
  /**
2943
3077
  * Wraps a BalanceModule's fetch/subscribe methods with a single `balances` method.
2944
3078
  * This `balances` method will subscribe if a callback parameter is provided, or otherwise fetch.
@@ -3244,7 +3378,7 @@ const SubAssetsModule = hydrate => {
3244
3378
  const tokens = {};
3245
3379
  for (const tokenConfig of moduleConfig?.tokens ?? []) {
3246
3380
  try {
3247
- const assetId = typeof tokenConfig.assetId === "number" ? tokenConfig.assetId.toString() : tokenConfig.assetId;
3381
+ const assetId = String(tokenConfig.assetId);
3248
3382
  const assetStateKey = tryEncode(assetCoder, BigInt(assetId)) ?? tryEncode(assetCoder, assetId);
3249
3383
  const metadataStateKey = tryEncode(metadataCoder, BigInt(assetId)) ?? tryEncode(metadataCoder, assetId);
3250
3384
  if (assetStateKey === null || metadataStateKey === null) throw new Error(`Failed to encode stateKey for asset ${assetId} on chain ${chainId}`);
@@ -3286,13 +3420,47 @@ const SubAssetsModule = hydrate => {
3286
3420
  async subscribeBalances({
3287
3421
  addressesByToken
3288
3422
  }, callback) {
3289
- const queries = await buildQueries$3(chaindataProvider, addressesByToken);
3290
- const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe((error, result) => {
3291
- if (error) return callback(error);
3292
- const balances = result?.filter(b => b !== null) ?? [];
3293
- if (balances.length > 0) callback(null, new Balances(balances));
3294
- });
3295
- return unsubscribe;
3423
+ const byNetwork = keys(addressesByToken).reduce((acc, tokenId) => {
3424
+ const networkId = parseSubAssetTokenId(tokenId).networkId;
3425
+ if (!acc[networkId]) acc[networkId] = {};
3426
+ acc[networkId][tokenId] = addressesByToken[tokenId];
3427
+ return acc;
3428
+ }, {});
3429
+ const controller = new AbortController();
3430
+ await Promise.all(toPairs(byNetwork).map(async ([networkId, addressesByToken]) => {
3431
+ const queries = await buildNetworkQueries(networkId, chainConnector, chaindataProvider, addressesByToken);
3432
+ if (controller.signal.aborted) return;
3433
+ const stateHelper = new RpcStateQueryHelper(chainConnector, queries);
3434
+ const unsubscribe = await stateHelper.subscribe((error, result) => {
3435
+ // console.log("SubstrateAssetsModule.callback", { error, result })
3436
+ if (error) return callback(error);
3437
+ const balances = result?.filter(b => b !== null) ?? [];
3438
+ if (balances.length > 0) callback(null, new Balances(balances));
3439
+ });
3440
+ controller.signal.addEventListener("abort", () => {
3441
+ log.debug("TMP subscribeBalances aborted, unsubscribing from network", networkId);
3442
+ unsubscribe();
3443
+ });
3444
+ }));
3445
+
3446
+ // const networkIds = uniq(uniq(keys(addressesByToken)).map((tokenId) => parseSubAssetTokenId(tokenId).networkId))
3447
+ // const
3448
+
3449
+ //console.log("SubstrateAssetsModule.subscribeBalances 1", { addressesByToken })
3450
+ // const queries = await buildQueries(chaindataProvider, addressesByToken)
3451
+ // //console.log("SubstrateAssetsModule.subscribeBalances 2", { queries, addressesByToken })
3452
+ // const unsubscribe = await new RpcStateQueryHelper(chainConnector, queries).subscribe(
3453
+ // (error, result) => {
3454
+ // // console.log("SubstrateAssetsModule.callback", { error, result })
3455
+ // if (error) return callback(error)
3456
+ // const balances = result?.filter((b): b is SubAssetsBalance => b !== null) ?? []
3457
+ // if (balances.length > 0) callback(null, new Balances(balances))
3458
+ // },
3459
+ // )
3460
+
3461
+ return () => {
3462
+ controller.abort();
3463
+ };
3296
3464
  },
3297
3465
  async fetchBalances(addressesByToken) {
3298
3466
  assert(chainConnectors.substrate, "This module requires a substrate chain connector");
@@ -3366,9 +3534,109 @@ const SubAssetsModule = hydrate => {
3366
3534
  }
3367
3535
  };
3368
3536
  };
3537
+ async function buildNetworkQueries(networkId, chainConnector, chaindataProvider, addressesByToken) {
3538
+ const miniMetadata = await getMiniMetadata(chaindataProvider, chainConnector, networkId, moduleType$4);
3539
+ const network = await chaindataProvider.chainById(networkId);
3540
+ const tokensById = await chaindataProvider.tokensById();
3541
+ const chainIds = [networkId];
3542
+ const chains = network ? {
3543
+ [networkId]: network
3544
+ } : {};
3545
+ const miniMetadatas = new Map([[miniMetadata.id, miniMetadata]]);
3546
+ const chainStorageCoders = buildStorageCoders({
3547
+ chainIds,
3548
+ chains,
3549
+ miniMetadatas,
3550
+ moduleType: moduleType$4,
3551
+ coders: {
3552
+ storage: ["Assets", "Account"]
3553
+ }
3554
+ });
3555
+ return Object.entries(addressesByToken).flatMap(([tokenId, addresses]) => {
3556
+ const token = tokensById[tokenId];
3557
+ if (!token) {
3558
+ log.warn(`Token ${tokenId} not found`);
3559
+ return [];
3560
+ }
3561
+ if (token.type !== "substrate-assets") {
3562
+ log.debug(`This module doesn't handle tokens of type ${token.type}`);
3563
+ return [];
3564
+ }
3565
+ const networkId = token.networkId;
3566
+ if (!networkId) {
3567
+ log.warn(`Token ${tokenId} has no chain`);
3568
+ return [];
3569
+ }
3570
+ const chain = chains[networkId];
3571
+ if (!chain) {
3572
+ log.warn(`Chain ${networkId} for token ${tokenId} not found`);
3573
+ return [];
3574
+ }
3575
+ return addresses.flatMap(address => {
3576
+ const scaleCoder = chainStorageCoders.get(networkId)?.storage;
3577
+ const stateKey = tryEncode(scaleCoder, BigInt(token.assetId), address) ?? tryEncode(scaleCoder, token.assetId, address);
3578
+ if (!stateKey) {
3579
+ log.warn(`Invalid assetId / address in ${networkId} storage query ${token.assetId} / ${address}`);
3580
+ return [];
3581
+ }
3582
+ const decodeResult = change => {
3583
+ /** NOTE: This type is only a hint for typescript, the chain can actually return whatever it wants to */
3584
+
3585
+ const decoded = decodeScale(scaleCoder, change, `Failed to decode substrate-assets balance on chain ${networkId}`) ?? {
3586
+ balance: 0n,
3587
+ status: {
3588
+ type: "Liquid"
3589
+ }};
3590
+ const isFrozen = decoded?.status?.type === "Frozen";
3591
+ const amount = (decoded?.balance ?? 0n).toString();
3592
+
3593
+ // due to the following balance calculations, which are made in the `Balance` type:
3594
+ //
3595
+ // total balance = (free balance) + (reserved balance)
3596
+ // transferable balance = (free balance) - (frozen balance)
3597
+ //
3598
+ // when `isFrozen` is true we need to set **both** the `free` and `frozen` amounts
3599
+ // of this balance to the value we received from the RPC.
3600
+ //
3601
+ // if we only set the `frozen` amount, then the `total` calculation will be incorrect!
3602
+ const free = amount;
3603
+ const frozen = token.isFrozen || isFrozen ? amount : "0";
3604
+
3605
+ // include balance values even if zero, so that newly-zero values overwrite old values
3606
+ const balanceValues = [{
3607
+ type: "free",
3608
+ label: "free",
3609
+ amount: free.toString()
3610
+ }, {
3611
+ type: "locked",
3612
+ label: "frozen",
3613
+ amount: frozen.toString()
3614
+ }];
3615
+ return {
3616
+ source: "substrate-assets",
3617
+ status: "live",
3618
+ address,
3619
+ networkId,
3620
+ tokenId: token.id,
3621
+ values: balanceValues
3622
+ };
3623
+ };
3624
+ return {
3625
+ chainId: networkId,
3626
+ stateKey,
3627
+ decodeResult
3628
+ };
3629
+ });
3630
+ });
3631
+ }
3369
3632
  async function buildQueries$3(chaindataProvider, addressesByToken) {
3370
3633
  const allChains = await chaindataProvider.chainsById();
3371
3634
  const tokens = await chaindataProvider.tokensById();
3635
+
3636
+ // const networkIds = Object.keys(addressesByToken)
3637
+
3638
+ // const
3639
+ // const miniMetadatas = await getMiniMetadatas(chainConnector, chaindataProvider, network)
3372
3640
  const miniMetadatas = new Map((await db.miniMetadatas.toArray()).map(miniMetadata => [miniMetadata.id, miniMetadata]));
3373
3641
  const uniqueChainIds = getUniqueChainIds(addressesByToken, tokens);
3374
3642
  const chains = Object.fromEntries(uniqueChainIds.map(chainId => [chainId, allChains[chainId]]));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@talismn/balances",
3
- "version": "0.0.0-pr2043-20250618043535",
3
+ "version": "0.0.0-pr2043-20250618090234",
4
4
  "author": "Talisman",
5
5
  "homepage": "https://talisman.xyz",
6
6
  "license": "GPL-3.0-or-later",
@@ -33,12 +33,12 @@
33
33
  "rxjs": "^7.8.1",
34
34
  "scale-ts": "^1.6.1",
35
35
  "viem": "^2.27.3",
36
- "@talismn/chain-connector": "0.0.0-pr2043-20250618043535",
37
- "@talismn/chain-connector-evm": "0.0.0-pr2043-20250618043535",
38
- "@talismn/chaindata-provider": "0.0.0-pr2043-20250618043535",
39
- "@talismn/sapi": "0.0.0-pr2043-20250618043535",
36
+ "@talismn/chain-connector-evm": "0.0.0-pr2043-20250618090234",
37
+ "@talismn/chaindata-provider": "0.0.0-pr2043-20250618090234",
38
+ "@talismn/chain-connector": "0.0.0-pr2043-20250618090234",
39
+ "@talismn/sapi": "0.0.0-pr2043-20250618090234",
40
40
  "@talismn/scale": "0.1.2",
41
- "@talismn/token-rates": "0.0.0-pr2043-20250618043535",
41
+ "@talismn/token-rates": "0.0.0-pr2043-20250618090234",
42
42
  "@talismn/util": "0.4.2"
43
43
  },
44
44
  "devDependencies": {