@talismn/balances 3.0.0 → 3.0.1

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.
package/dist/index.js CHANGED
@@ -1778,6 +1778,21 @@ const buildNetworkStorageCoders = (chainId, miniMetadata, coders) => {
1778
1778
  }));
1779
1779
  };
1780
1780
  //#endregion
1781
+ //#region src/modules/shared/fetchBestBlockHash.ts
1782
+ /**
1783
+ * Hash of the current best block, to pin a poll's reads to.
1784
+ *
1785
+ * Chain state moves between the round-trips of a multi-call poll, and unpinned reads each
1786
+ * run against whatever block is best when they land. Combining values read at different
1787
+ * blocks fabricates data (eg a dtao basket claim total read one block after its
1788
+ * per-validator breakdown leaves an unattributed remainder).
1789
+ */
1790
+ const fetchBestBlockHash = async (connector, networkId) => {
1791
+ const blockHash = await connector.send(networkId, "chain_getBlockHash", []);
1792
+ if (!blockHash) throw new Error(`Failed to fetch best block hash on ${networkId}`);
1793
+ return blockHash;
1794
+ };
1795
+ //#endregion
1781
1796
  //#region src/modules/shared/parseMetadataRpcCached.ts
1782
1797
  /**
1783
1798
  * parseMetadataRpc does a full metadata decode + dynamic-builder build (tens to hundreds
@@ -1825,11 +1840,15 @@ const getRuntimeCall = (builder, apiName, method) => {
1825
1840
  }
1826
1841
  return call;
1827
1842
  };
1828
- const fetchRuntimeCallResult = async (connector, networkId, metadataRpcOrBuilder, apiName, method, args) => {
1843
+ const fetchRuntimeCallResult = async (connector, networkId, metadataRpcOrBuilder, apiName, method, args, at) => {
1829
1844
  try {
1830
1845
  const builder = typeof metadataRpcOrBuilder === "string" ? parseMetadataRpcCached(metadataRpcOrBuilder).builder : metadataRpcOrBuilder;
1831
1846
  const call = getRuntimeCall(builder, apiName, method);
1832
- const hex = await connector.send(networkId, "state_call", [`${apiName}_${method}`, (0, _talismn_scale.toHex)(call.args.enc(args))]);
1847
+ const hex = await connector.send(networkId, "state_call", [
1848
+ `${apiName}_${method}`,
1849
+ (0, _talismn_scale.toHex)(call.args.enc(args)),
1850
+ ...at ? [at] : []
1851
+ ]);
1833
1852
  const start = performance.now();
1834
1853
  const result = call.value.dec(hex);
1835
1854
  (0, _talismn_util.reportJsActivity)(`runtimeCall decode ${networkId} ${apiName}.${method} (~${Math.round((hex?.length ?? 0) / 2048)}KB)`, performance.now() - start);
@@ -1840,10 +1859,10 @@ const fetchRuntimeCallResult = async (connector, networkId, metadataRpcOrBuilder
1840
1859
  };
1841
1860
  //#endregion
1842
1861
  //#region src/modules/shared/rpcQueryPack.ts
1843
- const fetchRpcQueryPack = async (connector, networkId, queries) => {
1862
+ const fetchRpcQueryPack = async (connector, networkId, queries, at) => {
1844
1863
  const allStateKeys = queries.flatMap(({ stateKeys }) => stateKeys).filter(_talismn_util.isNotNil);
1845
1864
  if (!allStateKeys.length) return queries.map(({ stateKeys, decodeResult }) => decodeResult(stateKeys.map(() => null)));
1846
- const [result] = await connector.send(networkId, "state_queryStorageAt", [allStateKeys]);
1865
+ const [result] = await connector.send(networkId, "state_queryStorageAt", at ? [allStateKeys, at] : [allStateKeys]);
1847
1866
  if (!result) throw new Error(`Empty state_queryStorageAt response on ${networkId}`);
1848
1867
  return decodeRpcQueryPackChunked(queries, new Map(result.changes), { label: `rpcQueryPack decode ${networkId}` });
1849
1868
  };
@@ -3927,6 +3946,74 @@ const taoToAlphaCeil = (tao, scaledAlphaPrice) => {
3927
3946
  return (tao * ALPHA_PRICE_SCALE + scaledAlphaPrice - 1n) / scaledAlphaPrice;
3928
3947
  };
3929
3948
  //#endregion
3949
+ //#region src/modules/substrate-dtao/basketClaims.ts
3950
+ /** label of the claimable-rewards values emitted on root staking balances */
3951
+ const CLAIMABLE_REWARDS_LABEL = "Claimable rewards";
3952
+ /** Whether a lock is a claimable root rewards entitlement */
3953
+ const isDTaoClaimableLock = (lock) => lock.label === CLAIMABLE_REWARDS_LABEL;
3954
+ /** Sum of a balance's claimable root rewards (TAO plancks, marked NAV quote) */
3955
+ const getDTaoClaimablePlancks = (locks) => (locks ?? []).filter(isDTaoClaimableLock).reduce((sum, lock) => sum + lock.amount.planck, 0n);
3956
+ /**
3957
+ * The target pair's claimable rewards, null once its entitlement is gone (eg claimed elsewhere).
3958
+ * Sourced from balances rather than staking positions: the chain keeps basket entitlement
3959
+ * after a full unstake, so a claim can have no stake left on its validator.
3960
+ */
3961
+ const findDTaoClaimablePlancks = (balances, { networkId, address, hotkey }) => {
3962
+ const balance = balances.find((b) => b.token?.type === "substrate-dtao" && b.token.networkId === networkId && b.token.netuid === 0 && b.token.hotkey === hotkey && (0, _talismn_crypto.isAddressEqual)(b.address, address));
3963
+ const claimablePlancks = balance ? getDTaoClaimablePlancks(balance.locks) : 0n;
3964
+ return claimablePlancks > 0n ? claimablePlancks : null;
3965
+ };
3966
+ /**
3967
+ * Fetches the TAO each coldkey would realize by redeeming its validator beta baskets
3968
+ * (Bittensor spec 441 "Root Reborn": root dividends accrue in per-validator escrow funds
3969
+ * and must be claimed manually; the payout is TAO staked back onto the root position).
3970
+ *
3971
+ * Amounts are marked NAV quotes (BetaBasketRuntimeApi), so they move with subnet pool
3972
+ * prices as well as accrual. Attribution is per validator hotkey via
3973
+ * `get_root_basket_positions`, which walks the chain's own coldkey→hotkeys index and so
3974
+ * includes validators the coldkey fully unstaked from (entitlement survives unstaking).
3975
+ *
3976
+ * It is the only entitlement read: the coldkey-wide `get_root_basket_owed` total sums the
3977
+ * same positions, and claiming is per validator hotkey, so entitlement outside a position
3978
+ * would be unclaimable anyway.
3979
+ */
3980
+ const fetchBasketClaims = async (connector, networkId, metadataRpc, addresses, at) => {
3981
+ if (!addresses.length) return [];
3982
+ const { unifiedMetadata, builder } = parseMetadataRpcCached(metadataRpc);
3983
+ if (!hasRuntimeApi(unifiedMetadata, "BetaBasketRuntimeApi", "get_root_basket_positions")) return [];
3984
+ try {
3985
+ return (await Promise.all(addresses.map(async (address) => [address, await fetchRuntimeCallResult(connector, networkId, builder, "BetaBasketRuntimeApi", "get_root_basket_positions", [address], at)]))).flatMap(([address, positions]) => positions.filter(([, , payoutTao]) => payoutTao > 0n).map(([hotkey, , payoutTao]) => ({
3986
+ address,
3987
+ hotkey,
3988
+ amount: payoutTao
3989
+ })));
3990
+ } catch (cause) {
3991
+ log_default.warn(`Failed to fetch beta basket claims on ${networkId}`, { cause });
3992
+ throw cause;
3993
+ }
3994
+ };
3995
+ //#endregion
3996
+ //#region src/modules/substrate-dtao/fetchStorageKeysPaged.ts
3997
+ const PAGE_SIZE = 1e3;
3998
+ /**
3999
+ * Prefix scan via state_getKeysPaged. The unpaged state_getKeys is classified as heavy storage
4000
+ * work and rejected by some public nodes (eg the bittensor testnet one: "Storage work rate limit
4001
+ * exceeded"), while the paged variant is universally allowed.
4002
+ */
4003
+ const fetchStorageKeysPaged = async (connector, networkId, keyPrefix, at) => {
4004
+ const keys = [];
4005
+ let startKey;
4006
+ do {
4007
+ const params = [keyPrefix, PAGE_SIZE];
4008
+ if (startKey || at) params.push(startKey ?? null);
4009
+ if (at) params.push(at);
4010
+ const page = await connector.send(networkId, "state_getKeysPaged", params);
4011
+ keys.push(...page);
4012
+ startKey = page.length === PAGE_SIZE ? page[page.length - 1] : void 0;
4013
+ } while (startKey);
4014
+ return keys;
4015
+ };
4016
+ //#endregion
3930
4017
  //#region src/modules/substrate-dtao/convictionLocks.ts
3931
4018
  const convictionLockKey = (address, netuid) => `${address}:${netuid}`;
3932
4019
  const getConvictionLockLabel = (lockType) => lockType === "perpetual" ? "Perpetual Conviction Lock" : "Decaying Conviction Lock";
@@ -3974,17 +4061,17 @@ const toBigIntValue = (value) => {
3974
4061
  * the chain keeps a zero-mass Lock entry alive while it still carries conviction ("ghost" lock),
3975
4062
  * even after the coldkey fully unstaked from the subnet.
3976
4063
  */
3977
- const fetchConvictionLocks = async (connector, networkId, metadataRpc, addresses) => {
4064
+ const fetchConvictionLocks = async (connector, networkId, metadataRpc, addresses, at) => {
3978
4065
  if (!addresses.length) return [];
3979
4066
  const { unifiedMetadata, builder } = parseMetadataRpcCached(metadataRpc);
3980
4067
  if (!hasRuntimeApi(unifiedMetadata, "StakeInfoRuntimeApi", "get_coldkey_lock") || !hasStorageItems(unifiedMetadata, "SubtensorModule", ["Lock", "DecayingLock"])) return [];
3981
4068
  try {
3982
4069
  const lockStorageCoder = builder.buildStorage("SubtensorModule", "Lock");
3983
4070
  const decayingLockStorageCoder = builder.buildStorage("SubtensorModule", "DecayingLock");
3984
- const lockStorageKeys = await fetchConvictionLockStorageKeys(connector, networkId, addresses, lockStorageCoder);
4071
+ const lockStorageKeys = await fetchConvictionLockStorageKeys(connector, networkId, addresses, lockStorageCoder, at);
3985
4072
  if (!lockStorageKeys.length) return [];
3986
4073
  const hotkeyByPair = new Map(lockStorageKeys.map(({ address, netuid, hotkey }) => [convictionLockKey(address, netuid), hotkey]));
3987
- const [lockModesByPair, lockStates] = await Promise.all([fetchConvictionLockModes(connector, networkId, lockStorageKeys, decayingLockStorageCoder), fetchColdkeyLockStates(connector, networkId, builder, lockStorageKeys)]);
4074
+ const [lockModesByPair, lockStates] = await Promise.all([fetchConvictionLockModes(connector, networkId, lockStorageKeys, decayingLockStorageCoder, at), fetchColdkeyLockStates(connector, networkId, builder, lockStorageKeys, at)]);
3988
4075
  return lockStates.flatMap(({ address, netuid, lockState }) => {
3989
4076
  const amount = lockState?.locked_mass ?? 0n;
3990
4077
  const convictionRaw = toBigIntValue(lockState?.conviction);
@@ -4012,7 +4099,7 @@ const fetchConvictionLocks = async (connector, networkId, metadataRpc, addresses
4012
4099
  * Encoding only the first map key (the coldkey) yields the storage key prefix covering all of the
4013
4100
  * coldkey's (netuid, hotkey) entries, using the hashers declared in metadata.
4014
4101
  */
4015
- const fetchConvictionLockStorageKeys = async (connector, networkId, addresses, storageCoder) => {
4102
+ const fetchConvictionLockStorageKeys = async (connector, networkId, addresses, storageCoder, at) => {
4016
4103
  return (await Promise.all(addresses.map(async (address) => {
4017
4104
  let keyPrefix;
4018
4105
  try {
@@ -4023,7 +4110,7 @@ const fetchConvictionLockStorageKeys = async (connector, networkId, addresses, s
4023
4110
  }
4024
4111
  let stateKeys;
4025
4112
  try {
4026
- stateKeys = await connector.send(networkId, "state_getKeys", [keyPrefix]);
4113
+ stateKeys = await fetchStorageKeysPaged(connector, networkId, keyPrefix, at);
4027
4114
  } catch (cause) {
4028
4115
  log_default.warn(`Failed to fetch conviction Lock keys (address=${address}) on ${networkId}`, { cause });
4029
4116
  throw cause;
@@ -4043,7 +4130,7 @@ const fetchConvictionLockStorageKeys = async (connector, networkId, addresses, s
4043
4130
  });
4044
4131
  }))).flat();
4045
4132
  };
4046
- const fetchConvictionLockModes = async (connector, networkId, pairs, storageCoder) => {
4133
+ const fetchConvictionLockModes = async (connector, networkId, pairs, storageCoder, at) => {
4047
4134
  const queries = pairs.map(({ address, netuid }) => {
4048
4135
  let stateKey;
4049
4136
  try {
@@ -4064,15 +4151,15 @@ const fetchConvictionLockModes = async (connector, networkId, pairs, storageCode
4064
4151
  }
4065
4152
  };
4066
4153
  });
4067
- return new Map(await fetchRpcQueryPack(connector, networkId, queries));
4154
+ return new Map(await fetchRpcQueryPack(connector, networkId, queries, at));
4068
4155
  };
4069
- const fetchColdkeyLockStates = async (connector, networkId, builder, pairs) => {
4156
+ const fetchColdkeyLockStates = async (connector, networkId, builder, pairs, at) => {
4070
4157
  return Promise.all(pairs.map(async ({ address, netuid }) => {
4071
4158
  try {
4072
4159
  return {
4073
4160
  address,
4074
4161
  netuid,
4075
- lockState: await fetchRuntimeCallResult(connector, networkId, builder, "StakeInfoRuntimeApi", "get_coldkey_lock", [address, netuid])
4162
+ lockState: await fetchRuntimeCallResult(connector, networkId, builder, "StakeInfoRuntimeApi", "get_coldkey_lock", [address, netuid], at)
4076
4163
  };
4077
4164
  } catch (cause) {
4078
4165
  log_default.warn(`Failed to fetch get_coldkey_lock for (netuid=${netuid}, address=${address}) on ${networkId}`, { cause });
@@ -4085,29 +4172,122 @@ const fetchColdkeyLockStates = async (connector, networkId, builder, pairs) => {
4085
4172
  const MODULE_TYPE$5 = _talismn_chaindata_provider.SubDTaoTokenSchema.shape.type.value;
4086
4173
  const PLATFORM$5 = _talismn_chaindata_provider.SubDTaoTokenSchema.shape.platform.value;
4087
4174
  //#endregion
4088
- //#region src/modules/substrate-dtao/calculatePendingRootClaimable.ts
4089
- const calculatePendingRootClaimable = ({ stake, hotkey, address, networkId, validatorRootClaimableRate, alreadyClaimedByNetuid }) => {
4090
- const pendingRootClaimBalances = [];
4091
- for (const [netuid, claimableRate] of validatorRootClaimableRate) {
4092
- if (claimableRate === 0n) continue;
4093
- const totalClaimable = stake * claimableRate + (1n << 31n) >> 32n;
4094
- const alreadyClaimed = alreadyClaimedByNetuid.get(netuid) ?? 0n;
4095
- const pendingRootClaim = totalClaimable > alreadyClaimed ? totalClaimable - alreadyClaimed : 0n;
4096
- pendingRootClaimBalances.push({
4097
- address,
4098
- tokenId: (0, _talismn_chaindata_provider.subDTaoTokenId)(networkId, netuid, hotkey),
4099
- baseTokenId: (0, _talismn_chaindata_provider.subDTaoTokenId)(networkId, netuid),
4100
- hotkey,
4101
- netuid,
4102
- pendingRootClaim,
4103
- stake: 0n
4175
+ //#region src/modules/substrate-dtao/rootStakeHold.ts
4176
+ /**
4177
+ * Extracts the root-stake hold from a balance's raw values (BalanceJson), if any.
4178
+ * While present, the pair's root stake cannot leave root — unstake/move/swap/transfer
4179
+ * would fail with `RootStakeLocked`. Only present while the window was still running
4180
+ * as of the last balances poll.
4181
+ */
4182
+ const findDTaoRootStakeHold = (balance) => {
4183
+ for (const value of balance?.values ?? []) {
4184
+ const hold = value.meta?.rootStakeHold;
4185
+ if (hold?.type === "root-stake-hold") return hold;
4186
+ }
4187
+ return null;
4188
+ };
4189
+ /**
4190
+ * Fetches active root-stake hold windows (spec 441): when `RootStakeUnlockInterval` is
4191
+ * non-zero, root stake cannot leave root (remove/move/swap/transfer) until `interval`
4192
+ * blocks after the pair's last root stake add/remove/claim (`LastColdkeyHotkeyStakeBlock`).
4193
+ * Pairs already past their window are omitted — a returned hold means the pair's root
4194
+ * stake is currently unremovable.
4195
+ *
4196
+ * The interval is 0 (disabled) unless governance enables it — either by setting the
4197
+ * storage entry or via a runtime default that leaves it unset (metadata fallback). The
4198
+ * per-pair queries only run when it is non-zero.
4199
+ */
4200
+ const fetchRootStakeHolds = async (connector, networkId, metadataRpc, rootPairs, at) => {
4201
+ if (!rootPairs.length) return [];
4202
+ const { unifiedMetadata, builder } = parseMetadataRpcCached(metadataRpc);
4203
+ if (!hasStorageItems(unifiedMetadata, "SubtensorModule", ["RootStakeUnlockInterval", "LastColdkeyHotkeyStakeBlock"])) return [];
4204
+ try {
4205
+ const interval = await fetchUnlockInterval(connector, networkId, unifiedMetadata, builder, at);
4206
+ if (interval === 0n) return [];
4207
+ const { currentBlock, lastStakeBlocks } = await fetchLastStakeBlocks(connector, networkId, builder, rootPairs, at);
4208
+ return lastStakeBlocks.flatMap(({ address, hotkey, lastStakeBlock }) => {
4209
+ if (lastStakeBlock === 0n) return [];
4210
+ const unlockAtBlock = lastStakeBlock + interval;
4211
+ if (unlockAtBlock <= BigInt(currentBlock)) return [];
4212
+ return [{
4213
+ address,
4214
+ hotkey,
4215
+ unlockAtBlock: Number(unlockAtBlock)
4216
+ }];
4104
4217
  });
4218
+ } catch (cause) {
4219
+ log_default.warn(`Failed to fetch root stake holds on ${networkId}`, { cause });
4220
+ throw cause;
4105
4221
  }
4106
- return pendingRootClaimBalances;
4222
+ };
4223
+ const fetchUnlockInterval = async (connector, networkId, unifiedMetadata, builder, at) => {
4224
+ const storageCoder = builder.buildStorage("SubtensorModule", "RootStakeUnlockInterval");
4225
+ const [interval] = await fetchRpcQueryPack(connector, networkId, [{
4226
+ stateKeys: [storageCoder.keys.enc()],
4227
+ decodeResult: (changes) => {
4228
+ const encoded = changes[0] ?? getUnlockIntervalFallback(unifiedMetadata, networkId);
4229
+ const decoded = (0, _talismn_scale.decodeScale)(storageCoder, encoded, `Failed to decode RootStakeUnlockInterval on ${networkId}`);
4230
+ if (decoded === null) throw new Error(`Failed to decode RootStakeUnlockInterval on ${networkId}`);
4231
+ return decoded;
4232
+ }
4233
+ }], at);
4234
+ return interval ?? 0n;
4235
+ };
4236
+ const getUnlockIntervalFallback = (unifiedMetadata, networkId) => {
4237
+ const fallback = unifiedMetadata.pallets.find((pallet) => pallet.name === "SubtensorModule")?.storage?.items.find((item) => item.name === "RootStakeUnlockInterval")?.fallback;
4238
+ if (!fallback) throw new Error(`Missing RootStakeUnlockInterval metadata fallback on ${networkId}`);
4239
+ return fallback;
4240
+ };
4241
+ /**
4242
+ * Reads each pair's `LastColdkeyHotkeyStakeBlock` plus the current block number, all from
4243
+ * one state snapshot: `System.Number` rides along as an extra key of the same
4244
+ * `state_queryStorageAt` instead of costing a separate `chain_getHeader` round trip.
4245
+ */
4246
+ const fetchLastStakeBlocks = async (connector, networkId, builder, rootPairs, at) => {
4247
+ const numberCoder = builder.buildStorage("System", "Number");
4248
+ const currentBlockQuery = {
4249
+ stateKeys: [numberCoder.keys.enc()],
4250
+ decodeResult: (changes) => {
4251
+ const decoded = (0, _talismn_scale.decodeScale)(numberCoder, changes[0], `Failed to decode System.Number on ${networkId}`);
4252
+ if (decoded === null) throw new Error(`Failed to decode System.Number on ${networkId}`);
4253
+ return Number(decoded);
4254
+ }
4255
+ };
4256
+ const storageCoder = builder.buildStorage("SubtensorModule", "LastColdkeyHotkeyStakeBlock");
4257
+ const [currentBlock, ...lastStakeBlocks] = await fetchRpcQueryPack(connector, networkId, [currentBlockQuery, ...rootPairs.map(({ address, hotkey }) => {
4258
+ let stateKey;
4259
+ try {
4260
+ stateKey = storageCoder.keys.enc(address, hotkey);
4261
+ } catch (cause) {
4262
+ log_default.warn(`Failed to encode LastColdkeyHotkeyStakeBlock key (address=${address}, hotkey=${hotkey}) on ${networkId}`, { cause });
4263
+ throw cause;
4264
+ }
4265
+ return {
4266
+ stateKeys: [stateKey],
4267
+ decodeResult: (changes) => {
4268
+ const hexValue = changes[0];
4269
+ if (!hexValue) return {
4270
+ address,
4271
+ hotkey,
4272
+ lastStakeBlock: 0n
4273
+ };
4274
+ const decoded = (0, _talismn_scale.decodeScale)(storageCoder, hexValue, `Failed to decode LastColdkeyHotkeyStakeBlock for (address=${address}, hotkey=${hotkey}) on ${networkId}`);
4275
+ if (decoded === null) throw new Error(`Failed to decode LastColdkeyHotkeyStakeBlock for (address=${address}, hotkey=${hotkey}) on ${networkId}`);
4276
+ return {
4277
+ address,
4278
+ hotkey,
4279
+ lastStakeBlock: decoded
4280
+ };
4281
+ }
4282
+ };
4283
+ })], at);
4284
+ return {
4285
+ currentBlock,
4286
+ lastStakeBlocks
4287
+ };
4107
4288
  };
4108
4289
  //#endregion
4109
4290
  //#region src/modules/substrate-dtao/fetchBalances.ts
4110
- const ROOT_NETUID = 0;
4111
4291
  const fetchBalances$5 = async ({ networkId, tokensWithAddresses, connector, miniMetadata, signal }) => {
4112
4292
  if (!tokensWithAddresses.length) return {
4113
4293
  success: [],
@@ -4150,29 +4330,36 @@ const fetchBalances$5 = async ({ networkId, tokensWithAddresses, connector, mini
4150
4330
  const addresses = (0, lodash_es.uniq)(balanceDefs.map((def) => def.address));
4151
4331
  try {
4152
4332
  const { builder } = parseMetadataRpcCached(miniMetadata.data);
4153
- const stakeInfos = await fetchRuntimeCallResult(connector, networkId, builder, "StakeInfoRuntimeApi", "get_stake_info_for_coldkeys", [addresses]);
4154
- const rootHotkeys = (0, lodash_es.uniq)(stakeInfos.flatMap(([, stakes]) => stakes.filter((stake) => stake.netuid === ROOT_NETUID).map((stake) => stake.hotkey)));
4155
- const rootClaimableRatesByHotkey = rootHotkeys.length && miniMetadata.data ? await fetchRootClaimableRates(connector, networkId, miniMetadata.data, rootHotkeys) : /* @__PURE__ */ new Map();
4156
- const addressHotkeyNetuidPairs = [];
4157
- for (const [address, stakes] of stakeInfos) for (const stake of stakes) if (stake.netuid === ROOT_NETUID) {
4158
- const claimableRates = rootClaimableRatesByHotkey.get(stake.hotkey);
4159
- if (claimableRates) for (const netuid of claimableRates.keys()) addressHotkeyNetuidPairs.push([
4333
+ const at = await fetchBestBlockHash(connector, networkId);
4334
+ const stakeInfos = await fetchRuntimeCallResult(connector, networkId, builder, "StakeInfoRuntimeApi", "get_stake_info_for_coldkeys", [addresses], at);
4335
+ const rootPairs = [];
4336
+ const seenRootPairs = /* @__PURE__ */ new Set();
4337
+ for (const [address, stakes] of stakeInfos) for (const stake of stakes) {
4338
+ if (stake.netuid !== 0) continue;
4339
+ const pairKey = `${address}:${stake.hotkey}`;
4340
+ if (seenRootPairs.has(pairKey)) continue;
4341
+ seenRootPairs.add(pairKey);
4342
+ rootPairs.push({
4160
4343
  address,
4161
- stake.hotkey,
4162
- netuid
4163
- ]);
4344
+ hotkey: stake.hotkey
4345
+ });
4164
4346
  }
4165
- const [rootClaimedAmounts, convictionLocks] = await Promise.all([addressHotkeyNetuidPairs.length && miniMetadata.data ? fetchRootClaimedAmounts(connector, networkId, miniMetadata.data, addressHotkeyNetuidPairs) : Promise.resolve(/* @__PURE__ */ new Map()), miniMetadata.data ? fetchConvictionLocks(connector, networkId, miniMetadata.data, addresses) : Promise.resolve([])]);
4347
+ const [convictionLocks, basketClaims, rootStakeHolds] = miniMetadata.data ? await Promise.all([
4348
+ fetchConvictionLocks(connector, networkId, miniMetadata.data, addresses, at),
4349
+ fetchBasketClaims(connector, networkId, miniMetadata.data, addresses, at),
4350
+ fetchRootStakeHolds(connector, networkId, miniMetadata.data, rootPairs, at)
4351
+ ]) : [
4352
+ [],
4353
+ [],
4354
+ []
4355
+ ];
4166
4356
  const upsertBalance = (acc, address, tokenId, balance) => {
4167
4357
  const key = `${address}:${tokenId}`;
4168
4358
  const recordedBalance = acc[key];
4169
- if (recordedBalance) acc[key] = {
4170
- ...recordedBalance,
4171
- stake: recordedBalance.stake + balance.stake,
4172
- ...balance.pendingRootClaim !== void 0 && { pendingRootClaim: balance.pendingRootClaim },
4173
- ...balance.convictionLock !== void 0 && { convictionLock: balance.convictionLock }
4174
- };
4175
- else acc[key] = balance;
4359
+ acc[key] = recordedBalance ? (0, lodash_es.mergeWith)({}, recordedBalance, balance, (recorded, incoming, field) => {
4360
+ if (field === "stake" && recorded !== void 0) return recorded + incoming;
4361
+ return incoming;
4362
+ }) : balance;
4176
4363
  };
4177
4364
  const slicer = (0, _talismn_util.createTimeSlicer)({
4178
4365
  signal,
@@ -4189,21 +4376,21 @@ const fetchBalances$5 = async ({ networkId, tokensWithAddresses, connector, mini
4189
4376
  netuid: stake.netuid
4190
4377
  };
4191
4378
  upsertBalance(balancesRaw, address, balance.tokenId, balance);
4192
- if (stake.netuid === ROOT_NETUID) {
4193
- const claimableRates = rootClaimableRatesByHotkey.get(stake.hotkey) ?? /* @__PURE__ */ new Map();
4194
- const alreadyClaimedMap = rootClaimedAmounts.get(address)?.get(stake.hotkey) ?? /* @__PURE__ */ new Map();
4195
- calculatePendingRootClaimable({
4196
- stake: stake.stake,
4197
- hotkey: stake.hotkey,
4198
- address,
4199
- networkId,
4200
- validatorRootClaimableRate: claimableRates,
4201
- alreadyClaimedByNetuid: alreadyClaimedMap
4202
- }).forEach((balance) => {
4203
- upsertBalance(balancesRaw, address, balance.tokenId, balance);
4204
- });
4205
- }
4206
4379
  }, { slicer });
4380
+ const upsertRootPairFields = (items, getFields) => (0, _talismn_util.forEachWithYield)(items, (item) => {
4381
+ const tokenId = (0, _talismn_chaindata_provider.subDTaoTokenId)(networkId, 0, item.hotkey);
4382
+ upsertBalance(balancesRaw, item.address, tokenId, {
4383
+ address: item.address,
4384
+ tokenId,
4385
+ baseTokenId: (0, _talismn_chaindata_provider.subDTaoTokenId)(networkId, 0),
4386
+ stake: 0n,
4387
+ hotkey: item.hotkey,
4388
+ netuid: 0,
4389
+ ...getFields(item)
4390
+ });
4391
+ }, { slicer });
4392
+ await upsertRootPairFields(basketClaims, ({ amount }) => ({ claimable: amount }));
4393
+ await upsertRootPairFields(rootStakeHolds, ({ unlockAtBlock }) => ({ rootStakeHoldUnlockBlock: unlockAtBlock }));
4207
4394
  await (0, _talismn_util.forEachWithYield)(convictionLocks, ({ address, netuid, lock }) => {
4208
4395
  const balance = {
4209
4396
  address,
@@ -4240,21 +4427,35 @@ const fetchBalances$5 = async ({ networkId, tokensWithAddresses, connector, mini
4240
4427
  const stake = balancesRaw[`${def.address}:${def.token.id}`];
4241
4428
  if (!stake) return null;
4242
4429
  const stakeAmount = BigInt(stake.stake?.toString() ?? "0");
4243
- const pendingRootClaimAmount = BigInt(stake.pendingRootClaim?.toString() ?? "0");
4430
+ const claimableAmount = BigInt(stake.claimable?.toString() ?? "0");
4244
4431
  const convictionLockAmount = BigInt(stake.convictionLock?.amount?.toString() ?? "0");
4245
4432
  const convictionLockConviction = BigInt(stake.convictionLock?.convictionRaw ?? "0");
4246
- const hasZeroStake = stakeAmount === 0n;
4247
- const hasPendingRootClaim = pendingRootClaimAmount > 0n;
4433
+ const rootStakeHoldMeta = stake.rootStakeHoldUnlockBlock !== void 0 ? { rootStakeHold: {
4434
+ type: "root-stake-hold",
4435
+ unlockAtBlock: stake.rootStakeHoldUnlockBlock
4436
+ } } : void 0;
4248
4437
  const values = [{
4249
4438
  type: "free",
4250
4439
  label: stake.netuid === 0 ? "Root Staking" : `Subnet Staking`,
4251
- amount: stakeAmount.toString()
4252
- }, {
4253
- type: "locked",
4254
- label: "Pending root claim",
4255
- amount: pendingRootClaimAmount.toString(),
4256
- includeInTransferable: true
4440
+ amount: stakeAmount.toString(),
4441
+ ...rootStakeHoldMeta && { meta: rootStakeHoldMeta }
4257
4442
  }];
4443
+ if (claimableAmount > 0n) {
4444
+ const claimable = {
4445
+ label: CLAIMABLE_REWARDS_LABEL,
4446
+ amount: claimableAmount.toString()
4447
+ };
4448
+ values.push({
4449
+ ...claimable,
4450
+ type: "locked",
4451
+ includeInTransferable: true
4452
+ });
4453
+ values.push({
4454
+ ...claimable,
4455
+ type: "extra",
4456
+ includeInTotal: true
4457
+ });
4458
+ }
4258
4459
  if (stake.convictionLock && (convictionLockAmount > 0n || convictionLockConviction > 0n)) {
4259
4460
  const convictionLockMeta = { convictionLock: {
4260
4461
  type: "conviction-lock",
@@ -4268,12 +4469,6 @@ const fetchBalances$5 = async ({ networkId, tokensWithAddresses, connector, mini
4268
4469
  meta: convictionLockMeta
4269
4470
  });
4270
4471
  }
4271
- if (hasZeroStake && hasPendingRootClaim) values.push({
4272
- type: "extra",
4273
- label: "Pending root claim",
4274
- amount: pendingRootClaimAmount.toString(),
4275
- includeInTotal: true
4276
- });
4277
4472
  return {
4278
4473
  address: def.address,
4279
4474
  networkId,
@@ -4299,124 +4494,6 @@ const fetchBalances$5 = async ({ networkId, tokensWithAddresses, connector, mini
4299
4494
  };
4300
4495
  }
4301
4496
  };
4302
- const buildStorageCoder = (metadataRpc, pallet, entry) => {
4303
- const { builder } = parseMetadataRpcCached(metadataRpc);
4304
- return builder.buildStorage(pallet, entry);
4305
- };
4306
- const buildRootClaimableStorageCoder = async (_connector, networkId, metadataRpc) => {
4307
- let storageCoder = null;
4308
- if (metadataRpc) try {
4309
- storageCoder = buildStorageCoder(metadataRpc, "SubtensorModule", "RootClaimable");
4310
- } catch (cause) {
4311
- log_default.warn(`Failed to build storage coder for SubtensorModule.RootClaimable using provided metadata on ${networkId}`, { cause });
4312
- }
4313
- return storageCoder;
4314
- };
4315
- const buildRootClaimedStorageCoder = async (networkId, metadataRpc) => {
4316
- let storageCoder = null;
4317
- if (metadataRpc) try {
4318
- storageCoder = buildStorageCoder(metadataRpc, "SubtensorModule", "RootClaimed");
4319
- } catch (cause) {
4320
- log_default.warn(`Failed to build storage coder for SubtensorModule.RootClaimed using provided metadata on ${networkId}`, { cause });
4321
- }
4322
- return storageCoder;
4323
- };
4324
- const buildRootClaimableQueries = (networkId, hotkeys, storageCoder) => {
4325
- return hotkeys.map((hotkey) => {
4326
- let stateKey;
4327
- try {
4328
- stateKey = storageCoder.keys.enc(hotkey);
4329
- } catch (cause) {
4330
- log_default.warn(`Failed to encode storage key for hotkey ${hotkey} on ${networkId}`, { cause });
4331
- throw cause;
4332
- }
4333
- const decodeResult = (changes) => {
4334
- const hexValue = changes[0];
4335
- if (!hexValue) return [hotkey, /* @__PURE__ */ new Map()];
4336
- const decoded = (0, _talismn_scale.decodeScale)(storageCoder, hexValue, `Failed to decode RootClaimable for hotkey ${hotkey} on ${networkId}`);
4337
- if (decoded === null) throw new Error(`Failed to decode RootClaimable for hotkey ${hotkey} on ${networkId}`);
4338
- return [hotkey, new Map(decoded)];
4339
- };
4340
- return {
4341
- stateKeys: [stateKey],
4342
- decodeResult
4343
- };
4344
- });
4345
- };
4346
- const fetchRootClaimableRates = async (connector, networkId, metadataRpc, hotkeys) => {
4347
- if (!hotkeys.length) return /* @__PURE__ */ new Map();
4348
- const storageCoder = await buildRootClaimableStorageCoder(connector, networkId, metadataRpc);
4349
- if (!storageCoder) return new Map(hotkeys.map((hotkey) => [hotkey, /* @__PURE__ */ new Map()]));
4350
- const queries = buildRootClaimableQueries(networkId, hotkeys, storageCoder);
4351
- try {
4352
- const results = await fetchRpcQueryPack(connector, networkId, queries);
4353
- return new Map(results);
4354
- } catch (cause) {
4355
- log_default.warn(`Failed to fetch RootClaimable for hotkeys on ${networkId}`, { cause });
4356
- throw cause;
4357
- }
4358
- };
4359
- const buildRootClaimedQueries = (networkId, addressHotkeyNetuidPairs, storageCoder) => {
4360
- return addressHotkeyNetuidPairs.map(([address, hotkey, netuid]) => {
4361
- let stateKey;
4362
- try {
4363
- stateKey = storageCoder.keys.enc(netuid, hotkey, address);
4364
- } catch (cause) {
4365
- log_default.warn(`Failed to encode storage key for RootClaimed (netuid=${netuid}, hotkey=${hotkey}, address=${address}) on ${networkId}`, { cause });
4366
- throw cause;
4367
- }
4368
- const decodeResult = (changes) => {
4369
- const hexValue = changes[0];
4370
- if (!hexValue) return [
4371
- address,
4372
- hotkey,
4373
- netuid,
4374
- 0n
4375
- ];
4376
- const decoded = (0, _talismn_scale.decodeScale)(storageCoder, hexValue, `Failed to decode RootClaimed for (netuid=${netuid}, hotkey=${hotkey}, address=${address}) on ${networkId}`);
4377
- if (decoded === null) throw new Error(`Failed to decode RootClaimed for (netuid=${netuid}, hotkey=${hotkey}, address=${address}) on ${networkId}`);
4378
- return [
4379
- address,
4380
- hotkey,
4381
- netuid,
4382
- decoded
4383
- ];
4384
- };
4385
- return {
4386
- stateKeys: [stateKey],
4387
- decodeResult
4388
- };
4389
- });
4390
- };
4391
- const fetchRootClaimedAmounts = async (connector, networkId, metadataRpc, addressHotkeyNetuidPairs) => {
4392
- if (!addressHotkeyNetuidPairs.length) return /* @__PURE__ */ new Map();
4393
- const storageCoder = await buildRootClaimedStorageCoder(networkId, metadataRpc);
4394
- if (!storageCoder) {
4395
- const result = /* @__PURE__ */ new Map();
4396
- for (const [address, hotkey, netuid] of addressHotkeyNetuidPairs) {
4397
- if (!result.has(address)) result.set(address, /* @__PURE__ */ new Map());
4398
- const addressMap = result.get(address);
4399
- if (!addressMap.has(hotkey)) addressMap.set(hotkey, /* @__PURE__ */ new Map());
4400
- addressMap.get(hotkey).set(netuid, 0n);
4401
- }
4402
- return result;
4403
- }
4404
- const queries = buildRootClaimedQueries(networkId, addressHotkeyNetuidPairs, storageCoder);
4405
- try {
4406
- const results = await fetchRpcQueryPack(connector, networkId, queries);
4407
- const result = /* @__PURE__ */ new Map();
4408
- for (const [address, hotkey, netuid, claimed] of results) {
4409
- if (!result.has(address)) result.set(address, /* @__PURE__ */ new Map());
4410
- const addressMap = result.get(address);
4411
- if (!addressMap.has(hotkey)) addressMap.set(hotkey, /* @__PURE__ */ new Map());
4412
- addressMap.get(hotkey).set(netuid, claimed);
4413
- }
4414
- return result;
4415
- } catch (cause) {
4416
- log_default.warn(`Failed to fetch RootClaimed for address-hotkey-netuid pairs on ${networkId}`, { cause });
4417
- throw cause;
4418
- }
4419
- };
4420
4497
  //#endregion
4421
4498
  //#region src/modules/substrate-dtao/fetchTokens.ts
4422
4499
  const NATIVE_TOKEN_SYMBOLS = {
@@ -4455,7 +4532,7 @@ const fetchTokens$5 = async ({ networkId, connector, tokens, miniMetadata }) =>
4455
4532
  const fetchTransferableTokensMap = async (connector, metadata, networkId) => {
4456
4533
  const { builder } = parseMetadataRpcCached(metadata);
4457
4534
  const transferToggleCodec = builder.buildStorage("SubtensorModule", "TransferToggle");
4458
- const transferToggleKeys = await connector.send(networkId, "state_getKeys", [(0, _talismn_scale.getStorageKeyPrefix)("SubtensorModule", "TransferToggle")]);
4535
+ const transferToggleKeys = await fetchStorageKeysPaged(connector, networkId, (0, _talismn_scale.getStorageKeyPrefix)("SubtensorModule", "TransferToggle"));
4459
4536
  const transferToggleResults = await connector.send(networkId, "state_queryStorageAt", [transferToggleKeys]);
4460
4537
  return (0, lodash_es.fromPairs)((transferToggleResults.length ? transferToggleResults[0].changes : []).map(([key, value]) => {
4461
4538
  const [netuid] = transferToggleCodec.keys.dec(key);
@@ -4493,18 +4570,28 @@ const getData$3 = (metadataRpc) => {
4493
4570
  pallet: "SubtensorModule",
4494
4571
  items: [
4495
4572
  "TransferToggle",
4496
- "RootClaimable",
4497
- "RootClaimed",
4498
4573
  "Lock",
4499
- "DecayingLock"
4574
+ "DecayingLock",
4575
+ "RootStakeUnlockInterval",
4576
+ "LastColdkeyHotkeyStakeBlock"
4500
4577
  ]
4501
- }], [{
4502
- runtimeApi: "StakeInfoRuntimeApi",
4503
- methods: ["get_stake_info_for_coldkeys", "get_coldkey_lock"]
4504
4578
  }, {
4505
- runtimeApi: "SubnetInfoRuntimeApi",
4506
- methods: ["get_all_dynamic_info"]
4507
- }]);
4579
+ pallet: "System",
4580
+ items: ["Number"]
4581
+ }], [
4582
+ {
4583
+ runtimeApi: "StakeInfoRuntimeApi",
4584
+ methods: ["get_stake_info_for_coldkeys", "get_coldkey_lock"]
4585
+ },
4586
+ {
4587
+ runtimeApi: "SubnetInfoRuntimeApi",
4588
+ methods: ["get_all_dynamic_info"]
4589
+ },
4590
+ {
4591
+ runtimeApi: "BetaBasketRuntimeApi",
4592
+ methods: ["get_root_basket_positions"]
4593
+ }
4594
+ ]);
4508
4595
  return (0, _talismn_scale.encodeMetadata)(metadata);
4509
4596
  };
4510
4597
  //#endregion
@@ -4530,7 +4617,8 @@ const getTransferCallData$5 = ({ from, to, value, token, metadataRpc }) => {
4530
4617
  };
4531
4618
  //#endregion
4532
4619
  //#region src/modules/substrate-dtao/isEffectivelyEqualDTaoBalance.ts
4533
- /** root dividends accrue continuously; the pending-claim display is informational */
4620
+ /** basket claimable amounts are marked NAV quotes: they move with subnet pool prices and
4621
+ * dividend accrual on (nearly) every block; the display is informational */
4534
4622
  const CLAIM_DRIFT_TOLERANCE_BPS = 100n;
4535
4623
  /**
4536
4624
  * subnet staking positions auto-compound: dividend injections land once per subnet tempo
@@ -4541,8 +4629,8 @@ const CLAIM_DRIFT_TOLERANCE_BPS = 100n;
4541
4629
  const STAKE_DRIFT_TOLERANCE_BPS = 100n;
4542
4630
  /**
4543
4631
  * dtao balances embed values that drift on (nearly) every block even when the user's
4544
- * position is untouched: "Pending root claim" amounts accrue continuously, staking
4545
- * positions auto-compound, and conviction locks decay.
4632
+ * position is untouched: staking positions auto-compound, basket claimable quotes move,
4633
+ * and conviction locks decay.
4546
4634
  *
4547
4635
  * Without special handling, every 6s poll re-emits the full result set, which defeats
4548
4636
  * every distinctUntilChanged stage downstream and forces the whole pipeline
@@ -4554,9 +4642,7 @@ const STAKE_DRIFT_TOLERANCE_BPS = 100n;
4554
4642
  * drift-prone values above (relative to the previously EMITTED value, so movement
4555
4643
  * accumulates and a sustained move still surfaces)
4556
4644
  * - "drift" when ONLY the drift-prone values moved, beyond tolerance. The stabilizer
4557
- * re-emits these at most once per refresh interval — important for fast-accruing
4558
- * values (a young pending claim can grow >1% per poll indefinitely, so a purely
4559
- * relative tolerance can never suppress it)
4645
+ * re-emits these at most once per refresh interval
4560
4646
  * - "changed" for structural changes (stake, locks, status, value set) — emitted
4561
4647
  * immediately
4562
4648
  */
@@ -4575,14 +4661,13 @@ const isWithinDriftTolerance = (previous, next, toleranceBps) => {
4575
4661
  const max = a > b ? a : b;
4576
4662
  return diff * 10000n <= max * toleranceBps;
4577
4663
  };
4578
- /** the only labels whose amounts accrue per block (see fetchBalances) */
4579
- const PENDING_ROOT_CLAIM_LABEL = "Pending root claim";
4580
4664
  const changed = (reason) => ({
4581
4665
  equivalence: "changed",
4582
4666
  reason
4583
4667
  });
4584
- /** claims accrue and locks decay — their values may appear/disappear as amounts cross zero */
4585
- const isDriftProneValue = (value) => value.label === PENDING_ROOT_CLAIM_LABEL || !!value.meta?.convictionLock;
4668
+ /** claimable quotes and decaying locks may appear as their amounts cross zero */
4669
+ const isDriftProneValue = (value) => value.label === "Claimable rewards" || hasConvictionLock(value);
4670
+ const hasConvictionLock = (value) => !!value.meta?.convictionLock;
4586
4671
  const classifyValue = (previous, next) => {
4587
4672
  if (("includeInTransferable" in previous ? previous.includeInTransferable : void 0) !== ("includeInTransferable" in next ? next.includeInTransferable : void 0) || ("includeInTotal" in previous ? previous.includeInTotal : void 0) !== ("includeInTotal" in next ? next.includeInTotal : void 0)) return changed("variant flags");
4588
4673
  const previousMeta = previous.meta;
@@ -4590,8 +4675,9 @@ const classifyValue = (previous, next) => {
4590
4675
  const previousLock = previousMeta?.convictionLock;
4591
4676
  const nextLock = nextMeta?.convictionLock;
4592
4677
  if (previousLock?.type !== nextLock?.type || previousLock?.hotkey !== nextLock?.hotkey || previousLock?.lockType !== nextLock?.lockType) return changed("conviction lock meta");
4678
+ if (previousMeta?.rootStakeHold?.unlockAtBlock !== nextMeta?.rootStakeHold?.unlockAtBlock) return changed("root stake hold meta");
4593
4679
  let drift = false;
4594
- if (previous.amount !== next.amount) if (previous.label === PENDING_ROOT_CLAIM_LABEL) {
4680
+ if (previous.amount !== next.amount) if (previous.label === "Claimable rewards") {
4595
4681
  if (!isWithinDriftTolerance(previous.amount, next.amount, CLAIM_DRIFT_TOLERANCE_BPS)) drift = true;
4596
4682
  } else if (previousLock) drift = true;
4597
4683
  else if (isWithinDriftTolerance(previous.amount, next.amount, STAKE_DRIFT_TOLERANCE_BPS)) drift = true;
@@ -4614,7 +4700,7 @@ const classifyBalance = (previous, next) => {
4614
4700
  for (const [key, previousValue] of previousByKey) {
4615
4701
  const nextValue = nextByKey.get(key);
4616
4702
  if (!nextValue) {
4617
- if (!isDriftProneValue(previousValue)) return changed(`value removed: ${key}`);
4703
+ if (!hasConvictionLock(previousValue)) return changed(`value removed: ${key}`);
4618
4704
  drift = true;
4619
4705
  continue;
4620
4706
  }
@@ -7486,11 +7572,14 @@ exports.excludeFromFeePayableLocks = excludeFromFeePayableLocks;
7486
7572
  exports.excludeFromTransferableAmount = excludeFromTransferableAmount;
7487
7573
  exports.filterBaseLocks = filterBaseLocks;
7488
7574
  exports.filterMirrorTokens = filterMirrorTokens;
7575
+ exports.findDTaoClaimablePlancks = findDTaoClaimablePlancks;
7489
7576
  exports.findDTaoConvictionLock = findDTaoConvictionLock;
7577
+ exports.findDTaoRootStakeHold = findDTaoRootStakeHold;
7490
7578
  exports.getBalanceFingerprint = getBalanceFingerprint;
7491
7579
  exports.getBalanceId = getBalanceId;
7492
7580
  exports.getBalanceStorageFingerprint = getBalanceStorageFingerprint;
7493
7581
  exports.getConvictionLockLabel = getConvictionLockLabel;
7582
+ exports.getDTaoClaimablePlancks = getDTaoClaimablePlancks;
7494
7583
  exports.getEpochTransferFee = getEpochTransferFee;
7495
7584
  exports.getExtension = getExtension;
7496
7585
  exports.getLockTitle = getLockTitle;
@@ -7504,6 +7593,7 @@ exports.getTransferFeeConfig = getTransferFeeConfig;
7504
7593
  exports.getTransferHook = getTransferHook;
7505
7594
  exports.getValueId = getValueId;
7506
7595
  exports.includeInTotalExtraAmount = includeInTotalExtraAmount;
7596
+ exports.isDTaoClaimableLock = isDTaoClaimableLock;
7507
7597
  exports.isEqualBalanceArrays = isEqualBalanceArrays;
7508
7598
  exports.isEqualBalancesResult = isEqualBalancesResult;
7509
7599
  exports.isEqualMiniMetadatas = isEqualMiniMetadatas;