@talismn/balances 0.0.0-pr2277-20251211070508 → 0.0.0-pr2279-20251222112424

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,11 @@
1
+ import { GetDynamicInfosResult, SubDTaoBalance } from "./types";
2
+ type DynamicInfo = NonNullable<GetDynamicInfosResult[number]>;
3
+ export declare const calculatePendingRootClaimable: ({ stake, hotkey, address, networkId, validatorRootClaimableRate, dynamicInfoByNetuid, }: {
4
+ stake: bigint;
5
+ hotkey: string;
6
+ address: string;
7
+ networkId: string;
8
+ validatorRootClaimableRate: Map<number, bigint>;
9
+ dynamicInfoByNetuid: Record<number, DynamicInfo | undefined>;
10
+ }) => SubDTaoBalance[];
11
+ export {};
@@ -1,3 +1,4 @@
1
+ import type { bittensor } from "@polkadot-api/descriptors";
1
2
  import z from "zod/v4";
2
3
  export declare const SubDTaoTokenConfigSchema: z.ZodObject<{
3
4
  symbol: z.ZodOptional<z.ZodString>;
@@ -15,3 +16,15 @@ export type SubDTaoTokenConfig = z.infer<typeof SubDTaoTokenConfigSchema>;
15
16
  export type SubDTaoBalanceMeta = {
16
17
  scaledAlphaPrice: string;
17
18
  };
19
+ export type SubDTaoBalance = {
20
+ address: string;
21
+ tokenId: string;
22
+ baseTokenId: string;
23
+ stake: bigint;
24
+ pendingRootClaim?: bigint;
25
+ hotkey: string;
26
+ netuid: number;
27
+ scaledAlphaPrice: bigint;
28
+ };
29
+ export type GetDynamicInfosResult = (typeof bittensor)["descriptors"]["apis"]["SubnetInfoRuntimeApi"]["get_all_dynamic_info"][1];
30
+ export type GetStakeInfosResult = (typeof bittensor)["descriptors"]["apis"]["StakeInfoRuntimeApi"]["get_stake_info_for_coldkeys"][1];
@@ -2295,6 +2295,42 @@ const taoToAlpha = (tao, scaledAlphaPrice) => {
2295
2295
  return tao * ALPHA_PRICE_SCALE / scaledAlphaPrice;
2296
2296
  };
2297
2297
 
2298
+ const calculatePendingRootClaimable = ({
2299
+ stake,
2300
+ hotkey,
2301
+ address,
2302
+ networkId,
2303
+ validatorRootClaimableRate,
2304
+ dynamicInfoByNetuid
2305
+ }) => {
2306
+ const pendingRootClaimBalances = [];
2307
+ for (const [netuid, claimableRate] of validatorRootClaimableRate) {
2308
+ if (claimableRate === 0n) {
2309
+ continue;
2310
+ }
2311
+ const dynamicInfo = dynamicInfoByNetuid[netuid];
2312
+ const scaledAlphaPrice = dynamicInfo ? getScaledAlphaPrice(dynamicInfo.alpha_in, dynamicInfo.tao_in) : 0n;
2313
+ // Calculate claimable = claimable_rate * root_stake
2314
+ // Note: claimableRate is a I96F32, a fixed-point number format
2315
+
2316
+ // Multiply claimable_rate by root_stake
2317
+ // I96F32 multiplication: round((a * b) / 2^32)
2318
+ const pendingRootClaim = stake * claimableRate + (1n << 31n) >> 32n;
2319
+ pendingRootClaimBalances.push({
2320
+ address,
2321
+ tokenId: chaindataProvider.subDTaoTokenId(networkId, netuid, hotkey),
2322
+ baseTokenId: chaindataProvider.subDTaoTokenId(networkId, netuid),
2323
+ hotkey: hotkey,
2324
+ netuid: netuid,
2325
+ scaledAlphaPrice,
2326
+ pendingRootClaim,
2327
+ stake: 0n
2328
+ });
2329
+ }
2330
+ return pendingRootClaimBalances;
2331
+ };
2332
+
2333
+ const ROOT_NETUID = 0;
2298
2334
  const fetchBalances$5 = async ({
2299
2335
  networkId,
2300
2336
  tokensWithAddresses,
@@ -2344,20 +2380,63 @@ const fetchBalances$5 = async ({
2344
2380
  const addresses = lodashEs.uniq(balanceDefs.map(def => def.address));
2345
2381
  try {
2346
2382
  const [stakeInfos, dynamicInfos] = await Promise.all([fetchRuntimeCallResult(connector, networkId, miniMetadata.data, "StakeInfoRuntimeApi", "get_stake_info_for_coldkeys", [addresses]), fetchRuntimeCallResult(connector, networkId, miniMetadata.data, "SubnetInfoRuntimeApi", "get_all_dynamic_info", [])]);
2383
+ const rootHotkeys = lodashEs.uniq(stakeInfos.flatMap(([, stakes]) => stakes.filter(stake => stake.netuid === ROOT_NETUID).map(stake => stake.hotkey)));
2384
+ const rootClaimableRatesByHotkey = rootHotkeys.length && miniMetadata.data ? await fetchRootClaimableRates(connector, networkId, miniMetadata.data, rootHotkeys) : new Map();
2347
2385
  const dynamicInfoByNetuid = lodashEs.keyBy(dynamicInfos.filter(util.isNotNil), info => info.netuid);
2348
- const balances = stakeInfos.flatMap(([address, stakes]) => stakes.map(stake => {
2349
- const dynamicInfo = dynamicInfoByNetuid[stake.netuid];
2350
- const scaledAlphaPrice = dynamicInfo ? getScaledAlphaPrice(dynamicInfo.alpha_in, dynamicInfo.tao_in) : 0n;
2351
- return {
2352
- address,
2353
- tokenId: chaindataProvider.subDTaoTokenId(networkId, stake.netuid, stake.hotkey),
2354
- baseTokenId: chaindataProvider.subDTaoTokenId(networkId, stake.netuid),
2355
- stake: stake.stake,
2356
- hotkey: stake.hotkey,
2357
- netuid: stake.netuid,
2358
- scaledAlphaPrice
2359
- };
2360
- }));
2386
+
2387
+ // Upserts a balance into the accumulator, merging stake values if the balance already exists.
2388
+ // Eg: Acc X has root staked with validator Y, but also staked on sn 45 with the same validator Y.
2389
+ // We merge the pending root claim of sn 45 and the sn 45 stake in the same balance.
2390
+ const upsertBalance = (acc, address, tokenId, balance) => {
2391
+ const key = `${address}:${tokenId}`;
2392
+ const recordedBalance = acc[key];
2393
+ if (recordedBalance) {
2394
+ acc[key] = {
2395
+ ...recordedBalance,
2396
+ stake: recordedBalance.stake + balance.stake,
2397
+ // If the new balance has pendingRootClaim, use it (it's calculated from current state)
2398
+ ...(balance.pendingRootClaim !== undefined && {
2399
+ pendingRootClaim: balance.pendingRootClaim
2400
+ })
2401
+ };
2402
+ } else {
2403
+ acc[key] = balance;
2404
+ }
2405
+ };
2406
+ const balancesRaw = stakeInfos.reduce((acc, [address, stakes]) => {
2407
+ for (const stake of stakes) {
2408
+ // Regular stake cases
2409
+ const dynamicInfo = dynamicInfoByNetuid[stake.netuid];
2410
+ const scaledAlphaPrice = dynamicInfo ? getScaledAlphaPrice(dynamicInfo.alpha_in, dynamicInfo.tao_in) : 0n;
2411
+ const balance = {
2412
+ address,
2413
+ tokenId: chaindataProvider.subDTaoTokenId(networkId, stake.netuid, stake.hotkey),
2414
+ baseTokenId: chaindataProvider.subDTaoTokenId(networkId, stake.netuid),
2415
+ stake: stake.stake,
2416
+ hotkey: stake.hotkey,
2417
+ netuid: stake.netuid,
2418
+ scaledAlphaPrice
2419
+ };
2420
+ upsertBalance(acc, address, balance.tokenId, balance);
2421
+
2422
+ // Root stake cases, we need to calculate the pending root claim and add to the balances
2423
+ if (stake.netuid === ROOT_NETUID) {
2424
+ const pendingRootClaimBalances = calculatePendingRootClaimable({
2425
+ stake: stake.stake,
2426
+ hotkey: stake.hotkey,
2427
+ address,
2428
+ networkId,
2429
+ validatorRootClaimableRate: rootClaimableRatesByHotkey.get(stake.hotkey) ?? new Map(),
2430
+ dynamicInfoByNetuid
2431
+ });
2432
+ pendingRootClaimBalances.forEach(balance => {
2433
+ upsertBalance(acc, address, balance.tokenId, balance);
2434
+ });
2435
+ }
2436
+ }
2437
+ return acc;
2438
+ }, {});
2439
+ const balances = Object.values(balancesRaw);
2361
2440
  const tokensById = lodashEs.keyBy(tokensWithAddresses.map(([token]) => token), t => t.id);
2362
2441
  const dynamicTokens = [];
2363
2442
 
@@ -2383,19 +2462,45 @@ const fetchBalances$5 = async ({
2383
2462
  const meta = {
2384
2463
  scaledAlphaPrice: stake?.scaledAlphaPrice.toString() ?? "0"
2385
2464
  };
2465
+ const stakeAmount = BigInt(stake?.stake?.toString() ?? "0");
2466
+ const pendingRootClaimAmount = BigInt(stake?.pendingRootClaim?.toString() ?? "0");
2467
+ const hasZeroStake = stakeAmount === 0n;
2468
+ const hasPendingRootClaim = pendingRootClaimAmount > 0n;
2386
2469
  const balanceValue = {
2387
2470
  type: "free",
2388
2471
  label: stake?.netuid === 0 ? "Root Staking" : `Subnet Staking`,
2389
- amount: stake?.stake.toString() ?? "0",
2472
+ amount: stakeAmount.toString(),
2390
2473
  meta
2391
2474
  };
2475
+ const pendingRootClaimValue = {
2476
+ type: "locked",
2477
+ label: "Pending root claim",
2478
+ amount: pendingRootClaimAmount.toString(),
2479
+ meta
2480
+ };
2481
+ const values = [balanceValue, pendingRootClaimValue];
2482
+
2483
+ // If stake is 0n but there's a pendingRootClaim, add it as an extra amount
2484
+ // with includeInTotal: true so it counts toward the total balance.
2485
+ // This ensures the balance isn't filtered out when stake is 0n.
2486
+ // The total.planck calculation is: free + reserved + extra (with includeInTotal: true)
2487
+ // So by adding pendingRootClaim as extra, it will be included in total.planck.
2488
+ if (hasZeroStake && hasPendingRootClaim) {
2489
+ values.push({
2490
+ type: "extra",
2491
+ label: "Pending root claim",
2492
+ amount: pendingRootClaimAmount.toString(),
2493
+ includeInTotal: true,
2494
+ meta
2495
+ });
2496
+ }
2392
2497
  return {
2393
2498
  address: def.address,
2394
2499
  networkId,
2395
2500
  tokenId: def.token.id,
2396
2501
  source: MODULE_TYPE$5,
2397
2502
  status: "live",
2398
- values: [balanceValue]
2503
+ values
2399
2504
  };
2400
2505
  });
2401
2506
  return {
@@ -2418,6 +2523,68 @@ const fetchBalances$5 = async ({
2418
2523
  };
2419
2524
  }
2420
2525
  };
2526
+ const buildStorageCoder = (metadataRpc, pallet, entry) => {
2527
+ const {
2528
+ builder
2529
+ } = scale.parseMetadataRpc(metadataRpc);
2530
+ return builder.buildStorage(pallet, entry);
2531
+ };
2532
+ const buildRootClaimableStorageCoder = async (connector, networkId, metadataRpc) => {
2533
+ let storageCoder = null;
2534
+ if (metadataRpc) {
2535
+ try {
2536
+ storageCoder = buildStorageCoder(metadataRpc, "SubtensorModule", "RootClaimable");
2537
+ } catch (cause) {
2538
+ log.warn(`Failed to build storage coder for SubtensorModule.RootClaimable using provided metadata on ${networkId}`, {
2539
+ cause
2540
+ });
2541
+ }
2542
+ }
2543
+ return storageCoder;
2544
+ };
2545
+ const buildRootClaimableQueries = (networkId, hotkeys, storageCoder) => {
2546
+ return hotkeys.map(hotkey => {
2547
+ let stateKey = null;
2548
+ try {
2549
+ stateKey = storageCoder.keys.enc(hotkey);
2550
+ } catch (cause) {
2551
+ log.warn(`Failed to encode storage key for hotkey ${hotkey} on ${networkId}`, {
2552
+ cause
2553
+ });
2554
+ }
2555
+ const decodeResult = changes => {
2556
+ const hexValue = changes[0];
2557
+ if (!hexValue) {
2558
+ return [hotkey, new Map()];
2559
+ }
2560
+ const decoded = scale.decodeScale(storageCoder, hexValue, `Failed to decode RootClaimable for hotkey ${hotkey} on ${networkId}`);
2561
+ return [hotkey, decoded ? new Map(decoded) : new Map()];
2562
+ };
2563
+ return {
2564
+ stateKeys: [stateKey],
2565
+ decodeResult
2566
+ };
2567
+ });
2568
+ };
2569
+ const fetchRootClaimableRates = async (connector, networkId, metadataRpc, hotkeys) => {
2570
+ if (!hotkeys.length) return new Map();
2571
+ const storageCoder = await buildRootClaimableStorageCoder(connector, networkId, metadataRpc);
2572
+ if (!storageCoder) {
2573
+ // Fallback: return empty map for all hotkeys
2574
+ return new Map(hotkeys.map(hotkey => [hotkey, new Map()]));
2575
+ }
2576
+ const queries = buildRootClaimableQueries(networkId, hotkeys, storageCoder);
2577
+ try {
2578
+ const results = await fetchRpcQueryPack(connector, networkId, queries);
2579
+ return new Map(results);
2580
+ } catch (cause) {
2581
+ log.warn(`Failed to fetch RootClaimable for hotkeys on ${networkId}`, {
2582
+ cause
2583
+ });
2584
+ // Fallback: return empty map for all hotkeys
2585
+ return new Map(hotkeys.map(hotkey => [hotkey, new Map()]));
2586
+ }
2587
+ };
2421
2588
 
2422
2589
  // hardcoded because we dont have access to native tokens from the balance module
2423
2590
  const NATIVE_TOKEN_SYMBOLS = {
@@ -2517,7 +2684,7 @@ const getData$4 = metadataRpc => {
2517
2684
  if (!isBittensor) return null;
2518
2685
  scale.compactMetadata(metadata, [{
2519
2686
  pallet: "SubtensorModule",
2520
- items: ["TransferToggle"]
2687
+ items: ["TransferToggle", "RootClaimable"]
2521
2688
  }], [{
2522
2689
  runtimeApi: "StakeInfoRuntimeApi",
2523
2690
  methods: ["get_stake_info_for_coldkeys"]
@@ -2295,6 +2295,42 @@ const taoToAlpha = (tao, scaledAlphaPrice) => {
2295
2295
  return tao * ALPHA_PRICE_SCALE / scaledAlphaPrice;
2296
2296
  };
2297
2297
 
2298
+ const calculatePendingRootClaimable = ({
2299
+ stake,
2300
+ hotkey,
2301
+ address,
2302
+ networkId,
2303
+ validatorRootClaimableRate,
2304
+ dynamicInfoByNetuid
2305
+ }) => {
2306
+ const pendingRootClaimBalances = [];
2307
+ for (const [netuid, claimableRate] of validatorRootClaimableRate) {
2308
+ if (claimableRate === 0n) {
2309
+ continue;
2310
+ }
2311
+ const dynamicInfo = dynamicInfoByNetuid[netuid];
2312
+ const scaledAlphaPrice = dynamicInfo ? getScaledAlphaPrice(dynamicInfo.alpha_in, dynamicInfo.tao_in) : 0n;
2313
+ // Calculate claimable = claimable_rate * root_stake
2314
+ // Note: claimableRate is a I96F32, a fixed-point number format
2315
+
2316
+ // Multiply claimable_rate by root_stake
2317
+ // I96F32 multiplication: round((a * b) / 2^32)
2318
+ const pendingRootClaim = stake * claimableRate + (1n << 31n) >> 32n;
2319
+ pendingRootClaimBalances.push({
2320
+ address,
2321
+ tokenId: chaindataProvider.subDTaoTokenId(networkId, netuid, hotkey),
2322
+ baseTokenId: chaindataProvider.subDTaoTokenId(networkId, netuid),
2323
+ hotkey: hotkey,
2324
+ netuid: netuid,
2325
+ scaledAlphaPrice,
2326
+ pendingRootClaim,
2327
+ stake: 0n
2328
+ });
2329
+ }
2330
+ return pendingRootClaimBalances;
2331
+ };
2332
+
2333
+ const ROOT_NETUID = 0;
2298
2334
  const fetchBalances$5 = async ({
2299
2335
  networkId,
2300
2336
  tokensWithAddresses,
@@ -2344,20 +2380,63 @@ const fetchBalances$5 = async ({
2344
2380
  const addresses = lodashEs.uniq(balanceDefs.map(def => def.address));
2345
2381
  try {
2346
2382
  const [stakeInfos, dynamicInfos] = await Promise.all([fetchRuntimeCallResult(connector, networkId, miniMetadata.data, "StakeInfoRuntimeApi", "get_stake_info_for_coldkeys", [addresses]), fetchRuntimeCallResult(connector, networkId, miniMetadata.data, "SubnetInfoRuntimeApi", "get_all_dynamic_info", [])]);
2383
+ const rootHotkeys = lodashEs.uniq(stakeInfos.flatMap(([, stakes]) => stakes.filter(stake => stake.netuid === ROOT_NETUID).map(stake => stake.hotkey)));
2384
+ const rootClaimableRatesByHotkey = rootHotkeys.length && miniMetadata.data ? await fetchRootClaimableRates(connector, networkId, miniMetadata.data, rootHotkeys) : new Map();
2347
2385
  const dynamicInfoByNetuid = lodashEs.keyBy(dynamicInfos.filter(util.isNotNil), info => info.netuid);
2348
- const balances = stakeInfos.flatMap(([address, stakes]) => stakes.map(stake => {
2349
- const dynamicInfo = dynamicInfoByNetuid[stake.netuid];
2350
- const scaledAlphaPrice = dynamicInfo ? getScaledAlphaPrice(dynamicInfo.alpha_in, dynamicInfo.tao_in) : 0n;
2351
- return {
2352
- address,
2353
- tokenId: chaindataProvider.subDTaoTokenId(networkId, stake.netuid, stake.hotkey),
2354
- baseTokenId: chaindataProvider.subDTaoTokenId(networkId, stake.netuid),
2355
- stake: stake.stake,
2356
- hotkey: stake.hotkey,
2357
- netuid: stake.netuid,
2358
- scaledAlphaPrice
2359
- };
2360
- }));
2386
+
2387
+ // Upserts a balance into the accumulator, merging stake values if the balance already exists.
2388
+ // Eg: Acc X has root staked with validator Y, but also staked on sn 45 with the same validator Y.
2389
+ // We merge the pending root claim of sn 45 and the sn 45 stake in the same balance.
2390
+ const upsertBalance = (acc, address, tokenId, balance) => {
2391
+ const key = `${address}:${tokenId}`;
2392
+ const recordedBalance = acc[key];
2393
+ if (recordedBalance) {
2394
+ acc[key] = {
2395
+ ...recordedBalance,
2396
+ stake: recordedBalance.stake + balance.stake,
2397
+ // If the new balance has pendingRootClaim, use it (it's calculated from current state)
2398
+ ...(balance.pendingRootClaim !== undefined && {
2399
+ pendingRootClaim: balance.pendingRootClaim
2400
+ })
2401
+ };
2402
+ } else {
2403
+ acc[key] = balance;
2404
+ }
2405
+ };
2406
+ const balancesRaw = stakeInfos.reduce((acc, [address, stakes]) => {
2407
+ for (const stake of stakes) {
2408
+ // Regular stake cases
2409
+ const dynamicInfo = dynamicInfoByNetuid[stake.netuid];
2410
+ const scaledAlphaPrice = dynamicInfo ? getScaledAlphaPrice(dynamicInfo.alpha_in, dynamicInfo.tao_in) : 0n;
2411
+ const balance = {
2412
+ address,
2413
+ tokenId: chaindataProvider.subDTaoTokenId(networkId, stake.netuid, stake.hotkey),
2414
+ baseTokenId: chaindataProvider.subDTaoTokenId(networkId, stake.netuid),
2415
+ stake: stake.stake,
2416
+ hotkey: stake.hotkey,
2417
+ netuid: stake.netuid,
2418
+ scaledAlphaPrice
2419
+ };
2420
+ upsertBalance(acc, address, balance.tokenId, balance);
2421
+
2422
+ // Root stake cases, we need to calculate the pending root claim and add to the balances
2423
+ if (stake.netuid === ROOT_NETUID) {
2424
+ const pendingRootClaimBalances = calculatePendingRootClaimable({
2425
+ stake: stake.stake,
2426
+ hotkey: stake.hotkey,
2427
+ address,
2428
+ networkId,
2429
+ validatorRootClaimableRate: rootClaimableRatesByHotkey.get(stake.hotkey) ?? new Map(),
2430
+ dynamicInfoByNetuid
2431
+ });
2432
+ pendingRootClaimBalances.forEach(balance => {
2433
+ upsertBalance(acc, address, balance.tokenId, balance);
2434
+ });
2435
+ }
2436
+ }
2437
+ return acc;
2438
+ }, {});
2439
+ const balances = Object.values(balancesRaw);
2361
2440
  const tokensById = lodashEs.keyBy(tokensWithAddresses.map(([token]) => token), t => t.id);
2362
2441
  const dynamicTokens = [];
2363
2442
 
@@ -2383,19 +2462,45 @@ const fetchBalances$5 = async ({
2383
2462
  const meta = {
2384
2463
  scaledAlphaPrice: stake?.scaledAlphaPrice.toString() ?? "0"
2385
2464
  };
2465
+ const stakeAmount = BigInt(stake?.stake?.toString() ?? "0");
2466
+ const pendingRootClaimAmount = BigInt(stake?.pendingRootClaim?.toString() ?? "0");
2467
+ const hasZeroStake = stakeAmount === 0n;
2468
+ const hasPendingRootClaim = pendingRootClaimAmount > 0n;
2386
2469
  const balanceValue = {
2387
2470
  type: "free",
2388
2471
  label: stake?.netuid === 0 ? "Root Staking" : `Subnet Staking`,
2389
- amount: stake?.stake.toString() ?? "0",
2472
+ amount: stakeAmount.toString(),
2390
2473
  meta
2391
2474
  };
2475
+ const pendingRootClaimValue = {
2476
+ type: "locked",
2477
+ label: "Pending root claim",
2478
+ amount: pendingRootClaimAmount.toString(),
2479
+ meta
2480
+ };
2481
+ const values = [balanceValue, pendingRootClaimValue];
2482
+
2483
+ // If stake is 0n but there's a pendingRootClaim, add it as an extra amount
2484
+ // with includeInTotal: true so it counts toward the total balance.
2485
+ // This ensures the balance isn't filtered out when stake is 0n.
2486
+ // The total.planck calculation is: free + reserved + extra (with includeInTotal: true)
2487
+ // So by adding pendingRootClaim as extra, it will be included in total.planck.
2488
+ if (hasZeroStake && hasPendingRootClaim) {
2489
+ values.push({
2490
+ type: "extra",
2491
+ label: "Pending root claim",
2492
+ amount: pendingRootClaimAmount.toString(),
2493
+ includeInTotal: true,
2494
+ meta
2495
+ });
2496
+ }
2392
2497
  return {
2393
2498
  address: def.address,
2394
2499
  networkId,
2395
2500
  tokenId: def.token.id,
2396
2501
  source: MODULE_TYPE$5,
2397
2502
  status: "live",
2398
- values: [balanceValue]
2503
+ values
2399
2504
  };
2400
2505
  });
2401
2506
  return {
@@ -2418,6 +2523,68 @@ const fetchBalances$5 = async ({
2418
2523
  };
2419
2524
  }
2420
2525
  };
2526
+ const buildStorageCoder = (metadataRpc, pallet, entry) => {
2527
+ const {
2528
+ builder
2529
+ } = scale.parseMetadataRpc(metadataRpc);
2530
+ return builder.buildStorage(pallet, entry);
2531
+ };
2532
+ const buildRootClaimableStorageCoder = async (connector, networkId, metadataRpc) => {
2533
+ let storageCoder = null;
2534
+ if (metadataRpc) {
2535
+ try {
2536
+ storageCoder = buildStorageCoder(metadataRpc, "SubtensorModule", "RootClaimable");
2537
+ } catch (cause) {
2538
+ log.warn(`Failed to build storage coder for SubtensorModule.RootClaimable using provided metadata on ${networkId}`, {
2539
+ cause
2540
+ });
2541
+ }
2542
+ }
2543
+ return storageCoder;
2544
+ };
2545
+ const buildRootClaimableQueries = (networkId, hotkeys, storageCoder) => {
2546
+ return hotkeys.map(hotkey => {
2547
+ let stateKey = null;
2548
+ try {
2549
+ stateKey = storageCoder.keys.enc(hotkey);
2550
+ } catch (cause) {
2551
+ log.warn(`Failed to encode storage key for hotkey ${hotkey} on ${networkId}`, {
2552
+ cause
2553
+ });
2554
+ }
2555
+ const decodeResult = changes => {
2556
+ const hexValue = changes[0];
2557
+ if (!hexValue) {
2558
+ return [hotkey, new Map()];
2559
+ }
2560
+ const decoded = scale.decodeScale(storageCoder, hexValue, `Failed to decode RootClaimable for hotkey ${hotkey} on ${networkId}`);
2561
+ return [hotkey, decoded ? new Map(decoded) : new Map()];
2562
+ };
2563
+ return {
2564
+ stateKeys: [stateKey],
2565
+ decodeResult
2566
+ };
2567
+ });
2568
+ };
2569
+ const fetchRootClaimableRates = async (connector, networkId, metadataRpc, hotkeys) => {
2570
+ if (!hotkeys.length) return new Map();
2571
+ const storageCoder = await buildRootClaimableStorageCoder(connector, networkId, metadataRpc);
2572
+ if (!storageCoder) {
2573
+ // Fallback: return empty map for all hotkeys
2574
+ return new Map(hotkeys.map(hotkey => [hotkey, new Map()]));
2575
+ }
2576
+ const queries = buildRootClaimableQueries(networkId, hotkeys, storageCoder);
2577
+ try {
2578
+ const results = await fetchRpcQueryPack(connector, networkId, queries);
2579
+ return new Map(results);
2580
+ } catch (cause) {
2581
+ log.warn(`Failed to fetch RootClaimable for hotkeys on ${networkId}`, {
2582
+ cause
2583
+ });
2584
+ // Fallback: return empty map for all hotkeys
2585
+ return new Map(hotkeys.map(hotkey => [hotkey, new Map()]));
2586
+ }
2587
+ };
2421
2588
 
2422
2589
  // hardcoded because we dont have access to native tokens from the balance module
2423
2590
  const NATIVE_TOKEN_SYMBOLS = {
@@ -2517,7 +2684,7 @@ const getData$4 = metadataRpc => {
2517
2684
  if (!isBittensor) return null;
2518
2685
  scale.compactMetadata(metadata, [{
2519
2686
  pallet: "SubtensorModule",
2520
- items: ["TransferToggle"]
2687
+ items: ["TransferToggle", "RootClaimable"]
2521
2688
  }], [{
2522
2689
  runtimeApi: "StakeInfoRuntimeApi",
2523
2690
  methods: ["get_stake_info_for_coldkeys"]
@@ -2286,6 +2286,42 @@ const taoToAlpha = (tao, scaledAlphaPrice) => {
2286
2286
  return tao * ALPHA_PRICE_SCALE / scaledAlphaPrice;
2287
2287
  };
2288
2288
 
2289
+ const calculatePendingRootClaimable = ({
2290
+ stake,
2291
+ hotkey,
2292
+ address,
2293
+ networkId,
2294
+ validatorRootClaimableRate,
2295
+ dynamicInfoByNetuid
2296
+ }) => {
2297
+ const pendingRootClaimBalances = [];
2298
+ for (const [netuid, claimableRate] of validatorRootClaimableRate) {
2299
+ if (claimableRate === 0n) {
2300
+ continue;
2301
+ }
2302
+ const dynamicInfo = dynamicInfoByNetuid[netuid];
2303
+ const scaledAlphaPrice = dynamicInfo ? getScaledAlphaPrice(dynamicInfo.alpha_in, dynamicInfo.tao_in) : 0n;
2304
+ // Calculate claimable = claimable_rate * root_stake
2305
+ // Note: claimableRate is a I96F32, a fixed-point number format
2306
+
2307
+ // Multiply claimable_rate by root_stake
2308
+ // I96F32 multiplication: round((a * b) / 2^32)
2309
+ const pendingRootClaim = stake * claimableRate + (1n << 31n) >> 32n;
2310
+ pendingRootClaimBalances.push({
2311
+ address,
2312
+ tokenId: subDTaoTokenId(networkId, netuid, hotkey),
2313
+ baseTokenId: subDTaoTokenId(networkId, netuid),
2314
+ hotkey: hotkey,
2315
+ netuid: netuid,
2316
+ scaledAlphaPrice,
2317
+ pendingRootClaim,
2318
+ stake: 0n
2319
+ });
2320
+ }
2321
+ return pendingRootClaimBalances;
2322
+ };
2323
+
2324
+ const ROOT_NETUID = 0;
2289
2325
  const fetchBalances$5 = async ({
2290
2326
  networkId,
2291
2327
  tokensWithAddresses,
@@ -2335,20 +2371,63 @@ const fetchBalances$5 = async ({
2335
2371
  const addresses = uniq(balanceDefs.map(def => def.address));
2336
2372
  try {
2337
2373
  const [stakeInfos, dynamicInfos] = await Promise.all([fetchRuntimeCallResult(connector, networkId, miniMetadata.data, "StakeInfoRuntimeApi", "get_stake_info_for_coldkeys", [addresses]), fetchRuntimeCallResult(connector, networkId, miniMetadata.data, "SubnetInfoRuntimeApi", "get_all_dynamic_info", [])]);
2374
+ const rootHotkeys = uniq(stakeInfos.flatMap(([, stakes]) => stakes.filter(stake => stake.netuid === ROOT_NETUID).map(stake => stake.hotkey)));
2375
+ const rootClaimableRatesByHotkey = rootHotkeys.length && miniMetadata.data ? await fetchRootClaimableRates(connector, networkId, miniMetadata.data, rootHotkeys) : new Map();
2338
2376
  const dynamicInfoByNetuid = keyBy(dynamicInfos.filter(isNotNil), info => info.netuid);
2339
- const balances = stakeInfos.flatMap(([address, stakes]) => stakes.map(stake => {
2340
- const dynamicInfo = dynamicInfoByNetuid[stake.netuid];
2341
- const scaledAlphaPrice = dynamicInfo ? getScaledAlphaPrice(dynamicInfo.alpha_in, dynamicInfo.tao_in) : 0n;
2342
- return {
2343
- address,
2344
- tokenId: subDTaoTokenId(networkId, stake.netuid, stake.hotkey),
2345
- baseTokenId: subDTaoTokenId(networkId, stake.netuid),
2346
- stake: stake.stake,
2347
- hotkey: stake.hotkey,
2348
- netuid: stake.netuid,
2349
- scaledAlphaPrice
2350
- };
2351
- }));
2377
+
2378
+ // Upserts a balance into the accumulator, merging stake values if the balance already exists.
2379
+ // Eg: Acc X has root staked with validator Y, but also staked on sn 45 with the same validator Y.
2380
+ // We merge the pending root claim of sn 45 and the sn 45 stake in the same balance.
2381
+ const upsertBalance = (acc, address, tokenId, balance) => {
2382
+ const key = `${address}:${tokenId}`;
2383
+ const recordedBalance = acc[key];
2384
+ if (recordedBalance) {
2385
+ acc[key] = {
2386
+ ...recordedBalance,
2387
+ stake: recordedBalance.stake + balance.stake,
2388
+ // If the new balance has pendingRootClaim, use it (it's calculated from current state)
2389
+ ...(balance.pendingRootClaim !== undefined && {
2390
+ pendingRootClaim: balance.pendingRootClaim
2391
+ })
2392
+ };
2393
+ } else {
2394
+ acc[key] = balance;
2395
+ }
2396
+ };
2397
+ const balancesRaw = stakeInfos.reduce((acc, [address, stakes]) => {
2398
+ for (const stake of stakes) {
2399
+ // Regular stake cases
2400
+ const dynamicInfo = dynamicInfoByNetuid[stake.netuid];
2401
+ const scaledAlphaPrice = dynamicInfo ? getScaledAlphaPrice(dynamicInfo.alpha_in, dynamicInfo.tao_in) : 0n;
2402
+ const balance = {
2403
+ address,
2404
+ tokenId: subDTaoTokenId(networkId, stake.netuid, stake.hotkey),
2405
+ baseTokenId: subDTaoTokenId(networkId, stake.netuid),
2406
+ stake: stake.stake,
2407
+ hotkey: stake.hotkey,
2408
+ netuid: stake.netuid,
2409
+ scaledAlphaPrice
2410
+ };
2411
+ upsertBalance(acc, address, balance.tokenId, balance);
2412
+
2413
+ // Root stake cases, we need to calculate the pending root claim and add to the balances
2414
+ if (stake.netuid === ROOT_NETUID) {
2415
+ const pendingRootClaimBalances = calculatePendingRootClaimable({
2416
+ stake: stake.stake,
2417
+ hotkey: stake.hotkey,
2418
+ address,
2419
+ networkId,
2420
+ validatorRootClaimableRate: rootClaimableRatesByHotkey.get(stake.hotkey) ?? new Map(),
2421
+ dynamicInfoByNetuid
2422
+ });
2423
+ pendingRootClaimBalances.forEach(balance => {
2424
+ upsertBalance(acc, address, balance.tokenId, balance);
2425
+ });
2426
+ }
2427
+ }
2428
+ return acc;
2429
+ }, {});
2430
+ const balances = Object.values(balancesRaw);
2352
2431
  const tokensById = keyBy(tokensWithAddresses.map(([token]) => token), t => t.id);
2353
2432
  const dynamicTokens = [];
2354
2433
 
@@ -2374,19 +2453,45 @@ const fetchBalances$5 = async ({
2374
2453
  const meta = {
2375
2454
  scaledAlphaPrice: stake?.scaledAlphaPrice.toString() ?? "0"
2376
2455
  };
2456
+ const stakeAmount = BigInt(stake?.stake?.toString() ?? "0");
2457
+ const pendingRootClaimAmount = BigInt(stake?.pendingRootClaim?.toString() ?? "0");
2458
+ const hasZeroStake = stakeAmount === 0n;
2459
+ const hasPendingRootClaim = pendingRootClaimAmount > 0n;
2377
2460
  const balanceValue = {
2378
2461
  type: "free",
2379
2462
  label: stake?.netuid === 0 ? "Root Staking" : `Subnet Staking`,
2380
- amount: stake?.stake.toString() ?? "0",
2463
+ amount: stakeAmount.toString(),
2381
2464
  meta
2382
2465
  };
2466
+ const pendingRootClaimValue = {
2467
+ type: "locked",
2468
+ label: "Pending root claim",
2469
+ amount: pendingRootClaimAmount.toString(),
2470
+ meta
2471
+ };
2472
+ const values = [balanceValue, pendingRootClaimValue];
2473
+
2474
+ // If stake is 0n but there's a pendingRootClaim, add it as an extra amount
2475
+ // with includeInTotal: true so it counts toward the total balance.
2476
+ // This ensures the balance isn't filtered out when stake is 0n.
2477
+ // The total.planck calculation is: free + reserved + extra (with includeInTotal: true)
2478
+ // So by adding pendingRootClaim as extra, it will be included in total.planck.
2479
+ if (hasZeroStake && hasPendingRootClaim) {
2480
+ values.push({
2481
+ type: "extra",
2482
+ label: "Pending root claim",
2483
+ amount: pendingRootClaimAmount.toString(),
2484
+ includeInTotal: true,
2485
+ meta
2486
+ });
2487
+ }
2383
2488
  return {
2384
2489
  address: def.address,
2385
2490
  networkId,
2386
2491
  tokenId: def.token.id,
2387
2492
  source: MODULE_TYPE$5,
2388
2493
  status: "live",
2389
- values: [balanceValue]
2494
+ values
2390
2495
  };
2391
2496
  });
2392
2497
  return {
@@ -2409,6 +2514,68 @@ const fetchBalances$5 = async ({
2409
2514
  };
2410
2515
  }
2411
2516
  };
2517
+ const buildStorageCoder = (metadataRpc, pallet, entry) => {
2518
+ const {
2519
+ builder
2520
+ } = parseMetadataRpc(metadataRpc);
2521
+ return builder.buildStorage(pallet, entry);
2522
+ };
2523
+ const buildRootClaimableStorageCoder = async (connector, networkId, metadataRpc) => {
2524
+ let storageCoder = null;
2525
+ if (metadataRpc) {
2526
+ try {
2527
+ storageCoder = buildStorageCoder(metadataRpc, "SubtensorModule", "RootClaimable");
2528
+ } catch (cause) {
2529
+ log.warn(`Failed to build storage coder for SubtensorModule.RootClaimable using provided metadata on ${networkId}`, {
2530
+ cause
2531
+ });
2532
+ }
2533
+ }
2534
+ return storageCoder;
2535
+ };
2536
+ const buildRootClaimableQueries = (networkId, hotkeys, storageCoder) => {
2537
+ return hotkeys.map(hotkey => {
2538
+ let stateKey = null;
2539
+ try {
2540
+ stateKey = storageCoder.keys.enc(hotkey);
2541
+ } catch (cause) {
2542
+ log.warn(`Failed to encode storage key for hotkey ${hotkey} on ${networkId}`, {
2543
+ cause
2544
+ });
2545
+ }
2546
+ const decodeResult = changes => {
2547
+ const hexValue = changes[0];
2548
+ if (!hexValue) {
2549
+ return [hotkey, new Map()];
2550
+ }
2551
+ const decoded = decodeScale(storageCoder, hexValue, `Failed to decode RootClaimable for hotkey ${hotkey} on ${networkId}`);
2552
+ return [hotkey, decoded ? new Map(decoded) : new Map()];
2553
+ };
2554
+ return {
2555
+ stateKeys: [stateKey],
2556
+ decodeResult
2557
+ };
2558
+ });
2559
+ };
2560
+ const fetchRootClaimableRates = async (connector, networkId, metadataRpc, hotkeys) => {
2561
+ if (!hotkeys.length) return new Map();
2562
+ const storageCoder = await buildRootClaimableStorageCoder(connector, networkId, metadataRpc);
2563
+ if (!storageCoder) {
2564
+ // Fallback: return empty map for all hotkeys
2565
+ return new Map(hotkeys.map(hotkey => [hotkey, new Map()]));
2566
+ }
2567
+ const queries = buildRootClaimableQueries(networkId, hotkeys, storageCoder);
2568
+ try {
2569
+ const results = await fetchRpcQueryPack(connector, networkId, queries);
2570
+ return new Map(results);
2571
+ } catch (cause) {
2572
+ log.warn(`Failed to fetch RootClaimable for hotkeys on ${networkId}`, {
2573
+ cause
2574
+ });
2575
+ // Fallback: return empty map for all hotkeys
2576
+ return new Map(hotkeys.map(hotkey => [hotkey, new Map()]));
2577
+ }
2578
+ };
2412
2579
 
2413
2580
  // hardcoded because we dont have access to native tokens from the balance module
2414
2581
  const NATIVE_TOKEN_SYMBOLS = {
@@ -2508,7 +2675,7 @@ const getData$4 = metadataRpc => {
2508
2675
  if (!isBittensor) return null;
2509
2676
  compactMetadata(metadata, [{
2510
2677
  pallet: "SubtensorModule",
2511
- items: ["TransferToggle"]
2678
+ items: ["TransferToggle", "RootClaimable"]
2512
2679
  }], [{
2513
2680
  runtimeApi: "StakeInfoRuntimeApi",
2514
2681
  methods: ["get_stake_info_for_coldkeys"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@talismn/balances",
3
- "version": "0.0.0-pr2277-20251211070508",
3
+ "version": "0.0.0-pr2279-20251222112424",
4
4
  "author": "Talisman",
5
5
  "homepage": "https://talisman.xyz",
6
6
  "license": "GPL-3.0-or-later",
@@ -38,14 +38,14 @@
38
38
  "scale-ts": "^1.6.1",
39
39
  "viem": "^2.27.3",
40
40
  "zod": "^3.25.76",
41
- "@talismn/chaindata-provider": "1.3.1",
42
- "@talismn/chain-connectors": "0.0.10",
43
- "@talismn/sapi": "0.0.0-pr2277-20251211070508",
44
- "@talismn/crypto": "0.0.0-pr2277-20251211070508",
45
- "@talismn/token-rates": "3.0.12",
46
- "@talismn/scale": "0.0.0-pr2277-20251211070508",
47
- "@talismn/util": "0.5.6",
48
- "@talismn/solana": "0.0.0-pr2277-20251211070508"
41
+ "@talismn/chain-connectors": "0.0.0-pr2279-20251222112424",
42
+ "@talismn/chaindata-provider": "0.0.0-pr2279-20251222112424",
43
+ "@talismn/sapi": "0.1.0",
44
+ "@talismn/crypto": "0.3.0",
45
+ "@talismn/scale": "0.3.0",
46
+ "@talismn/solana": "0.0.5",
47
+ "@talismn/token-rates": "0.0.0-pr2279-20251222112424",
48
+ "@talismn/util": "0.5.6"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@polkadot/api-contract": "16.1.2",
@@ -60,8 +60,8 @@
60
60
  "jest": "^29.7.0",
61
61
  "ts-jest": "^29.2.5",
62
62
  "typescript": "^5.6.3",
63
- "@talismn/tsconfig": "0.0.3",
64
- "@talismn/eslint-config": "0.0.3"
63
+ "@talismn/eslint-config": "0.0.3",
64
+ "@talismn/tsconfig": "0.0.3"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "@polkadot/api-contract": "*",