Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 8 additions & 114 deletions docs/tx/remove-stake-limit.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ payout: the claim pays 100% of the basket.

| Signer | Origin | Pallet | Wraps |
| --- | --- | --- | --- |
| `coldkey` | signed account (pallet role may apply) | SubtensorModule | [`SubtensorModule.remove_stake_limit`](/code/pallets/subtensor/src/macros/dispatches.rs#L1468-L1486), [`SubtensorModule.claim_root_with_hotkey`](/code/pallets/subtensor/src/macros/dispatches.rs#L1989-L2008), [`Utility.batch_all`](/code/pallets/utility/src/lib.rs#L308-L362) |
| `coldkey` | signed account (pallet role may apply) | SubtensorModule | [`SubtensorModule.remove_stake_limit`](/code/pallets/subtensor/src/macros/dispatches.rs#L1468-L1486), [`SubtensorModule.remove_stake`](/code/pallets/subtensor/src/macros/dispatches.rs#L610-L619), [`SubtensorModule.claim_root_with_hotkey`](/code/pallets/subtensor/src/macros/dispatches.rs#L1989-L2008), [`Utility.batch_all`](/code/pallets/utility/src/lib.rs#L308-L362) |

## Parameters

Expand All @@ -31,7 +31,7 @@ payout: the claim pays 100% of the basket.
| `amount_alpha` | number \| `"all"` | yes | How much to unstake from this position, or ``all``. |
| `limit_price_rao` | integer | yes | Worst pool price you will accept for the swap. The call fails (or fills partially when allow-partial is set) instead of executing beyond this price. |
| `allow_partial` | boolean | no | Execute whatever portion fits within the limit price and drop the remainder, instead of failing the whole call when the limit would be breached. |
| `claim` | boolean | no | Also redeem this validator's whole basket entitlement before unstaking, in one atomic batch. Root only (netuid 0). This is not a proportional payout: unstaking 40% still claims 100% of the basket. The chain has no proportional-claim call. Claimed yield is restaked on root, then the unstake runs — pass `all` to take principal and yield out together. |
| `claim` | boolean | no | Also redeem this validator's whole basket entitlement before unstaking, in one atomic batch. Root only (netuid 0). This is not a proportional payout: unstaking 40% still claims 100% of the basket. The chain has no proportional-claim call. Claimed yield is restaked on root, then the unstake runs — pass `all` to take principal and yield out together. Unavailable while RootStakeUnlockInterval is nonzero, because the claim starts a new hold window; claim first, wait, then unstake in that mode. |

Address parameters (`--hotkey`, `--coldkey`, `--dest`, ...) accept a raw ss58
address, an address-book or proxy-book name, or a local wallet/hotkey name.
Expand Down Expand Up @@ -80,117 +80,11 @@ result = sub.execute_tool("remove_stake_limit", {...}, wallet)

## On-chain implementation

`SubtensorModule.remove_stake_limit` — [`pallets/subtensor/src/macros/dispatches.rs#L1470`](/code/pallets/subtensor/src/macros/dispatches.rs#L1468-L1486):

```rust
#[pallet::call_index(89)]
#[pallet::weight(<T as crate::pallet::Config>::WeightInfo::remove_stake_limit())]
pub fn remove_stake_limit(
origin: OriginFor<T>,
hotkey: T::AccountId,
netuid: NetUid,
amount_unstaked: AlphaBalance,
limit_price: TaoBalance,
allow_partial: bool,
) -> DispatchResult {
Self::do_remove_stake_limit(
origin,
hotkey,
netuid,
amount_unstaked,
limit_price,
allow_partial,
)
}
```

Delegates to [`do_remove_stake_limit`](/code/pallets/subtensor/src/staking/remove_stake.rs#L302).

`SubtensorModule.claim_root_with_hotkey` — [`pallets/subtensor/src/macros/dispatches.rs#L1993`](/code/pallets/subtensor/src/macros/dispatches.rs#L1989-L2008):

```rust
#[pallet::call_index(148)]
#[pallet::weight(
<T as crate::pallet::Config>::WeightInfo::claim_root(crate::MAX_ROOT_CLAIM_WORK)
)]
pub fn claim_root_with_hotkey(
origin: OriginFor<T>,
hotkey: T::AccountId,
) -> DispatchResultWithPostInfo {
let coldkey: T::AccountId = ensure_signed(origin)?;
ensure!(
Self::root_claim_fits_declared_budget(core::slice::from_ref(&hotkey)),
Error::<T>::RootClaimTooHeavy
);

let outcome = Self::do_root_claim(coldkey.clone(), vec![hotkey])?;
Self::maybe_add_coldkey_index(&coldkey);

let weight = Self::root_claim_actual_weight(1, &outcome);
Ok((Some(weight), Pays::Yes).into())
}
```

Delegates to [`root_claim_fits_declared_budget`](/code/pallets/subtensor/src/staking/claim_root.rs#L761), [`do_root_claim`](/code/pallets/subtensor/src/staking/claim_root.rs#L798), [`maybe_add_coldkey_index`](/code/pallets/subtensor/src/staking/claim_root.rs#L827).

`Utility.batch_all` — [`pallets/utility/src/lib.rs#L314`](/code/pallets/utility/src/lib.rs#L308-L362):

```rust
#[pallet::call_index(2)]
#[pallet::weight({
let (dispatch_weight, pays) = Pallet::<T>::weight_and_dispatch_class(calls);
let dispatch_weight = dispatch_weight.saturating_add(T::WeightInfo::batch_all(calls.len() as u32));
(dispatch_weight, DispatchClass::Normal, pays)
})]
pub fn batch_all(
origin: OriginFor<T>,
calls: Vec<<T as Config>::RuntimeCall>,
) -> DispatchResultWithPostInfo {
// Do not allow the `None` origin.
if ensure_none(origin.clone()).is_ok() {
return Err(BadOrigin.into());
}

let is_root = ensure_root(origin.clone()).is_ok();
let calls_len = calls.len();
ensure!(
calls_len <= Self::batched_calls_limit() as usize,
Error::<T>::TooManyCalls
);

// Track the actual weight of each of the batch calls.
let mut weight = Weight::zero();
for (index, call) in calls.into_iter().enumerate() {
let info = call.get_dispatch_info();
// If origin is root, bypass any dispatch filter; root can call anything.
let result = if is_root {
call.dispatch_bypass_filter(origin.clone())
} else {
let mut filtered_origin = origin.clone();
// Don't allow users to nest `batch_all` calls.
filtered_origin.add_filter(
move |c: &<T as frame_system::Config>::RuntimeCall| {
let c = <T as Config>::RuntimeCall::from_ref(c);
!matches!(c.is_sub_type(), Some(Call::batch_all { .. }))
},
);
call.dispatch(filtered_origin)
};
// Add the weight of this call.
weight = weight.saturating_add(extract_actual_weight(&result, &info));
result.map_err(|mut err| {
// Take the weight of this function itself into account.
let base_weight = T::WeightInfo::batch_all(index.saturating_add(1) as u32);
// Return the actual used weight + base_weight of this call.
err.post_info = Some(base_weight.saturating_add(weight)).into();
err
})?;
Self::deposit_event(Event::ItemCompleted);
}
Self::deposit_event(Event::BatchCompleted);
let base_weight = T::WeightInfo::batch_all(calls_len as u32);
Ok(Some(base_weight.saturating_add(weight)).into())
}
```
| Chain call | Source |
| --- | --- |
| `SubtensorModule.remove_stake_limit` | [`pallets/subtensor/src/macros/dispatches.rs#L1470`](/code/pallets/subtensor/src/macros/dispatches.rs#L1468-L1486) |
| `SubtensorModule.remove_stake` | [`pallets/subtensor/src/macros/dispatches.rs#L612`](/code/pallets/subtensor/src/macros/dispatches.rs#L610-L619) |
| `SubtensorModule.claim_root_with_hotkey` | [`pallets/subtensor/src/macros/dispatches.rs#L1993`](/code/pallets/subtensor/src/macros/dispatches.rs#L1989-L2008) |
| `Utility.batch_all` | [`pallets/utility/src/lib.rs#L314`](/code/pallets/utility/src/lib.rs#L308-L362) |

Every file is browsable under [/code](/code) exactly as built into the runtime, or as plain text under `/code/raw/<path>` (index: [`/code/index.json`](/code/index.json)).
2 changes: 1 addition & 1 deletion docs/tx/remove-stake.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ not a slice of the amount you unstake.
| `amount_alpha` | number \| `"all"` | yes | How much to unstake from this position, or ``all``. |
| `slippage_protection` | boolean | no | Bound the price the swap may execute at (on by default): the call fails (`SlippageTooHigh`) instead of filling once the pool price moves more than `rate_tolerance` from the price at submission. Disable to execute at any price. |
| `rate_tolerance` | number | no | Maximum price move slippage protection accepts, as a fraction (0.05 = 5%). Ignored when slippage protection is disabled. |
| `claim` | boolean | no | Also redeem this validator's whole basket entitlement before unstaking, in one atomic batch. Root only (netuid 0). This is not a proportional payout: unstaking 40% still claims 100% of the basket. The chain has no proportional-claim call. Claimed yield is restaked on root, then the unstake runs — pass `all` to take principal and yield out together. |
| `claim` | boolean | no | Also redeem this validator's whole basket entitlement before unstaking, in one atomic batch. Root only (netuid 0). This is not a proportional payout: unstaking 40% still claims 100% of the basket. The chain has no proportional-claim call. Claimed yield is restaked on root, then the unstake runs — pass `all` to take principal and yield out together. Unavailable while RootStakeUnlockInterval is nonzero, because the claim starts a new hold window; claim first, wait, then unstake in that mode. |

Address parameters (`--hotkey`, `--coldkey`, `--dest`, ...) accept a raw ss58
address, an address-book or proxy-book name, or a local wallet/hotkey name.
Expand Down
2 changes: 1 addition & 1 deletion docs/tx/unstake-all.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ payout: the claim pays 100% of the basket.
| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `hotkey_ss58` | string | yes | Hotkey whose entire stake is removed. |
| `claim` | boolean | no | Also redeem this validator's whole root basket entitlement before unstaking, in one atomic batch. This is not a proportional payout: the claim pays 100% of the basket. Claimed yield is restaked on root, then unstake-all takes principal and that yield out together. |
| `claim` | boolean | no | Also redeem this validator's whole root basket entitlement before unstaking, in one atomic batch. This is not a proportional payout: the claim pays 100% of the basket. Claimed yield is restaked on root, then unstake-all takes principal and that yield out together. Unavailable while RootStakeUnlockInterval is nonzero; claim first, wait for the hold, then unstake in that mode. |

Address parameters (`--hotkey`, `--coldkey`, `--dest`, ...) accept a raw ss58
address, an address-book or proxy-book name, or a local wallet/hotkey name.
Expand Down
38 changes: 12 additions & 26 deletions sdk/python/bittensor/cli/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

from .. import config as cfg
from .. import wallets
from .._generated import calls as generated_calls
from .._generated import runtime_apis, storage
from ..balance import Balance
from ..client import Client
Expand All @@ -44,7 +43,6 @@
)
from ..settings import error_docs_url
from ..signing import public_view
from ..sp_core import ss58_decode
from ..vault import VaultSigner
from ..wallets import is_bittensor_address
from . import multisig_helpers as ms_helpers
Expand All @@ -53,16 +51,6 @@
T = TypeVar("T")


class _FeeAddressView:
"""Public-only keypair shape for ``estimate_fee`` (zeroed signature)."""

crypto_type = 1 # sr25519

def __init__(self, address: str):
self.ss58_address = address
self.public_key = bytes(ss58_decode(address))


def address_cli_name(param: str) -> str:
"""CLI flag for a param resolved by ``resolve_address`` (drops the ``_ss58`` suffix)."""
base = param[: -len("_ss58")] if param.endswith("_ss58") else param.replace("_ss58", "")
Expand Down Expand Up @@ -912,17 +900,20 @@ async def _shield_fee_warning(client):

async def _preflight(client):
try:
origin = public_view(wallet, intent.signer).ss58_address
preview = await client.preflight(
intent,
wallet,
proxy_for=proxy_for,
proxy_type=force_proxy_type,
)
except Exception:
return [], [], []
target = proxy_for or origin
warnings = list(await intent.warnings(client._substrate, target))
warnings = list(preview.warnings)
if semantic_intent.op in ("claim_root", "claim_root_with_hotkey"):
effects = list(await intent.effects(client._substrate, target))
blocks = list(await intent.blocks(client._substrate, target))
effects = list(preview.effects)
else:
effects, blocks = [], []
return effects, warnings, blocks
effects = []
return effects, warnings, list(preview.blocks)

effects, warnings, blocks = self.run(_preflight)
summary_line = intent.summary()
Expand All @@ -938,7 +929,7 @@ async def _preflight(client):
for block in blocks:
self.output.error(
block,
help="fund the coldkey so free TAO covers the reserved inclusion fee",
help="resolve this hard stop before submitting",
)
raise typer.Exit(1)

Expand Down Expand Up @@ -1389,12 +1380,7 @@ async def _shield_outer_fee_shortfall(
"""
try:
free = await client.balances.get(fee_payer_ss58)
# Exact ciphertext size is unknown until encrypt; length fee is
# 1 rao/byte so a padded dummy keeps the estimate conservative.
outer = await client.compose(
generated_calls.MevShield.submit_encrypted(ciphertext=bytes(8192))
)
fee = await client._substrate.estimate_fee(outer, _FeeAddressView(fee_payer_ss58))
fee = await client.estimate_shielded_carrier_fee(fee_payer_ss58)
except Exception:
# Best-effort: if we only know free is zero, that is enough to warn.
try:
Expand Down
23 changes: 22 additions & 1 deletion sdk/python/bittensor/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@
from ._substrate import RpcSubstrate, Substrate
from ._transport.contract import UnsignedExtrinsic
from .balance import Balance
from .executor import Executor
from .executor import Executor, estimate_shielded_carrier_fee
from .intents import Intent, Plan, Policy
from .intents.base import IntentPreflight
from .multisig import Multisig
from .namespaces import (
Balances,
Expand Down Expand Up @@ -413,6 +414,26 @@ async def wait() -> EpochEvent:

# Intent layer -----------------------------------------------------------

async def preflight(
self,
intent: Intent,
wallet: WalletLike,
*,
proxy_for: Optional[str] = None,
proxy_type: Optional[str] = None,
) -> IntentPreflight:
"""Preview context-sensitive effects and hard stops without fee planning."""
return await self._executor.preflight(
intent,
wallet,
proxy_for=proxy_for,
proxy_type=proxy_type,
)

async def estimate_shielded_carrier_fee(self, fee_payer: str) -> Balance:
"""Conservatively estimate the TAO fee of a MevShield carrier."""
return await estimate_shielded_carrier_fee(self._substrate, fee_payer)

async def plan(self, intent: Intent, wallet: WalletLike, **kwargs) -> Plan:
"""Preview an intent (fee, effects, warnings, policy) without submitting.

Expand Down
Loading
Loading