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

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.
@@ -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-20250618082459"};
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 = 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-20250618082459",
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": "0.0.0-pr2043-20250618082459",
37
+ "@talismn/chaindata-provider": "0.0.0-pr2043-20250618082459",
38
+ "@talismn/sapi": "0.0.0-pr2043-20250618082459",
39
+ "@talismn/token-rates": "0.0.0-pr2043-20250618082459",
40
+ "@talismn/chain-connector-evm": "0.0.0-pr2043-20250618082459",
40
41
  "@talismn/scale": "0.1.2",
41
- "@talismn/token-rates": "0.0.0-pr2043-20250618043535",
42
42
  "@talismn/util": "0.4.2"
43
43
  },
44
44
  "devDependencies": {
@@ -54,8 +54,8 @@
54
54
  "jest": "^29.7.0",
55
55
  "ts-jest": "^29.2.5",
56
56
  "typescript": "^5.6.3",
57
- "@talismn/eslint-config": "0.0.3",
58
- "@talismn/tsconfig": "0.0.2"
57
+ "@talismn/tsconfig": "0.0.2",
58
+ "@talismn/eslint-config": "0.0.3"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@polkadot/api-contract": "*",