From cfd90dd8bcb904384e64484c0aa9ea0811ff5423 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Thu, 18 Jun 2026 18:37:46 +0200 Subject: [PATCH 1/3] feat(chain-extensions): add read-only WASM queries for subnet registration, coldkey lock, and stake availability Adds three additive chain extension functions (IDs 34-36) for ink! contracts: - get_subnet_registration_state -> SubnetRegistrationState - get_coldkey_lock -> Option - get_stake_availability -> StakeAvailability Includes runtime handlers, encodable types (frozen via #[freeze_struct]), ink! contract bindings, unit tests, and docs. Makes Pallet::stake_availability public so the chain extension can reuse it. --- Cargo.lock | 1 + chain-extensions/Cargo.toml | 1 + chain-extensions/src/lib.rs | 57 ++++- chain-extensions/src/tests.rs | 296 +++++++++++++++++++++++++- chain-extensions/src/types.rs | 38 +++- contract-tests/bittensor/lib.rs | 75 +++++++ docs/wasm-contracts.md | 3 + pallets/subtensor/src/staking/lock.rs | 2 +- 8 files changed, 469 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bcf52ff339..c1a7353984 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18458,6 +18458,7 @@ dependencies = [ "sp-runtime", "sp-std", "substrate-fixed", + "subtensor-macros", "subtensor-runtime-common", "subtensor-swap-interface", ] diff --git a/chain-extensions/Cargo.toml b/chain-extensions/Cargo.toml index 74fa71e78a..74209cc106 100644 --- a/chain-extensions/Cargo.toml +++ b/chain-extensions/Cargo.toml @@ -21,6 +21,7 @@ sp-std.workspace = true codec = { workspace = true, features = ["derive"] } scale-info = { workspace = true, features = ["derive"] } subtensor-runtime-common.workspace = true +subtensor-macros.workspace = true pallet-contracts.workspace = true pallet-subtensor.workspace = true pallet-subtensor-swap.workspace = true diff --git a/chain-extensions/src/lib.rs b/chain-extensions/src/lib.rs index 0e842fe7d1..439b27600b 100644 --- a/chain-extensions/src/lib.rs +++ b/chain-extensions/src/lib.rs @@ -7,7 +7,7 @@ mod tests; pub mod types; -use crate::types::{FunctionId, Output}; +use crate::types::{ColdkeyLock, FunctionId, Output, StakeAvailability, SubnetRegistrationState}; use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::{DebugNoBound, traits::Get}; use frame_system::RawOrigin; @@ -595,6 +595,61 @@ where Ok(RetVal::Converging(Output::Success as u32)) } + FunctionId::GetSubnetRegistrationStateV1 => { + let netuid: NetUid = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + let state = SubnetRegistrationState { + netuid, + exists: pallet_subtensor::Pallet::::if_subnet_exist(netuid), + registered_subnet_counter: + pallet_subtensor::Pallet::::get_registered_subnet_counter(netuid), + }; + + env.write_output(&state.encode()) + .map_err(|_| DispatchError::Other("Failed to write output"))?; + + Ok(RetVal::Converging(Output::Success as u32)) + } + FunctionId::GetColdkeyLockV1 => { + let (coldkey, netuid): (T::AccountId, NetUid) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + let lock = + pallet_subtensor::Pallet::::get_coldkey_lock(&coldkey, netuid).map(|lock| { + ColdkeyLock { + locked_mass: u64::from(lock.locked_mass), + conviction_bits: lock.conviction.to_bits(), + last_update: lock.last_update, + } + }); + + env.write_output(&lock.encode()) + .map_err(|_| DispatchError::Other("Failed to write output"))?; + + Ok(RetVal::Converging(Output::Success as u32)) + } + FunctionId::GetStakeAvailabilityV1 => { + let (coldkey, netuid): (T::AccountId, NetUid) = env + .read_as() + .map_err(|_| DispatchError::Other("Failed to decode input parameters"))?; + + let (total, locked, available) = + pallet_subtensor::Pallet::::stake_availability(&coldkey, netuid); + let availability = StakeAvailability { + netuid, + total: u64::from(total), + locked: u64::from(locked), + available: u64::from(available), + }; + + env.write_output(&availability.encode()) + .map_err(|_| DispatchError::Other("Failed to write output"))?; + + Ok(RetVal::Converging(Output::Success as u32)) + } FunctionId::AddStakeV1 => { let origin = RawOrigin::Signed(env.caller()); Self::dispatch_add_stake_v1(env, origin) diff --git a/chain-extensions/src/tests.rs b/chain-extensions/src/tests.rs index deba3e5bd8..6d28793994 100644 --- a/chain-extensions/src/tests.rs +++ b/chain-extensions/src/tests.rs @@ -1,7 +1,7 @@ #![allow(clippy::unwrap_used)] use super::{SubtensorChainExtension, SubtensorExtensionEnv, mock}; -use crate::types::{FunctionId, Output}; +use crate::types::{ColdkeyLock, FunctionId, Output, StakeAvailability, SubnetRegistrationState}; use codec::{Decode, Encode}; use frame_support::pallet_prelude::Zero; use frame_support::{assert_ok, weights::Weight}; @@ -1625,6 +1625,300 @@ fn get_alpha_price_returns_encoded_price() { }); } +#[test] +fn get_subnet_registration_state_returns_existing_subnet_counter() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(8101); + let owner_coldkey = U256::from(8102); + let caller = U256::from(8103); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + let expected_counter = + pallet_subtensor::Pallet::::get_registered_subnet_counter(netuid); + + let mut env = MockEnv::new( + FunctionId::GetSubnetRegistrationStateV1, + caller, + netuid.encode(), + ); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_success(ret); + assert!(env.charged_weight().is_none()); + + let state = SubnetRegistrationState::decode(&mut &env.output()[..]).unwrap(); + assert_eq!( + state, + SubnetRegistrationState { + netuid, + exists: true, + registered_subnet_counter: expected_counter, + } + ); + }); +} + +#[test] +fn get_subnet_registration_state_preserves_counter_after_dissolve() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(8201); + let owner_coldkey = U256::from(8202); + let caller = U256::from(8203); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + let registered_counter = + pallet_subtensor::Pallet::::get_registered_subnet_counter(netuid); + + assert_ok!(pallet_subtensor::Pallet::::do_dissolve_network( + netuid + )); + + let mut env = MockEnv::new( + FunctionId::GetSubnetRegistrationStateV1, + caller, + netuid.encode(), + ); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_success(ret); + + let state = SubnetRegistrationState::decode(&mut &env.output()[..]).unwrap(); + assert_eq!(state.netuid, netuid); + assert!(!state.exists); + assert_eq!(state.registered_subnet_counter, registered_counter); + }); +} + +#[test] +fn get_subnet_registration_state_detects_reused_netuid_generation() { + mock::new_test_ext(1).execute_with(|| { + let first_hotkey = U256::from(8301); + let first_coldkey = U256::from(8302); + let second_hotkey = U256::from(8303); + let second_coldkey = U256::from(8304); + let caller = U256::from(8305); + + let netuid = mock::add_dynamic_network(&first_hotkey, &first_coldkey); + let first_counter = + pallet_subtensor::Pallet::::get_registered_subnet_counter(netuid); + + assert_ok!(pallet_subtensor::Pallet::::do_dissolve_network( + netuid + )); + let reused_netuid = mock::add_dynamic_network(&second_hotkey, &second_coldkey); + assert_eq!(reused_netuid, netuid); + + let mut env = MockEnv::new( + FunctionId::GetSubnetRegistrationStateV1, + caller, + netuid.encode(), + ); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_success(ret); + + let state = SubnetRegistrationState::decode(&mut &env.output()[..]).unwrap(); + assert_eq!(state.netuid, netuid); + assert!(state.exists); + assert!(state.registered_subnet_counter > first_counter); + }); +} + +#[test] +fn get_coldkey_lock_returns_none_without_lock() { + mock::new_test_ext(1).execute_with(|| { + let caller = U256::from(8401); + let coldkey = U256::from(8402); + let netuid = NetUid::from(1); + + let mut env = MockEnv::new( + FunctionId::GetColdkeyLockV1, + caller, + (coldkey, netuid).encode(), + ); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_success(ret); + assert!(env.charged_weight().is_none()); + + let lock = Option::::decode(&mut &env.output()[..]).unwrap(); + assert_eq!(lock, None); + }); +} + +#[test] +fn get_coldkey_lock_returns_rolled_lock() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(8501); + let owner_coldkey = U256::from(8502); + let coldkey = U256::from(8503); + let hotkey = U256::from(8504); + let stake = AlphaBalance::from(10_000u64); + let lock_amount = AlphaBalance::from(5_000u64); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, stake, + ); + + assert_ok!(pallet_subtensor::Pallet::::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + + frame_system::Pallet::::set_block_number(1_001); + let expected = + pallet_subtensor::Pallet::::get_coldkey_lock(&coldkey, netuid).unwrap(); + + let mut env = MockEnv::new( + FunctionId::GetColdkeyLockV1, + coldkey, + (coldkey, netuid).encode(), + ); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_success(ret); + assert!(env.charged_weight().is_none()); + + let lock = Option::::decode(&mut &env.output()[..]) + .unwrap() + .unwrap(); + assert_eq!(lock.locked_mass, u64::from(expected.locked_mass)); + assert_eq!(lock.conviction_bits, expected.conviction.to_bits()); + assert!(lock.conviction_bits > 0); + assert_eq!( + lock.last_update, + pallet_subtensor::Pallet::::get_current_block_as_u64() + ); + }); +} + +#[test] +fn get_stake_availability_returns_zeroes_without_stake_or_lock() { + mock::new_test_ext(1).execute_with(|| { + let caller = U256::from(8601); + let coldkey = U256::from(8602); + let netuid = NetUid::from(1); + + let mut env = MockEnv::new( + FunctionId::GetStakeAvailabilityV1, + caller, + (coldkey, netuid).encode(), + ); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_success(ret); + assert!(env.charged_weight().is_none()); + + let availability = StakeAvailability::decode(&mut &env.output()[..]).unwrap(); + assert_eq!( + availability, + StakeAvailability { + netuid, + total: 0, + locked: 0, + available: 0, + } + ); + }); +} + +#[test] +fn get_stake_availability_returns_partial_lock_breakdown() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(8701); + let owner_coldkey = U256::from(8702); + let coldkey = U256::from(8703); + let hotkey = U256::from(8704); + let stake = AlphaBalance::from(10_000u64); + let lock_amount = AlphaBalance::from(4_000u64); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, stake, + ); + assert_ok!(pallet_subtensor::Pallet::::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + + let mut env = MockEnv::new( + FunctionId::GetStakeAvailabilityV1, + coldkey, + (coldkey, netuid).encode(), + ); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_success(ret); + assert!(env.charged_weight().is_none()); + + let availability = StakeAvailability::decode(&mut &env.output()[..]).unwrap(); + assert_eq!( + availability, + StakeAvailability { + netuid, + total: u64::from(stake), + locked: u64::from(lock_amount), + available: u64::from(stake) - u64::from(lock_amount), + } + ); + }); +} + +#[test] +fn get_stake_availability_uses_rolled_forward_lock() { + mock::new_test_ext(1).execute_with(|| { + let owner_hotkey = U256::from(8801); + let owner_coldkey = U256::from(8802); + let coldkey = U256::from(8803); + let hotkey = U256::from(8804); + let stake = AlphaBalance::from(10_000u64); + let lock_amount = AlphaBalance::from(5_000u64); + + let netuid = mock::add_dynamic_network(&owner_hotkey, &owner_coldkey); + mock::register_ok_neuron(netuid, hotkey, coldkey, 0); + pallet_subtensor::Pallet::::increase_stake_for_hotkey_and_coldkey_on_subnet( + &hotkey, &coldkey, netuid, stake, + ); + assert_ok!(pallet_subtensor::Pallet::::do_lock_stake( + &coldkey, + netuid, + &hotkey, + lock_amount, + )); + + frame_system::Pallet::::set_block_number(1_001); + let expected_locked = + pallet_subtensor::Pallet::::get_current_locked(&coldkey, netuid); + assert!(expected_locked < lock_amount); + + let mut env = MockEnv::new( + FunctionId::GetStakeAvailabilityV1, + coldkey, + (coldkey, netuid).encode(), + ); + + let ret = SubtensorChainExtension::::dispatch(&mut env).unwrap(); + assert_success(ret); + assert!(env.charged_weight().is_none()); + + let availability = StakeAvailability::decode(&mut &env.output()[..]).unwrap(); + assert_eq!(availability.netuid, netuid); + assert_eq!(availability.total, u64::from(stake)); + assert_eq!(availability.locked, u64::from(expected_locked)); + assert_eq!( + availability.available, + u64::from(stake).saturating_sub(u64::from(expected_locked)) + ); + }); +} + /// `Caller*` dispatch uses `env.origin()` via `convert_origin`; with [`MockEnv`] both match /// `Signed(caller)`, so outcomes align with non-`Caller` arms. Weight expectations match the shared /// `dispatch_*_v1` helpers used by each pair. diff --git a/chain-extensions/src/types.rs b/chain-extensions/src/types.rs index 5c799d71e2..bb6ae3d6b4 100644 --- a/chain-extensions/src/types.rs +++ b/chain-extensions/src/types.rs @@ -1,6 +1,8 @@ use codec::{Decode, Encode}; use num_enum::{IntoPrimitive, TryFromPrimitive}; use sp_runtime::{DispatchError, ModuleError}; +use subtensor_macros::freeze_struct; +use subtensor_runtime_common::NetUid; #[repr(u16)] #[derive(TryFromPrimitive, IntoPrimitive, Decode, Encode)] @@ -39,6 +41,37 @@ pub enum FunctionId { CallerSetColdkeyAutoStakeHotkeyV1 = 31, CallerAddProxyV1 = 32, CallerRemoveProxyV1 = 33, + GetSubnetRegistrationStateV1 = 34, + GetColdkeyLockV1 = 35, + GetStakeAvailabilityV1 = 36, +} + +#[freeze_struct("cd2c2a7591a9d860")] +#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct SubnetRegistrationState { + pub netuid: NetUid, + pub exists: bool, + pub registered_subnet_counter: u64, +} + +#[freeze_struct("15e8c8d2d16cae4e")] +#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct ColdkeyLock { + pub locked_mass: u64, + pub conviction_bits: u128, + pub last_update: u64, +} + +#[freeze_struct("40d916a395c4566a")] +#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug)] +#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +pub struct StakeAvailability { + pub netuid: NetUid, + pub total: u64, + pub locked: u64, + pub available: u64, } #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug)] @@ -151,11 +184,14 @@ mod function_id_tests { assert_eq!(FunctionId::CallerSetColdkeyAutoStakeHotkeyV1 as u16, 31); assert_eq!(FunctionId::CallerAddProxyV1 as u16, 32); assert_eq!(FunctionId::CallerRemoveProxyV1 as u16, 33); + assert_eq!(FunctionId::GetSubnetRegistrationStateV1 as u16, 34); + assert_eq!(FunctionId::GetColdkeyLockV1 as u16, 35); + assert_eq!(FunctionId::GetStakeAvailabilityV1 as u16, 36); } #[test] fn caller_ids_roundtrip_try_from_primitive() { - for id in 16u16..=33u16 { + for id in 16u16..=36u16 { let v = FunctionId::try_from_primitive(id) .unwrap_or_else(|_| panic!("try_from_primitive failed for {id}")); assert_eq!(v as u16, id); diff --git a/contract-tests/bittensor/lib.rs b/contract-tests/bittensor/lib.rs index bff61d3b08..85adf61083 100755 --- a/contract-tests/bittensor/lib.rs +++ b/contract-tests/bittensor/lib.rs @@ -40,6 +40,9 @@ pub enum FunctionId { CallerSetColdkeyAutoStakeHotkeyV1 = 31, CallerAddProxyV1 = 32, CallerRemoveProxyV1 = 33, + GetSubnetRegistrationStateV1 = 34, + GetColdkeyLockV1 = 35, + GetStakeAvailabilityV1 = 36, } #[ink::chain_extension(extension = 0x1000)] @@ -269,6 +272,21 @@ pub trait RuntimeReadWrite { #[ink(function = 33)] fn caller_remove_proxy(delegate: ::AccountId); + + #[ink(function = 34)] + fn get_subnet_registration_state(netuid: u16) -> SubnetRegistrationState; + + #[ink(function = 35)] + fn get_coldkey_lock( + coldkey: ::AccountId, + netuid: u16, + ) -> Option; + + #[ink(function = 36)] + fn get_stake_availability( + coldkey: ::AccountId, + netuid: u16, + ) -> StakeAvailability; } #[ink::scale_derive(Encode, Decode, TypeInfo)] @@ -313,6 +331,28 @@ pub struct StakeInfo { is_registered: bool, } +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub struct SubnetRegistrationState { + netuid: u16, + exists: bool, + registered_subnet_counter: u64, +} + +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub struct ColdkeyLock { + locked_mass: u64, + conviction_bits: u128, + last_update: u64, +} + +#[ink::scale_derive(Encode, Decode, TypeInfo)] +pub struct StakeAvailability { + netuid: u16, + total: u64, + locked: u64, + available: u64, +} + #[ink::contract(env = crate::CustomEnvironment)] mod bittensor { use super::*; @@ -801,5 +841,40 @@ mod bittensor { .caller_remove_proxy(delegate.into()) .map_err(|_e| ReadWriteErrorCode::WriteFailed) } + + #[ink(message)] + pub fn get_subnet_registration_state( + &self, + netuid: u16, + ) -> Result { + self.env() + .extension() + .get_subnet_registration_state(netuid) + .map_err(|_e| ReadWriteErrorCode::ReadFailed) + } + + #[ink(message)] + pub fn get_coldkey_lock( + &self, + coldkey: [u8; 32], + netuid: u16, + ) -> Result, ReadWriteErrorCode> { + self.env() + .extension() + .get_coldkey_lock(coldkey.into(), netuid) + .map_err(|_e| ReadWriteErrorCode::ReadFailed) + } + + #[ink(message)] + pub fn get_stake_availability( + &self, + coldkey: [u8; 32], + netuid: u16, + ) -> Result { + self.env() + .extension() + .get_stake_availability(coldkey.into(), netuid) + .map_err(|_e| ReadWriteErrorCode::ReadFailed) + } } } diff --git a/docs/wasm-contracts.md b/docs/wasm-contracts.md index f787ea4375..c2b4734fa5 100644 --- a/docs/wasm-contracts.md +++ b/docs/wasm-contracts.md @@ -48,6 +48,9 @@ Subtensor provides a custom chain extension that allows smart contracts to inter | 17 | `burn_alpha` | Burn alpha stake without reducing SubnetAlphaOut (supply neutral) | `(AccountId, NetUid, AlphaBalance)` | `u64` (actual amount burned) | | 18 | `add_stake_recycle` | Atomically add stake then recycle the resulting alpha | `(AccountId, NetUid, TaoBalance)` | `u64` (alpha amount recycled) | | 19 | `add_stake_burn` | Atomically add stake then burn the resulting alpha | `(AccountId, NetUid, TaoBalance)` | `u64` (alpha amount burned) | +| 34 | `get_subnet_registration_state` | Query whether a subnet exists and which registration generation currently owns the netuid | `(NetUid)` | `SubnetRegistrationState { netuid, exists, registered_subnet_counter }` | +| 35 | `get_coldkey_lock` | Query the current rolled-forward lock state for a coldkey on a subnet | `(AccountId, NetUid)` | `Option` | +| 36 | `get_stake_availability` | Query total, locked, and currently available alpha for a coldkey on a subnet | `(AccountId, NetUid)` | `StakeAvailability { netuid, total, locked, available }` | > [!NOTE] > Functions **16** and **17** use the decoded argument order **`(hotkey, netuid, amount)`**, matching [`SubtensorChainExtension`](../chain-extensions/src/lib.rs). If your ink! contract encoded **`(hotkey, amount, netuid)`** for those functions, **recompile and redeploy**; the runtime will decode the older layout incorrectly. diff --git a/pallets/subtensor/src/staking/lock.rs b/pallets/subtensor/src/staking/lock.rs index e27f3e8d1e..78e2734c5b 100644 --- a/pallets/subtensor/src/staking/lock.rs +++ b/pallets/subtensor/src/staking/lock.rs @@ -688,7 +688,7 @@ impl Pallet { /// /// The lock is subnet-wide: it blocks unstaking from any hotkey on that subnet, /// not from a single hotkey position. - pub(crate) fn stake_availability( + pub fn stake_availability( coldkey: &T::AccountId, netuid: NetUid, ) -> (AlphaBalance, AlphaBalance, AlphaBalance) { From f14c9e9737aca9567905c0815cc6c04ead2895d4 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Mon, 22 Jun 2026 10:15:24 +0200 Subject: [PATCH 2/3] fix(chain-extensions): stabilize readonly dto hashes --- chain-extensions/src/types.rs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/chain-extensions/src/types.rs b/chain-extensions/src/types.rs index bb6ae3d6b4..9b14124b14 100644 --- a/chain-extensions/src/types.rs +++ b/chain-extensions/src/types.rs @@ -46,27 +46,24 @@ pub enum FunctionId { GetStakeAvailabilityV1 = 36, } -#[freeze_struct("cd2c2a7591a9d860")] -#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug)] -#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +#[freeze_struct("5dc33d60abed5c08")] +#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, scale_info::TypeInfo)] pub struct SubnetRegistrationState { pub netuid: NetUid, pub exists: bool, pub registered_subnet_counter: u64, } -#[freeze_struct("15e8c8d2d16cae4e")] -#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug)] -#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +#[freeze_struct("66308df56160c90c")] +#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, scale_info::TypeInfo)] pub struct ColdkeyLock { pub locked_mass: u64, pub conviction_bits: u128, pub last_update: u64, } -#[freeze_struct("40d916a395c4566a")] -#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug)] -#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] +#[freeze_struct("9bc2007bdf4287bc")] +#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, scale_info::TypeInfo)] pub struct StakeAvailability { pub netuid: NetUid, pub total: u64, From aadc739147bdf789ef02d3540002cba8e9fce752 Mon Sep 17 00:00:00 2001 From: Francisco Silva Date: Mon, 22 Jun 2026 15:29:52 +0200 Subject: [PATCH 3/3] fix(chain-extensions): use alpha balance lock DTOs --- chain-extensions/src/lib.rs | 8 ++++---- chain-extensions/src/tests.rs | 20 ++++++++++---------- chain-extensions/src/types.rs | 14 +++++++------- docs/wasm-contracts.md | 4 ++-- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/chain-extensions/src/lib.rs b/chain-extensions/src/lib.rs index 439b27600b..0315668441 100644 --- a/chain-extensions/src/lib.rs +++ b/chain-extensions/src/lib.rs @@ -620,7 +620,7 @@ where let lock = pallet_subtensor::Pallet::::get_coldkey_lock(&coldkey, netuid).map(|lock| { ColdkeyLock { - locked_mass: u64::from(lock.locked_mass), + locked_mass: lock.locked_mass, conviction_bits: lock.conviction.to_bits(), last_update: lock.last_update, } @@ -640,9 +640,9 @@ where pallet_subtensor::Pallet::::stake_availability(&coldkey, netuid); let availability = StakeAvailability { netuid, - total: u64::from(total), - locked: u64::from(locked), - available: u64::from(available), + total, + locked, + available, }; env.write_output(&availability.encode()) diff --git a/chain-extensions/src/tests.rs b/chain-extensions/src/tests.rs index 6d28793994..16619389bf 100644 --- a/chain-extensions/src/tests.rs +++ b/chain-extensions/src/tests.rs @@ -1786,7 +1786,7 @@ fn get_coldkey_lock_returns_rolled_lock() { let lock = Option::::decode(&mut &env.output()[..]) .unwrap() .unwrap(); - assert_eq!(lock.locked_mass, u64::from(expected.locked_mass)); + assert_eq!(lock.locked_mass, expected.locked_mass); assert_eq!(lock.conviction_bits, expected.conviction.to_bits()); assert!(lock.conviction_bits > 0); assert_eq!( @@ -1818,9 +1818,9 @@ fn get_stake_availability_returns_zeroes_without_stake_or_lock() { availability, StakeAvailability { netuid, - total: 0, - locked: 0, - available: 0, + total: AlphaBalance::ZERO, + locked: AlphaBalance::ZERO, + available: AlphaBalance::ZERO, } ); }); @@ -1863,9 +1863,9 @@ fn get_stake_availability_returns_partial_lock_breakdown() { availability, StakeAvailability { netuid, - total: u64::from(stake), - locked: u64::from(lock_amount), - available: u64::from(stake) - u64::from(lock_amount), + total: stake, + locked: lock_amount, + available: stake.saturating_sub(lock_amount), } ); }); @@ -1910,11 +1910,11 @@ fn get_stake_availability_uses_rolled_forward_lock() { let availability = StakeAvailability::decode(&mut &env.output()[..]).unwrap(); assert_eq!(availability.netuid, netuid); - assert_eq!(availability.total, u64::from(stake)); - assert_eq!(availability.locked, u64::from(expected_locked)); + assert_eq!(availability.total, stake); + assert_eq!(availability.locked, expected_locked); assert_eq!( availability.available, - u64::from(stake).saturating_sub(u64::from(expected_locked)) + stake.saturating_sub(expected_locked) ); }); } diff --git a/chain-extensions/src/types.rs b/chain-extensions/src/types.rs index 9b14124b14..001965cc90 100644 --- a/chain-extensions/src/types.rs +++ b/chain-extensions/src/types.rs @@ -2,7 +2,7 @@ use codec::{Decode, Encode}; use num_enum::{IntoPrimitive, TryFromPrimitive}; use sp_runtime::{DispatchError, ModuleError}; use subtensor_macros::freeze_struct; -use subtensor_runtime_common::NetUid; +use subtensor_runtime_common::{AlphaBalance, NetUid}; #[repr(u16)] #[derive(TryFromPrimitive, IntoPrimitive, Decode, Encode)] @@ -54,21 +54,21 @@ pub struct SubnetRegistrationState { pub registered_subnet_counter: u64, } -#[freeze_struct("66308df56160c90c")] +#[freeze_struct("bf4c1e249109618")] #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, scale_info::TypeInfo)] pub struct ColdkeyLock { - pub locked_mass: u64, + pub locked_mass: AlphaBalance, pub conviction_bits: u128, pub last_update: u64, } -#[freeze_struct("9bc2007bdf4287bc")] +#[freeze_struct("fb12f00479cf6990")] #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug, scale_info::TypeInfo)] pub struct StakeAvailability { pub netuid: NetUid, - pub total: u64, - pub locked: u64, - pub available: u64, + pub total: AlphaBalance, + pub locked: AlphaBalance, + pub available: AlphaBalance, } #[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, Debug)] diff --git a/docs/wasm-contracts.md b/docs/wasm-contracts.md index c2b4734fa5..2e9db982d5 100644 --- a/docs/wasm-contracts.md +++ b/docs/wasm-contracts.md @@ -49,8 +49,8 @@ Subtensor provides a custom chain extension that allows smart contracts to inter | 18 | `add_stake_recycle` | Atomically add stake then recycle the resulting alpha | `(AccountId, NetUid, TaoBalance)` | `u64` (alpha amount recycled) | | 19 | `add_stake_burn` | Atomically add stake then burn the resulting alpha | `(AccountId, NetUid, TaoBalance)` | `u64` (alpha amount burned) | | 34 | `get_subnet_registration_state` | Query whether a subnet exists and which registration generation currently owns the netuid | `(NetUid)` | `SubnetRegistrationState { netuid, exists, registered_subnet_counter }` | -| 35 | `get_coldkey_lock` | Query the current rolled-forward lock state for a coldkey on a subnet | `(AccountId, NetUid)` | `Option` | -| 36 | `get_stake_availability` | Query total, locked, and currently available alpha for a coldkey on a subnet | `(AccountId, NetUid)` | `StakeAvailability { netuid, total, locked, available }` | +| 35 | `get_coldkey_lock` | Query the current rolled-forward lock state for a coldkey on a subnet | `(AccountId, NetUid)` | `Option` | +| 36 | `get_stake_availability` | Query total, locked, and currently available alpha for a coldkey on a subnet | `(AccountId, NetUid)` | `StakeAvailability { netuid: NetUid, total: AlphaBalance, locked: AlphaBalance, available: AlphaBalance }` | > [!NOTE] > Functions **16** and **17** use the decoded argument order **`(hotkey, netuid, amount)`**, matching [`SubtensorChainExtension`](../chain-extensions/src/lib.rs). If your ink! contract encoded **`(hotkey, amount, netuid)`** for those functions, **recompile and redeploy**; the runtime will decode the older layout incorrectly.