diff --git a/CHANGELOG.md b/CHANGELOG.md
index 27b5ca54b..95aae0d8e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## 9.12.0 /2025-09-25
+* Removes warning icon in transfer by @ibraheem-abe in https://github.com/opentensor/btcli/pull/634
+* Add Extrinsic Identifier Output by @thewhaleking in https://github.com/opentensor/btcli/pull/633
+* Update the example text for sudo trim by @thewhaleking in https://github.com/opentensor/btcli/pull/636
+* Feat/Individual wallet list by @ibraheem-abe in https://github.com/opentensor/btcli/pull/638
+* Feat/ subnet mechanisms by @ibraheem-abe in https://github.com/opentensor/btcli/pull/627
+
+**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.11.2...v9.12.0
+
## 9.11.2 /2025-09-19
* Fix: Stake movement between non-root sns by @ibraheem-abe in https://github.com/opentensor/btcli/pull/629
diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py
index 88ae40580..7fc4fbde8 100755
--- a/bittensor_cli/cli.py
+++ b/bittensor_cli/cli.py
@@ -78,7 +78,11 @@
add as add_stake,
remove as remove_stake,
)
-from bittensor_cli.src.commands.subnets import price, subnets
+from bittensor_cli.src.commands.subnets import (
+ price,
+ subnets,
+ mechanisms as subnet_mechanisms,
+)
from bittensor_cli.version import __version__, __version_as_int__
try:
@@ -229,6 +233,15 @@ def edit_help(cls, option_name: str, help_text: str):
help="The netuid of the subnet in the network, (e.g. 1).",
prompt=False,
)
+ mechanism_id = typer.Option(
+ None,
+ "--mechid",
+ "--mech-id",
+ "--mech_id",
+ "--mechanism_id",
+ "--mechanism-id",
+ help="Mechanism ID within the subnet (defaults to 0).",
+ )
all_netuids = typer.Option(
False,
help="Use all netuids",
@@ -650,6 +663,7 @@ class CLIManager:
:var wallet_app: the Typer app as it relates to wallet commands
:var stake_app: the Typer app as it relates to stake commands
:var sudo_app: the Typer app as it relates to sudo commands
+ :var subnet_mechanisms_app: the Typer app for subnet mechanism commands
:var subnets_app: the Typer app as it relates to subnets commands
:var subtensor: the `SubtensorInterface` object passed to the various commands that require it
"""
@@ -658,7 +672,9 @@ class CLIManager:
app: typer.Typer
config_app: typer.Typer
wallet_app: typer.Typer
+ sudo_app: typer.Typer
subnets_app: typer.Typer
+ subnet_mechanisms_app: typer.Typer
weights_app: typer.Typer
utils_app: typer.Typer
view_app: typer.Typer
@@ -733,6 +749,7 @@ def __init__(self):
self.stake_app = typer.Typer(epilog=_epilog)
self.sudo_app = typer.Typer(epilog=_epilog)
self.subnets_app = typer.Typer(epilog=_epilog)
+ self.subnet_mechanisms_app = typer.Typer(epilog=_epilog)
self.weights_app = typer.Typer(epilog=_epilog)
self.view_app = typer.Typer(epilog=_epilog)
self.liquidity_app = typer.Typer(epilog=_epilog)
@@ -794,6 +811,19 @@ def __init__(self):
self.subnets_app, name="subnet", hidden=True, no_args_is_help=True
)
+ # subnet mechanisms aliases
+ self.subnets_app.add_typer(
+ self.subnet_mechanisms_app,
+ name="mechanisms",
+ short_help="Subnet mechanism commands, alias: `mech`",
+ no_args_is_help=True,
+ )
+ self.subnets_app.add_typer(
+ self.subnet_mechanisms_app,
+ name="mech",
+ hidden=True,
+ no_args_is_help=True,
+ )
# weights aliases
self.app.add_typer(
self.weights_app,
@@ -938,6 +968,20 @@ def __init__(self):
children_app.command("revoke")(self.stake_revoke_children)
children_app.command("take")(self.stake_childkey_take)
+ # subnet mechanism commands
+ self.subnet_mechanisms_app.command(
+ "count", rich_help_panel=HELP_PANELS["MECHANISMS"]["CONFIG"]
+ )(self.mechanism_count_get)
+ self.subnet_mechanisms_app.command(
+ "set", rich_help_panel=HELP_PANELS["MECHANISMS"]["CONFIG"]
+ )(self.mechanism_count_set)
+ self.subnet_mechanisms_app.command(
+ "emissions", rich_help_panel=HELP_PANELS["MECHANISMS"]["EMISSION"]
+ )(self.mechanism_emission_get)
+ self.subnet_mechanisms_app.command(
+ "emissions-split", rich_help_panel=HELP_PANELS["MECHANISMS"]["EMISSION"]
+ )(self.mechanism_emission_set)
+
# sudo commands
self.sudo_app.command("set", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])(
self.sudo_set
@@ -960,6 +1004,9 @@ def __init__(self):
self.sudo_app.command("get-take", rich_help_panel=HELP_PANELS["SUDO"]["TAKE"])(
self.sudo_get_take
)
+ self.sudo_app.command("trim", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])(
+ self.sudo_trim
+ )
# subnets commands
self.subnets_app.command(
@@ -1759,6 +1806,43 @@ def ask_partial_stake(
logger.debug(f"Partial staking {partial_staking}")
return False
+ def ask_subnet_mechanism(
+ self,
+ mechanism_id: Optional[int],
+ mechanism_count: int,
+ netuid: int,
+ ) -> int:
+ """Resolve the mechanism ID to use."""
+
+ if mechanism_count is None or mechanism_count <= 0:
+ err_console.print(f"Subnet {netuid} does not exist.")
+ raise typer.Exit()
+
+ if mechanism_id is not None:
+ if mechanism_id < 0 or mechanism_id >= mechanism_count:
+ err_console.print(
+ f"Mechanism ID {mechanism_id} is out of range for subnet {netuid}. "
+ f"Valid range: [bold cyan]0 to {mechanism_count - 1}[/bold cyan]."
+ )
+ raise typer.Exit()
+ return mechanism_id
+
+ if mechanism_count == 1:
+ return 0
+
+ while True:
+ selected_mechanism_id = IntPrompt.ask(
+ f"Select mechanism ID for subnet {netuid} "
+ f"([bold cyan]0 to {mechanism_count - 1}[/bold cyan])",
+ default=0,
+ )
+ if 0 <= selected_mechanism_id < mechanism_count:
+ return selected_mechanism_id
+ err_console.print(
+ f"Mechanism ID {selected_mechanism_id} is out of range for subnet {netuid}. "
+ f"Valid range: [bold cyan]0 to {mechanism_count - 1}[/bold cyan]."
+ )
+
def wallet_ask(
self,
wallet_name: Optional[str],
@@ -1872,6 +1956,7 @@ def wallet_ask(
def wallet_list(
self,
+ wallet_name: Optional[str] = Options.wallet_name,
wallet_path: str = Options.wallet_path,
quiet: bool = Options.quiet,
verbose: bool = Options.verbose,
@@ -1895,7 +1980,13 @@ def wallet_list(
wallet = self.wallet_ask(
None, wallet_path, None, ask_for=[WO.PATH], validate=WV.NONE
)
- return self._run_command(wallets.wallet_list(wallet.path, json_output))
+ return self._run_command(
+ wallets.wallet_list(
+ wallet.path,
+ json_output,
+ wallet_name=wallet_name,
+ )
+ )
def wallet_overview(
self,
@@ -3751,6 +3842,8 @@ def stake_add(
subnets.show(
subtensor=self.initialize_chain(network),
netuid=netuid_,
+ mechanism_id=0,
+ mechanism_count=1,
sort=False,
max_rows=12,
prompt=False,
@@ -4376,7 +4469,7 @@ def stake_move(
f"interactive_selection: {interactive_selection}\n"
f"prompt: {prompt}\n"
)
- result = self._run_command(
+ result, ext_id = self._run_command(
move_stake.move_stake(
subtensor=self.initialize_chain(network),
wallet=wallet,
@@ -4392,7 +4485,9 @@ def stake_move(
)
)
if json_output:
- json_console.print(json.dumps({"success": result}))
+ json_console.print(
+ json.dumps({"success": result, "extrinsic_identifier": ext_id or None})
+ )
return result
def stake_transfer(
@@ -4552,7 +4647,7 @@ def stake_transfer(
f"era: {period}\n"
f"stake_all: {stake_all}"
)
- result = self._run_command(
+ result, ext_id = self._run_command(
move_stake.transfer_stake(
wallet=wallet,
subtensor=self.initialize_chain(network),
@@ -4568,7 +4663,9 @@ def stake_transfer(
)
)
if json_output:
- json_console.print(json.dumps({"success": result}))
+ json_console.print(
+ json.dumps({"success": result, "extrinsic_identifier": ext_id or None})
+ )
return result
def stake_swap(
@@ -4672,7 +4769,7 @@ def stake_swap(
f"wait_for_inclusion: {wait_for_inclusion}\n"
f"wait_for_finalization: {wait_for_finalization}\n"
)
- result = self._run_command(
+ result, ext_id = self._run_command(
move_stake.swap_stake(
wallet=wallet,
subtensor=self.initialize_chain(network),
@@ -4688,7 +4785,9 @@ def stake_swap(
)
)
if json_output:
- json_console.print(json.dumps({"success": result}))
+ json_console.print(
+ json.dumps({"success": result, "extrinsic_identifier": ext_id or None})
+ )
return result
def stake_get_children(
@@ -4987,7 +5086,7 @@ def stake_childkey_take(
f"wait_for_inclusion: {wait_for_inclusion}\n"
f"wait_for_finalization: {wait_for_finalization}\n"
)
- results: list[tuple[Optional[int], bool]] = self._run_command(
+ results: list[tuple[Optional[int], bool, Optional[str]]] = self._run_command(
children_hotkeys.childkey_take(
wallet=wallet,
subtensor=self.initialize_chain(network),
@@ -5001,11 +5100,243 @@ def stake_childkey_take(
)
if json_output:
output = {}
- for netuid_, success in results:
- output[netuid_] = success
+ for netuid_, success, ext_id in results:
+ output[netuid_] = {"success": success, "extrinsic_identifier": ext_id}
json_console.print(json.dumps(output))
return results
+ def mechanism_count_set(
+ self,
+ network: Optional[list[str]] = Options.network,
+ wallet_name: str = Options.wallet_name,
+ wallet_path: str = Options.wallet_path,
+ wallet_hotkey: str = Options.wallet_hotkey,
+ netuid: int = Options.netuid,
+ mechanism_count: Optional[int] = typer.Option(
+ None,
+ "--count",
+ "--mech-count",
+ help="Number of mechanisms to set for the subnet.",
+ ),
+ wait_for_inclusion: bool = Options.wait_for_inclusion,
+ wait_for_finalization: bool = Options.wait_for_finalization,
+ prompt: bool = Options.prompt,
+ quiet: bool = Options.quiet,
+ verbose: bool = Options.verbose,
+ json_output: bool = Options.json_output,
+ ):
+ """
+ Configure how many mechanisms are registered for a subnet.
+
+ The base mechanism at index 0 and new ones are incremented by 1.
+
+ [bold]Common Examples:[/bold]
+
+ 1. Prompt for the new mechanism count interactively:
+ [green]$[/green] btcli subnet mech set --netuid 12
+
+ 2. Set the count to 2 using a specific wallet:
+ [green]$[/green] btcli subnet mech set --netuid 12 --count 2 --wallet.name my_wallet --wallet.hotkey admin
+
+ """
+
+ self.verbosity_handler(quiet, verbose, json_output)
+ subtensor = self.initialize_chain(network)
+
+ if not json_output:
+ current_count = self._run_command(
+ subnet_mechanisms.count(
+ subtensor=subtensor,
+ netuid=netuid,
+ json_output=False,
+ ),
+ exit_early=False,
+ )
+ else:
+ current_count = self._run_command(
+ subtensor.get_subnet_mechanisms(netuid),
+ exit_early=False,
+ )
+
+ if mechanism_count is None:
+ if not prompt:
+ err_console.print(
+ "Mechanism count not supplied with `--no-prompt` flag. Cannot continue."
+ )
+ return False
+ prompt_text = "\n\nEnter the [blue]number of mechanisms[/blue] to set"
+ mechanism_count = IntPrompt.ask(prompt_text)
+
+ if mechanism_count == current_count:
+ visible_count = max(mechanism_count - 1, 0)
+ message = (
+ ":white_heavy_check_mark: "
+ f"[dark_sea_green3]Subnet {netuid} already has {visible_count} mechanism"
+ f"{'s' if visible_count != 1 else ''}.[/dark_sea_green3]"
+ )
+ if json_output:
+ json_console.print(
+ json.dumps(
+ {
+ "success": True,
+ "message": f"Subnet {netuid} already has {visible_count} mechanisms.",
+ "extrinsic_identifier": None,
+ }
+ )
+ )
+ else:
+ console.print(message)
+ return True
+
+ wallet = self.wallet_ask(
+ wallet_name,
+ wallet_path,
+ wallet_hotkey,
+ ask_for=[WO.NAME, WO.PATH],
+ )
+
+ logger.debug(
+ "args:\n"
+ f"network: {network}\n"
+ f"netuid: {netuid}\n"
+ f"mechanism_count: {mechanism_count}\n"
+ )
+
+ result, err_msg, ext_id = self._run_command(
+ subnet_mechanisms.set_mechanism_count(
+ wallet=wallet,
+ subtensor=subtensor,
+ netuid=netuid,
+ mechanism_count=mechanism_count,
+ previous_count=current_count or 0,
+ wait_for_inclusion=wait_for_inclusion,
+ wait_for_finalization=wait_for_finalization,
+ json_output=json_output,
+ )
+ )
+
+ if json_output:
+ json_console.print_json(
+ data={
+ "success": result,
+ "message": err_msg,
+ "extrinsic_identifier": ext_id,
+ }
+ )
+
+ return result
+
+ def mechanism_count_get(
+ self,
+ network: Optional[list[str]] = Options.network,
+ netuid: int = Options.netuid,
+ quiet: bool = Options.quiet,
+ verbose: bool = Options.verbose,
+ json_output: bool = Options.json_output,
+ ):
+ """
+ Display how many mechanisms are registered under a subnet.
+
+ Includes the base mechanism (index 0). Helpful for verifying the active
+ mechanism counts in a subnet.
+
+ [green]$[/green] btcli subnet mech count --netuid 12
+ """
+
+ self.verbosity_handler(quiet, verbose, json_output)
+ subtensor = self.initialize_chain(network)
+ return self._run_command(
+ subnet_mechanisms.count(
+ subtensor=subtensor,
+ netuid=netuid,
+ json_output=json_output,
+ )
+ )
+
+ def mechanism_emission_set(
+ self,
+ network: Optional[list[str]] = Options.network,
+ wallet_name: str = Options.wallet_name,
+ wallet_path: str = Options.wallet_path,
+ wallet_hotkey: str = Options.wallet_hotkey,
+ netuid: int = Options.netuid,
+ split: Optional[str] = typer.Option(
+ None,
+ "--split",
+ help="Comma-separated relative weights for each mechanism (normalised automatically).",
+ ),
+ wait_for_inclusion: bool = Options.wait_for_inclusion,
+ wait_for_finalization: bool = Options.wait_for_finalization,
+ prompt: bool = Options.prompt,
+ quiet: bool = Options.quiet,
+ verbose: bool = Options.verbose,
+ json_output: bool = Options.json_output,
+ ):
+ """
+ Update the emission split across mechanisms for a subnet.
+
+ Accepts comma-separated weights (U16 values or percentages). When `--split`
+ is omitted and prompts remain enabled, you will be guided interactively and
+ the CLI automatically normalises the weights.
+
+ [bold]Common Examples:[/bold]
+
+ 1. Configure the split interactively:
+ [green]$[/green] btcli subnet mech emissions-split --netuid 12
+
+ 2. Apply a 70/30 distribution in one command:
+ [green]$[/green] btcli subnet mech emissions-split --netuid 12 --split 70,30 --wallet.name my_wallet --wallet.hotkey admin
+ """
+
+ self.verbosity_handler(quiet, verbose, json_output)
+ subtensor = self.initialize_chain(network)
+ wallet = self.wallet_ask(
+ wallet_name,
+ wallet_path,
+ wallet_hotkey,
+ ask_for=[WO.NAME, WO.PATH],
+ )
+
+ return self._run_command(
+ subnet_mechanisms.set_emission_split(
+ subtensor=subtensor,
+ wallet=wallet,
+ netuid=netuid,
+ new_emission_split=split,
+ wait_for_inclusion=wait_for_inclusion,
+ wait_for_finalization=wait_for_finalization,
+ prompt=prompt,
+ json_output=json_output,
+ )
+ )
+
+ def mechanism_emission_get(
+ self,
+ network: Optional[list[str]] = Options.network,
+ netuid: int = Options.netuid,
+ quiet: bool = Options.quiet,
+ verbose: bool = Options.verbose,
+ json_output: bool = Options.json_output,
+ ):
+ """
+ Display the current emission split across mechanisms for a subnet.
+
+ Shows raw U16 weights alongside percentage shares for each mechanism. Useful
+ for verifying the emission split in a subnet.
+
+ [green]$[/green] btcli subnet mech emissions --netuid 12
+ """
+
+ self.verbosity_handler(quiet, verbose, json_output)
+ subtensor = self.initialize_chain(network)
+ return self._run_command(
+ subnet_mechanisms.get_emission_split(
+ subtensor=subtensor,
+ netuid=netuid,
+ json_output=json_output,
+ )
+ )
+
def sudo_set(
self,
network: Optional[list[str]] = Options.network,
@@ -5125,7 +5456,7 @@ def sudo_set(
f"param_name: {param_name}\n"
f"param_value: {param_value}"
)
- result, err_msg = self._run_command(
+ result, err_msg, ext_id = self._run_command(
sudo.sudo_set_hyperparameter(
wallet,
self.initialize_chain(network),
@@ -5137,7 +5468,15 @@ def sudo_set(
)
)
if json_output:
- json_console.print(json.dumps({"success": result, "err_msg": err_msg}))
+ json_console.print(
+ json.dumps(
+ {
+ "success": result,
+ "err_msg": err_msg,
+ "extrinsic_identifier": ext_id,
+ }
+ )
+ )
return result
def sudo_get(
@@ -5222,7 +5561,7 @@ def sudo_senate_vote(
None,
"--vote-aye/--vote-nay",
prompt="Enter y to vote Aye, or enter n to vote Nay",
- help="The vote casted on the proposal",
+ help="The vote cast on the proposal",
),
):
"""
@@ -5299,11 +5638,13 @@ def sudo_set_take(
)
raise typer.Exit()
logger.debug(f"args:\nnetwork: {network}\ntake: {take}")
- result = self._run_command(
+ result, ext_id = self._run_command(
sudo.set_take(wallet, self.initialize_chain(network), take)
)
if json_output:
- json_console.print(json.dumps({"success": result}))
+ json_console.print(
+ json.dumps({"success": result, "extrinsic_identifier": ext_id})
+ )
return result
def sudo_get_take(
@@ -5343,6 +5684,53 @@ def sudo_get_take(
sudo.display_current_take(self.initialize_chain(network), wallet)
)
+ def sudo_trim(
+ self,
+ network: Optional[list[str]] = Options.network,
+ wallet_name: Optional[str] = Options.wallet_name,
+ wallet_path: Optional[str] = Options.wallet_path,
+ wallet_hotkey: Optional[str] = Options.wallet_hotkey,
+ netuid: int = Options.netuid,
+ max_uids: int = typer.Option(
+ None,
+ "--max",
+ "--max-uids",
+ help="The maximum number of allowed uids to which to trim",
+ prompt="Max UIDs",
+ ),
+ quiet: bool = Options.quiet,
+ verbose: bool = Options.verbose,
+ json_output: bool = Options.json_output,
+ prompt: bool = Options.prompt,
+ period: int = Options.period,
+ ):
+ """
+ Allows subnet owners to trim UIDs on their subnet to a specified max number of netuids.
+
+ EXAMPLE
+ [green]$[/green] btcli sudo trim --netuid 95 --wallet-name my_wallet --wallet-hotkey my_hotkey --max 64
+ """
+ self.verbosity_handler(quiet, verbose, json_output)
+
+ wallet = self.wallet_ask(
+ wallet_name,
+ wallet_path,
+ wallet_hotkey,
+ ask_for=[WO.NAME, WO.PATH],
+ validate=WV.WALLET,
+ )
+ self._run_command(
+ sudo.trim(
+ subtensor=self.initialize_chain(network),
+ wallet=wallet,
+ netuid=netuid,
+ max_n=max_uids,
+ period=period,
+ json_output=json_output,
+ prompt=prompt,
+ )
+ )
+
def subnets_list(
self,
network: Optional[list[str]] = Options.network,
@@ -5512,6 +5900,7 @@ def subnets_show(
self,
network: Optional[list[str]] = Options.network,
netuid: int = Options.netuid,
+ mechanism_id: Optional[int] = Options.mechanism_id,
sort: bool = typer.Option(
False,
"--sort",
@@ -5523,18 +5912,43 @@ def subnets_show(
json_output: bool = Options.json_output,
):
"""
- Displays detailed information about a subnet including participants and their state.
+ Inspect the metagraph for a subnet.
- EXAMPLE
+ Shows miners, validators, stake, ranks, emissions, and other runtime stats.
+ When multiple mechanisms exist, the CLI prompts for one unless `--mechid`
+ is supplied. Netuid 0 always uses mechid 0.
+
+ [bold]Common Examples:[/bold]
+
+ 1. Inspect the mechanism with prompts for selection:
+ [green]$[/green] btcli subnets show --netuid 12
- [green]$[/green] btcli subnets show
+ 2. Pick mechanism 1 explicitly:
+ [green]$[/green] btcli subnets show --netuid 12 --mechid 1
"""
self.verbosity_handler(quiet, verbose, json_output)
subtensor = self.initialize_chain(network)
+ if netuid == 0:
+ mechanism_count = 1
+ selected_mechanism_id = 0
+ if mechanism_id not in (None, 0):
+ console.print(
+ "[dim]Mechanism selection ignored for the root subnet (only mechanism 0 exists).[/dim]"
+ )
+ else:
+ mechanism_count = self._run_command(
+ subtensor.get_subnet_mechanisms(netuid), exit_early=False
+ )
+ selected_mechanism_id = self.ask_subnet_mechanism(
+ mechanism_id, mechanism_count, netuid
+ )
+
return self._run_command(
subnets.show(
subtensor=subtensor,
netuid=netuid,
+ mechanism_id=selected_mechanism_id,
+ mechanism_count=mechanism_count,
sort=sort,
max_rows=None,
delegate_selection=False,
@@ -5820,13 +6234,15 @@ def subnets_set_identity(
logger.debug(
f"args:\nnetwork: {network}\nnetuid: {netuid}\nidentity: {identity}"
)
- success = self._run_command(
+ success, ext_id = self._run_command(
subnets.set_identity(
wallet, self.initialize_chain(network), netuid, identity, prompt
)
)
if json_output:
- json_console.print(json.dumps({"success": success}))
+ json_console.print(
+ json.dumps({"success": success, "extrinsic_identifier": ext_id})
+ )
def subnets_pow_register(
self,
@@ -6154,6 +6570,7 @@ def weights_reveal(
[green]$[/green] btcli wt reveal --netuid 1 --uids 1,2,3,4 --weights 0.1,0.2,0.3,0.4 --salt 163,241,217,11,161,142,147,189
"""
self.verbosity_handler(quiet, verbose, json_output)
+ # TODO think we need to ','.split uids and weights ?
uids = list_prompt(uids, int, "UIDs of interest for the specified netuid")
weights = list_prompt(
weights, float, "Corresponding weights for the specified UIDs"
diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py
index f93aed504..bc46bd485 100644
--- a/bittensor_cli/src/__init__.py
+++ b/bittensor_cli/src/__init__.py
@@ -666,6 +666,7 @@ class WalletValidationTypes(Enum):
"user_liquidity_enabled": ("toggle_user_liquidity", True),
"bonds_reset_enabled": ("sudo_set_bonds_reset_enabled", False),
"transfers_enabled": ("sudo_set_toggle_transfer", False),
+ "min_allowed_uids": ("sudo_set_min_allowed_uids", True),
}
HYPERPARAMS_MODULE = {
@@ -699,6 +700,10 @@ class WalletValidationTypes(Enum):
"GOVERNANCE": "Governance",
"TAKE": "Delegate take configuration",
},
+ "MECHANISMS": {
+ "CONFIG": "Mechanism Configuration",
+ "EMISSION": "Mechanism Emission",
+ },
"SUBNETS": {
"INFO": "Subnet Information",
"CREATION": "Subnet Creation & Management",
diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py
index 07fd8c906..ffd4ba3ff 100644
--- a/bittensor_cli/src/bittensor/chain_data.py
+++ b/bittensor_cli/src/bittensor/chain_data.py
@@ -13,6 +13,7 @@
u16_normalized_float as u16tf,
u64_normalized_float as u64tf,
decode_account_id,
+ get_netuid_and_subuid_by_storage_index,
)
@@ -1084,12 +1085,13 @@ class MetagraphInfo(InfoBase):
alpha_dividends_per_hotkey: list[
tuple[str, Balance]
] # List of dividend payout in alpha via subnet.
+ subuid: int = 0
@classmethod
def _fix_decoded(cls, decoded: dict) -> "MetagraphInfo":
"""Returns a MetagraphInfo object from decoded chain data."""
# Subnet index
- _netuid = decoded["netuid"]
+ _netuid, _subuid = get_netuid_and_subuid_by_storage_index(decoded["netuid"])
# Name and symbol
decoded.update({"name": bytes(decoded.get("name")).decode()})
@@ -1102,6 +1104,7 @@ def _fix_decoded(cls, decoded: dict) -> "MetagraphInfo":
return cls(
# Subnet index
netuid=_netuid,
+ subuid=_subuid,
# Name and symbol
name=decoded["name"],
symbol=decoded["symbol"],
diff --git a/bittensor_cli/src/bittensor/extrinsics/registration.py b/bittensor_cli/src/bittensor/extrinsics/registration.py
index 2c2371761..a32bc1c3d 100644
--- a/bittensor_cli/src/bittensor/extrinsics/registration.py
+++ b/bittensor_cli/src/bittensor/extrinsics/registration.py
@@ -18,6 +18,7 @@
from typing import Optional
import subprocess
+from async_substrate_interface import AsyncExtrinsicReceipt
from bittensor_wallet import Wallet
from Crypto.Hash import keccak
import numpy as np
@@ -40,6 +41,7 @@
unlock_key,
hex_to_bytes,
get_hotkey_pub_ss58,
+ print_extrinsic_id,
)
if typing.TYPE_CHECKING:
@@ -679,7 +681,7 @@ async def burned_register_extrinsic(
wait_for_inclusion: bool = True,
wait_for_finalization: bool = True,
era: Optional[int] = None,
-) -> tuple[bool, str]:
+) -> tuple[bool, str, Optional[str]]:
"""Registers the wallet to chain by recycling TAO.
:param subtensor: The SubtensorInterface object to use for the call, initialized
@@ -698,7 +700,7 @@ async def burned_register_extrinsic(
"""
if not (unlock_status := unlock_key(wallet, print_out=False)).success:
- return False, unlock_status.message
+ return False, unlock_status.message, None
with console.status(
f":satellite: Checking Account on [bold]subnet:{netuid}[/bold]...",
@@ -742,7 +744,7 @@ async def burned_register_extrinsic(
f"hotkey: [{COLOR_PALETTE.G.HK}]{neuron.hotkey}[/{COLOR_PALETTE.G.HK}]\n"
f"coldkey: [{COLOR_PALETTE.G.CK}]{neuron.coldkey}[/{COLOR_PALETTE.G.CK}]"
)
- return True, "Already registered"
+ return True, "Already registered", None
with console.status(
":satellite: Recycling TAO for Registration...", spinner="aesthetic"
@@ -755,16 +757,18 @@ async def burned_register_extrinsic(
"hotkey": get_hotkey_pub_ss58(wallet),
},
)
- success, err_msg = await subtensor.sign_and_send_extrinsic(
+ success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic(
call, wallet, wait_for_inclusion, wait_for_finalization, era=era_
)
if not success:
err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}")
await asyncio.sleep(0.5)
- return False, err_msg
+ return False, err_msg, None
# Successful registration, final check for neuron and pubkey
else:
+ ext_id = await ext_receipt.get_extrinsic_identifier()
+ await print_extrinsic_id(ext_receipt)
with console.status(":satellite: Checking Balance...", spinner="aesthetic"):
block_hash = await subtensor.substrate.get_chain_head()
new_balance, netuids_for_hotkey, my_uid = await asyncio.gather(
@@ -791,13 +795,13 @@ async def burned_register_extrinsic(
console.print(
f":white_heavy_check_mark: [green]Registered on netuid {netuid} with UID {my_uid}[/green]"
)
- return True, f"Registered on {netuid} with UID {my_uid}"
+ return True, f"Registered on {netuid} with UID {my_uid}", ext_id
else:
# neuron not found, try again
err_console.print(
":cross_mark: [red]Unknown error. Neuron not found.[/red]"
)
- return False, "Unknown error. Neuron not found."
+ return False, "Unknown error. Neuron not found.", ext_id
async def run_faucet_extrinsic(
@@ -1749,7 +1753,7 @@ async def swap_hotkey_extrinsic(
new_wallet: Wallet,
netuid: Optional[int] = None,
prompt: bool = False,
-) -> bool:
+) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]:
"""
Performs an extrinsic update for swapping two hotkeys on the chain
@@ -1770,14 +1774,14 @@ async def swap_hotkey_extrinsic(
err_console.print(
f":cross_mark: [red]Failed[/red]: Original hotkey {hk_ss58} is not registered on subnet {netuid}"
)
- return False
+ return False, None
elif not len(netuids_registered) > 0:
err_console.print(
f"Original hotkey [dark_orange]{hk_ss58}[/dark_orange] is not registered on any subnet. "
f"Please register and try again"
)
- return False
+ return False, None
if netuid is not None:
if netuid in netuids_registered_new_hotkey:
@@ -1785,17 +1789,17 @@ async def swap_hotkey_extrinsic(
f":cross_mark: [red]Failed[/red]: New hotkey {new_hk_ss58} "
f"is already registered on subnet {netuid}"
)
- return False
+ return False, None
else:
if len(netuids_registered_new_hotkey) > 0:
err_console.print(
f":cross_mark: [red]Failed[/red]: New hotkey {new_hk_ss58} "
f"is already registered on subnet(s) {netuids_registered_new_hotkey}"
)
- return False
+ return False, None
if not unlock_key(wallet).success:
- return False
+ return False, None
if prompt:
# Prompt user for confirmation.
@@ -1815,7 +1819,7 @@ async def swap_hotkey_extrinsic(
)
if not Confirm.ask(confirm_message):
- return False
+ return False, None
print_verbose(
f"Swapping {wallet.name}'s hotkey ({hk_ss58} - {wallet.hotkey_str}) with "
f"{new_wallet.name}'s hotkey ({new_hk_ss58} - {new_wallet.hotkey_str})"
@@ -1832,15 +1836,17 @@ async def swap_hotkey_extrinsic(
call_function="swap_hotkey",
call_params=call_params,
)
- success, err_msg = await subtensor.sign_and_send_extrinsic(call, wallet)
+ success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic(
+ call, wallet
+ )
if success:
console.print(
f"Hotkey {hk_ss58} ({wallet.hotkey_str}) swapped for new hotkey: "
f"{new_hk_ss58} ({new_wallet.hotkey_str})"
)
- return True
+ return True, ext_receipt
else:
err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}")
time.sleep(0.5)
- return False
+ return False, ext_receipt
diff --git a/bittensor_cli/src/bittensor/extrinsics/root.py b/bittensor_cli/src/bittensor/extrinsics/root.py
index 207fb8642..ea515ed1a 100644
--- a/bittensor_cli/src/bittensor/extrinsics/root.py
+++ b/bittensor_cli/src/bittensor/extrinsics/root.py
@@ -18,7 +18,7 @@
import asyncio
import hashlib
import time
-from typing import Union, List, TYPE_CHECKING
+from typing import Union, List, TYPE_CHECKING, Optional
from bittensor_wallet import Wallet, Keypair
import numpy as np
@@ -38,6 +38,7 @@
format_error_message,
unlock_key,
get_hotkey_pub_ss58,
+ print_extrinsic_id,
)
if TYPE_CHECKING:
@@ -291,7 +292,7 @@ async def root_register_extrinsic(
wallet: Wallet,
wait_for_inclusion: bool = True,
wait_for_finalization: bool = True,
-) -> tuple[bool, str]:
+) -> tuple[bool, str, Optional[str]]:
r"""Registers the wallet to root network.
:param subtensor: The SubtensorInterface object
@@ -307,7 +308,7 @@ async def root_register_extrinsic(
"""
if not (unlock := unlock_key(wallet)).success:
- return False, unlock.message
+ return False, unlock.message, None
print_verbose(f"Checking if hotkey ({wallet.hotkey_str}) is registered on root")
is_registered = await is_hotkey_registered(
@@ -317,7 +318,7 @@ async def root_register_extrinsic(
console.print(
":white_heavy_check_mark: [green]Already registered on root network.[/green]"
)
- return True, "Already registered on root network"
+ return True, "Already registered on root network", None
with console.status(":satellite: Registering to root network...", spinner="earth"):
call = await subtensor.substrate.compose_call(
@@ -325,7 +326,7 @@ async def root_register_extrinsic(
call_function="root_register",
call_params={"hotkey": get_hotkey_pub_ss58(wallet)},
)
- success, err_msg = await subtensor.sign_and_send_extrinsic(
+ success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic(
call,
wallet=wallet,
wait_for_inclusion=wait_for_inclusion,
@@ -335,10 +336,12 @@ async def root_register_extrinsic(
if not success:
err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}")
await asyncio.sleep(0.5)
- return False, err_msg
+ return False, err_msg, None
# Successful registration, final check for neuron and pubkey
else:
+ ext_id = await ext_receipt.get_extrinsic_identifier()
+ await print_extrinsic_id(ext_receipt)
uid = await subtensor.query(
module="SubtensorModule",
storage_function="Uids",
@@ -348,13 +351,13 @@ async def root_register_extrinsic(
console.print(
f":white_heavy_check_mark: [green]Registered with UID {uid}[/green]"
)
- return True, f"Registered with UID {uid}"
+ return True, f"Registered with UID {uid}", ext_id
else:
# neuron not found, try again
err_console.print(
":cross_mark: [red]Unknown error. Neuron not found.[/red]"
)
- return False, "Unknown error. Neuron not found."
+ return False, "Unknown error. Neuron not found.", ext_id
async def set_root_weights_extrinsic(
diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py
index ad3168a23..f71e747ff 100644
--- a/bittensor_cli/src/bittensor/extrinsics/transfer.py
+++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py
@@ -1,10 +1,11 @@
import asyncio
+from typing import Optional, Union
+from async_substrate_interface import AsyncExtrinsicReceipt
from bittensor_wallet import Wallet
from rich.prompt import Confirm
from async_substrate_interface.errors import SubstrateRequestException
-from bittensor_cli.src import NETWORK_EXPLORER_MAP
from bittensor_cli.src.bittensor.balances import Balance
from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface
from bittensor_cli.src.bittensor.utils import (
@@ -12,7 +13,6 @@
err_console,
print_verbose,
format_error_message,
- get_explorer_url_for_network,
is_valid_bittensor_address_or_public_key,
print_error,
unlock_key,
@@ -30,7 +30,7 @@ async def transfer_extrinsic(
wait_for_inclusion: bool = True,
wait_for_finalization: bool = False,
prompt: bool = False,
-) -> bool:
+) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]:
"""Transfers funds from this wallet to the destination public key address.
:param subtensor: initialized SubtensorInterface object used for transfer
@@ -75,7 +75,7 @@ async def get_transfer_fee() -> Balance:
return Balance.from_rao(payment_info["partial_fee"])
- async def do_transfer() -> tuple[bool, str, str]:
+ async def do_transfer() -> tuple[bool, str, str, AsyncExtrinsicReceipt]:
"""
Makes transfer from wallet to destination public key address.
:return: success, block hash, formatted error message
@@ -95,27 +95,32 @@ async def do_transfer() -> tuple[bool, str, str]:
)
# We only wait here if we expect finalization.
if not wait_for_finalization and not wait_for_inclusion:
- return True, "", ""
+ return True, "", "", response
# Otherwise continue with finalization.
if await response.is_success:
block_hash_ = response.block_hash
- return True, block_hash_, ""
+ return True, block_hash_, "", response
else:
- return False, "", format_error_message(await response.error_message)
+ return (
+ False,
+ "",
+ format_error_message(await response.error_message),
+ response,
+ )
# Validate destination address.
if not is_valid_bittensor_address_or_public_key(destination):
err_console.print(
f":cross_mark: [red]Invalid destination SS58 address[/red]:[bold white]\n {destination}[/bold white]"
)
- return False
+ return False, None
console.print(f"[dark_orange]Initiating transfer on network: {subtensor.network}")
# Unlock wallet coldkey.
if not unlock_key(wallet).success:
- return False
+ return False, None
- call_params = {"dest": destination}
+ call_params: dict[str, Optional[Union[str, int]]] = {"dest": destination}
if transfer_all:
call_function = "transfer_all"
if allow_death:
@@ -158,7 +163,7 @@ async def do_transfer() -> tuple[bool, str, str]:
f" would bring you under the existential deposit: [bright_cyan]{existential_deposit}[/bright_cyan].\n"
f"You can try again with `--allow-death`."
)
- return False
+ return False, None
elif account_balance < (amount + fee) and allow_death:
print_error(
":cross_mark: [bold red]Not enough balance[/bold red]:\n\n"
@@ -166,7 +171,7 @@ async def do_transfer() -> tuple[bool, str, str]:
f" amount: [bright_red]{amount}[/bright_red]\n"
f" for fee: [bright_red]{fee}[/bright_red]"
)
- return False
+ return False, None
# Ask before moving on.
if prompt:
@@ -176,30 +181,18 @@ async def do_transfer() -> tuple[bool, str, str]:
f" from: [light_goldenrod2]{wallet.name}[/light_goldenrod2] : "
f"[bright_magenta]{wallet.coldkey.ss58_address}\n[/bright_magenta]"
f" to: [bright_magenta]{destination}[/bright_magenta]\n for fee: [bright_cyan]{fee}[/bright_cyan]\n"
- f":warning:[bright_yellow]Transferring is not the same as staking. To instead stake, use "
- f"[dark_orange]btcli stake add[/dark_orange] instead[/bright_yellow]:warning:"
+ f"[bright_yellow]Transferring is not the same as staking. To instead stake, use "
+ f"[dark_orange]btcli stake add[/dark_orange] instead[/bright_yellow]"
):
- return False
+ return False, None
- with console.status(":satellite: Transferring...", spinner="earth") as status:
- success, block_hash, err_msg = await do_transfer()
+ with console.status(":satellite: Transferring...", spinner="earth"):
+ success, block_hash, err_msg, ext_receipt = await do_transfer()
if success:
console.print(":white_heavy_check_mark: [green]Finalized[/green]")
console.print(f"[green]Block Hash: {block_hash}[/green]")
- if subtensor.network == "finney":
- print_verbose("Fetching explorer URLs", status)
- explorer_urls = get_explorer_url_for_network(
- subtensor.network, block_hash, NETWORK_EXPLORER_MAP
- )
- if explorer_urls != {} and explorer_urls:
- console.print(
- f"[green]Opentensor Explorer Link: {explorer_urls.get('opentensor')}[/green]"
- )
- console.print(
- f"[green]Taostats Explorer Link: {explorer_urls.get('taostats')}[/green]"
- )
else:
console.print(f":cross_mark: [red]Failed[/red]: {err_msg}")
@@ -212,6 +205,6 @@ async def do_transfer() -> tuple[bool, str, str]:
f"Balance:\n"
f" [blue]{account_balance}[/blue] :arrow_right: [green]{new_balance}[/green]"
)
- return True
+ return True, ext_receipt
- return False
+ return False, None
diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py
index 27df7d94c..d37d5f3db 100644
--- a/bittensor_cli/src/bittensor/subtensor_interface.py
+++ b/bittensor_cli/src/bittensor/subtensor_interface.py
@@ -4,6 +4,7 @@
from typing import Optional, Any, Union, TypedDict, Iterable
import aiohttp
+from async_substrate_interface import AsyncExtrinsicReceipt
from async_substrate_interface.async_substrate import (
DiskCachedAsyncSubstrateInterface,
AsyncSubstrateInterface,
@@ -1081,7 +1082,7 @@ async def sign_and_send_extrinsic(
wait_for_inclusion: bool = True,
wait_for_finalization: bool = False,
era: Optional[dict[str, int]] = None,
- ) -> tuple[bool, str]:
+ ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]:
"""
Helper method to sign and submit an extrinsic call to chain.
@@ -1093,7 +1094,10 @@ async def sign_and_send_extrinsic(
:return: (success, error message)
"""
- call_args = {"call": call, "keypair": wallet.coldkey}
+ call_args: dict[str, Union[GenericCall, Keypair, dict[str, int]]] = {
+ "call": call,
+ "keypair": wallet.coldkey,
+ }
if era is not None:
call_args["era"] = era
extrinsic = await self.substrate.create_signed_extrinsic(
@@ -1107,13 +1111,13 @@ async def sign_and_send_extrinsic(
)
# We only wait here if we expect finalization.
if not wait_for_finalization and not wait_for_inclusion:
- return True, ""
+ return True, "", response
if await response.is_success:
- return True, ""
+ return True, "", response
else:
- return False, format_error_message(await response.error_message)
+ return False, format_error_message(await response.error_message), None
except SubstrateRequestException as e:
- return False, format_error_message(e)
+ return False, format_error_message(e), None
async def get_children(self, hotkey, netuid) -> tuple[bool, list, str]:
"""
@@ -1171,6 +1175,55 @@ async def get_subnet_hyperparameters(
return SubnetHyperparameters.from_any(result)
+ async def get_subnet_mechanisms(
+ self, netuid: int, block_hash: Optional[str] = None
+ ) -> int:
+ """Return the number of mechanisms that belong to the provided subnet."""
+
+ result = await self.query(
+ module="SubtensorModule",
+ storage_function="MechanismCountCurrent",
+ params=[netuid],
+ block_hash=block_hash,
+ )
+
+ if result is None:
+ return 0
+ return int(result)
+
+ async def get_all_subnet_mechanisms(
+ self, block_hash: Optional[str] = None
+ ) -> dict[int, int]:
+ """Return mechanism counts for every subnet with a recorded value."""
+
+ results = await self.substrate.query_map(
+ module="SubtensorModule",
+ storage_function="MechanismCountCurrent",
+ params=[],
+ block_hash=block_hash,
+ )
+ res = {}
+ async for netuid, count in results:
+ res[int(netuid)] = int(count.value)
+ return res
+
+ async def get_mechanism_emission_split(
+ self, netuid: int, block_hash: Optional[str] = None
+ ) -> list[int]:
+ """Return the emission split configured for the provided subnet."""
+
+ result = await self.query(
+ module="SubtensorModule",
+ storage_function="MechanismEmissionSplit",
+ params=[netuid],
+ block_hash=block_hash,
+ )
+
+ if not result:
+ return []
+
+ return [int(value) for value in result]
+
async def burn_cost(self, block_hash: Optional[str] = None) -> Optional[Balance]:
result = await self.query_runtime_api(
runtime_api="SubnetRegistrationRuntimeApi",
@@ -1296,37 +1349,51 @@ async def get_stake_for_coldkey_and_hotkey_on_netuid(
else:
return Balance.from_rao(fixed_to_float(_result)).set_unit(int(netuid))
+ async def get_mechagraph_info(
+ self, netuid: int, mech_id: int, block_hash: Optional[str] = None
+ ) -> Optional[MetagraphInfo]:
+ """
+ Returns the metagraph info for a given subnet and mechanism id.
+ And yes, it is indeed 'mecha'graph
+ """
+ query = await self.query_runtime_api(
+ runtime_api="SubnetInfoRuntimeApi",
+ method="get_mechagraph",
+ params=[netuid, mech_id],
+ block_hash=block_hash,
+ )
+
+ if query is None:
+ return None
+
+ return MetagraphInfo.from_any(query)
+
async def get_metagraph_info(
self, netuid: int, block_hash: Optional[str] = None
) -> Optional[MetagraphInfo]:
- hex_bytes_result = await self.query_runtime_api(
+ query = await self.query_runtime_api(
runtime_api="SubnetInfoRuntimeApi",
method="get_metagraph",
params=[netuid],
block_hash=block_hash,
)
- if hex_bytes_result is None:
+ if query is None:
return None
- try:
- bytes_result = bytes.fromhex(hex_bytes_result[2:])
- except ValueError:
- bytes_result = bytes.fromhex(hex_bytes_result)
-
- return MetagraphInfo.from_any(bytes_result)
+ return MetagraphInfo.from_any(query)
async def get_all_metagraphs_info(
self, block_hash: Optional[str] = None
) -> list[MetagraphInfo]:
- hex_bytes_result = await self.query_runtime_api(
+ query = await self.query_runtime_api(
runtime_api="SubnetInfoRuntimeApi",
method="get_all_metagraphs",
params=[],
block_hash=block_hash,
)
- return MetagraphInfo.list_from_any(hex_bytes_result)
+ return MetagraphInfo.list_from_any(query)
async def multi_get_stake_for_coldkey_and_hotkey_on_netuid(
self,
diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py
index 80aab6916..c8be33563 100644
--- a/bittensor_cli/src/bittensor/utils.py
+++ b/bittensor_cli/src/bittensor/utils.py
@@ -10,6 +10,7 @@
from functools import partial
import re
+from async_substrate_interface import AsyncExtrinsicReceipt
from bittensor_wallet import Wallet, Keypair
from bittensor_wallet.utils import SS58_FORMAT
from bittensor_wallet.errors import KeyFileError, PasswordError
@@ -34,6 +35,7 @@
BT_DOCS_LINK = "https://docs.learnbittensor.org"
+GLOBAL_MAX_SUBNET_COUNT = 4096
console = Console()
json_console = Console()
@@ -507,6 +509,7 @@ def get_explorer_url_for_network(
:return: The explorer url for the given block hash and network
"""
+ # TODO remove
explorer_urls: dict[str, str] = {}
# Will be None if the network is not known. i.e. not in network_map
@@ -1462,3 +1465,50 @@ def get_hotkey_pub_ss58(wallet: Wallet) -> str:
return wallet.hotkeypub.ss58_address
except (KeyFileError, AttributeError):
return wallet.hotkey.ss58_address
+
+
+def get_netuid_and_subuid_by_storage_index(storage_index: int) -> tuple[int, int]:
+ """Returns the netuid and subuid from the storage index.
+
+ Chain APIs (e.g., SubMetagraph response) returns netuid which is storage index that encodes both the netuid and
+ subuid. This function reverses the encoding to extract these components.
+
+ Parameters:
+ storage_index: The storage index of the subnet.
+
+ Returns:
+ tuple[int, int]:
+ - netuid subnet identifier.
+ - subuid identifier.
+ """
+ return (
+ storage_index % GLOBAL_MAX_SUBNET_COUNT,
+ storage_index // GLOBAL_MAX_SUBNET_COUNT,
+ )
+
+
+async def print_extrinsic_id(
+ extrinsic_receipt: Optional[AsyncExtrinsicReceipt],
+) -> None:
+ """
+ Prints the extrinsic identifier to the console. If the substrate attached to the extrinsic receipt is on a finney
+ node, it will also include a link to browse the extrinsic in tao dot app.
+ Args:
+ extrinsic_receipt: AsyncExtrinsicReceipt object from a successful extrinsic submission.
+ """
+ if extrinsic_receipt is None:
+ return
+ substrate = extrinsic_receipt.substrate
+ ext_id = await extrinsic_receipt.get_extrinsic_identifier()
+ if substrate:
+ query = await substrate.rpc_request("system_chainType", [])
+ if query.get("result") == "Live":
+ console.print(
+ f":white_heavy_check_mark:Your extrinsic has been included as {ext_id}: "
+ f"[blue]https://tao.app/extrinsic/{ext_id}[/blue]"
+ )
+ return
+ console.print(
+ f":white_heavy_check_mark:Your extrinsic has been included as {ext_id}"
+ )
+ return
diff --git a/bittensor_cli/src/commands/liquidity/liquidity.py b/bittensor_cli/src/commands/liquidity/liquidity.py
index 60f5c6529..cc25ff1e9 100644
--- a/bittensor_cli/src/commands/liquidity/liquidity.py
+++ b/bittensor_cli/src/commands/liquidity/liquidity.py
@@ -2,6 +2,7 @@
import json
from typing import TYPE_CHECKING, Optional
+from async_substrate_interface import AsyncExtrinsicReceipt
from rich.prompt import Confirm
from rich.table import Column, Table
@@ -11,6 +12,7 @@
console,
err_console,
json_console,
+ print_extrinsic_id,
)
from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float
from bittensor_cli.src.commands.liquidity.utils import (
@@ -36,7 +38,7 @@ async def add_liquidity_extrinsic(
price_high: Balance,
wait_for_inclusion: bool = True,
wait_for_finalization: bool = False,
-) -> tuple[bool, str]:
+) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]:
"""
Adds liquidity to the specified price range.
@@ -60,7 +62,7 @@ async def add_liquidity_extrinsic(
`toggle_user_liquidity_extrinsic` to enable/disable user liquidity.
"""
if not (unlock := unlock_key(wallet)).success:
- return False, unlock.message
+ return False, unlock.message, None
tick_low = price_to_tick(price_low.tao)
tick_high = price_to_tick(price_high.tao)
@@ -94,7 +96,7 @@ async def modify_liquidity_extrinsic(
liquidity_delta: Balance,
wait_for_inclusion: bool = True,
wait_for_finalization: bool = False,
-) -> tuple[bool, str]:
+) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]:
"""Modifies liquidity in liquidity position by adding or removing liquidity from it.
Arguments:
@@ -116,7 +118,7 @@ async def modify_liquidity_extrinsic(
Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity.
"""
if not (unlock := unlock_key(wallet)).success:
- return False, unlock.message
+ return False, unlock.message, None
call = await subtensor.substrate.compose_call(
call_module="Swap",
@@ -145,7 +147,7 @@ async def remove_liquidity_extrinsic(
position_id: int,
wait_for_inclusion: bool = True,
wait_for_finalization: bool = False,
-) -> tuple[bool, str]:
+) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]:
"""Remove liquidity and credit balances back to wallet's hotkey stake.
Arguments:
@@ -166,7 +168,7 @@ async def remove_liquidity_extrinsic(
Call `toggle_user_liquidity_extrinsic` to enable/disable user liquidity.
"""
if not (unlock := unlock_key(wallet)).success:
- return False, unlock.message
+ return False, unlock.message, None
call = await subtensor.substrate.compose_call(
call_module="Swap",
@@ -193,7 +195,7 @@ async def toggle_user_liquidity_extrinsic(
enable: bool,
wait_for_inclusion: bool = True,
wait_for_finalization: bool = False,
-) -> tuple[bool, str]:
+) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]:
"""Allow to toggle user liquidity for specified subnet.
Arguments:
@@ -210,7 +212,7 @@ async def toggle_user_liquidity_extrinsic(
- False and an error message if the submission fails or the wallet cannot be unlocked.
"""
if not (unlock := unlock_key(wallet)).success:
- return False, unlock.message
+ return False, unlock.message, None
call = await subtensor.substrate.compose_call(
call_module="Swap",
@@ -232,16 +234,16 @@ async def add_liquidity(
wallet: "Wallet",
hotkey_ss58: str,
netuid: Optional[int],
- liquidity: Optional[float],
- price_low: Optional[float],
- price_high: Optional[float],
+ liquidity: Balance,
+ price_low: Balance,
+ price_high: Balance,
prompt: bool,
json_output: bool,
) -> tuple[bool, str]:
"""Add liquidity position to provided subnet."""
# Check wallet access
- if not unlock_key(wallet).success:
- return False
+ if not (ulw := unlock_key(wallet)).success:
+ return False, ulw.message
# Check that the subnet exists.
if not await subtensor.subnet_exists(netuid=netuid):
@@ -260,7 +262,7 @@ async def add_liquidity(
if not Confirm.ask("Would you like to continue?"):
return False, "User cancelled operation."
- success, message = await add_liquidity_extrinsic(
+ success, message, ext_receipt = await add_liquidity_extrinsic(
subtensor=subtensor,
wallet=wallet,
hotkey_ss58=hotkey_ss58,
@@ -269,8 +271,14 @@ async def add_liquidity(
price_low=price_low,
price_high=price_high,
)
+ await print_extrinsic_id(ext_receipt)
+ ext_id = await ext_receipt.get_extrinsic_identifier()
if json_output:
- json_console.print(json.dumps({"success": success, "message": message}))
+ json_console.print(
+ json.dumps(
+ {"success": success, "message": message, "extrinsic_identifier": ext_id}
+ )
+ )
else:
if success:
console.print(
@@ -278,6 +286,7 @@ async def add_liquidity(
)
else:
err_console.print(f"[red]Error: {message}[/red]")
+ return success, message
async def get_liquidity_list(
@@ -535,11 +544,12 @@ async def remove_liquidity(
success, msg, positions = await get_liquidity_list(subtensor, wallet, netuid)
if not success:
if json_output:
- return json_console.print(
- {"success": False, "err_msg": msg, "positions": positions}
+ json_console.print_json(
+ data={"success": False, "err_msg": msg, "positions": positions}
)
else:
return err_console.print(f"Error: {msg}")
+ return False, msg
else:
position_ids = [p.id for p in positions]
else:
@@ -568,16 +578,21 @@ async def remove_liquidity(
]
)
if not json_output:
- for (success, msg), posid in zip(results, position_ids):
+ for (success, msg, ext_receipt), posid in zip(results, position_ids):
if success:
+ await print_extrinsic_id(ext_receipt)
console.print(f"[green] Position {posid} has been removed.")
else:
err_console.print(f"[red] Error removing {posid}: {msg}")
else:
json_table = {}
- for (success, msg), posid in zip(results, position_ids):
- json_table[posid] = {"success": success, "err_msg": msg}
- json_console.print(json.dumps(json_table))
+ for (success, msg, ext_receipt), posid in zip(results, position_ids):
+ json_table[posid] = {
+ "success": success,
+ "err_msg": msg,
+ "extrinsic_identifier": await ext_receipt.get_extrinsic_identifier(),
+ }
+ json_console.print_json(data=json_table)
async def modify_liquidity(
@@ -586,7 +601,7 @@ async def modify_liquidity(
hotkey_ss58: str,
netuid: int,
position_id: int,
- liquidity_delta: Optional[float],
+ liquidity_delta: Balance,
prompt: Optional[bool] = None,
json_output: bool = False,
) -> bool:
@@ -611,7 +626,7 @@ async def modify_liquidity(
if not Confirm.ask("Would you like to continue?"):
return False
- success, msg = await modify_liquidity_extrinsic(
+ success, msg, ext_receipt = await modify_liquidity_extrinsic(
subtensor=subtensor,
wallet=wallet,
hotkey_ss58=hotkey_ss58,
@@ -620,9 +635,14 @@ async def modify_liquidity(
liquidity_delta=liquidity_delta,
)
if json_output:
- json_console.print(json.dumps({"success": success, "err_msg": msg}))
+ ext_id = await ext_receipt.get_extrinsic_identifier() if success else None
+ json_console.print_json(
+ data={"success": success, "err_msg": msg, "extrinsic_identifier": ext_id}
+ )
else:
if success:
+ await print_extrinsic_id(ext_receipt)
console.print(f"[green] Position {position_id} has been modified.")
else:
err_console.print(f"[red] Error modifying {position_id}: {msg}")
+ return success
diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py
index 18a507c6d..9f3ffc0d0 100644
--- a/bittensor_cli/src/commands/stake/add.py
+++ b/bittensor_cli/src/commands/stake/add.py
@@ -4,6 +4,8 @@
from functools import partial
from typing import TYPE_CHECKING, Optional
+
+from async_substrate_interface import AsyncExtrinsicReceipt
from rich.table import Table
from rich.prompt import Confirm, Prompt
@@ -21,6 +23,7 @@
unlock_key,
json_console,
get_hotkey_pub_ss58,
+ print_extrinsic_id,
)
from bittensor_wallet import Wallet
@@ -112,7 +115,7 @@ async def safe_stake_extrinsic(
hotkey_ss58_: str,
price_limit: Balance,
status=None,
- ) -> tuple[bool, str]:
+ ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]:
err_out = partial(print_error, status=status)
failure_prelude = (
f":cross_mark: [red]Failed[/red] to stake {amount_} on Netuid {netuid_}"
@@ -153,15 +156,16 @@ async def safe_stake_extrinsic(
else:
err_msg = f"{failure_prelude} with error: {format_error_message(e)}"
err_out("\n" + err_msg)
- return False, err_msg
+ return False, err_msg, None
if not await response.is_success:
err_msg = f"{failure_prelude} with error: {format_error_message(await response.error_message)}"
err_out("\n" + err_msg)
- return False, err_msg
+ return False, err_msg, None
else:
if json_output:
# the rest of this checking is not necessary if using json_output
- return True, ""
+ return True, "", response
+ await print_extrinsic_id(response)
block_hash = await subtensor.substrate.get_chain_head()
new_balance, new_stake = await asyncio.gather(
subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash),
@@ -199,11 +203,11 @@ async def safe_stake_extrinsic(
f":arrow_right: "
f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n"
)
- return True, ""
+ return True, "", response
async def stake_extrinsic(
netuid_i, amount_, current, staking_address_ss58, status=None
- ) -> tuple[bool, str]:
+ ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]:
err_out = partial(print_error, status=status)
current_balance, next_nonce, call = await asyncio.gather(
subtensor.get_balance(wallet.coldkeypub.ss58_address),
@@ -231,16 +235,17 @@ async def stake_extrinsic(
except SubstrateRequestException as e:
err_msg = f"{failure_prelude} with error: {format_error_message(e)}"
err_out("\n" + err_msg)
- return False, err_msg
+ return False, err_msg, None
else:
if not await response.is_success:
err_msg = f"{failure_prelude} with error: {format_error_message(await response.error_message)}"
err_out("\n" + err_msg)
- return False, err_msg
+ return False, err_msg, None
else:
if json_output:
# the rest of this is not necessary if using json_output
- return True, ""
+ return True, "", response
+ await print_extrinsic_id(response)
new_block_hash = await subtensor.substrate.get_chain_head()
new_balance, new_stake = await asyncio.gather(
subtensor.get_balance(
@@ -269,7 +274,7 @@ async def stake_extrinsic(
f":arrow_right: "
f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}\n"
)
- return True, ""
+ return True, "", response
netuids = (
netuids if netuids is not None else await subtensor.get_all_subnet_netuids()
@@ -470,15 +475,23 @@ async def stake_extrinsic(
}
successes = defaultdict(dict)
error_messages = defaultdict(dict)
+ extrinsic_ids = defaultdict(dict)
with console.status(f"\n:satellite: Staking on netuid(s): {netuids} ..."):
# We can gather them all at once but balance reporting will be in race-condition.
for (ni, staking_address), coroutine in stake_coroutines.items():
- success, er_msg = await coroutine
+ success, er_msg, ext_receipt = await coroutine
successes[ni][staking_address] = success
error_messages[ni][staking_address] = er_msg
+ extrinsic_ids[ni][
+ staking_address
+ ] = await ext_receipt.get_extrinsic_identifier()
if json_output:
- json_console.print(
- json.dumps({"staking_success": successes, "error_messages": error_messages})
+ json_console.print_json(
+ data={
+ "staking_success": successes,
+ "error_messages": error_messages,
+ "extrinsic_ids": extrinsic_ids,
+ }
)
diff --git a/bittensor_cli/src/commands/stake/children_hotkeys.py b/bittensor_cli/src/commands/stake/children_hotkeys.py
index d01e8d147..d50ecc65a 100644
--- a/bittensor_cli/src/commands/stake/children_hotkeys.py
+++ b/bittensor_cli/src/commands/stake/children_hotkeys.py
@@ -22,6 +22,7 @@
unlock_key,
json_console,
get_hotkey_pub_ss58,
+ print_extrinsic_id,
)
@@ -59,7 +60,7 @@ async def set_children_extrinsic(
wait_for_inclusion: bool = True,
wait_for_finalization: bool = False,
prompt: bool = False,
-) -> tuple[bool, str]:
+) -> tuple[bool, str, Optional[str]]:
"""
Sets children hotkeys with proportions assigned from the parent.
@@ -74,7 +75,7 @@ async def set_children_extrinsic(
`True`, or returns `False` if the extrinsic fails to be finalized within the timeout.
:param: prompt: If `True`, the call waits for confirmation from the user before proceeding.
- :return: A tuple containing a success flag and an optional error message.
+ :return: A tuple containing a success flag, an optional error message, and the extrinsic identifier
"""
# Check if all children are being revoked
all_revoked = len(children_with_proportions) == 0
@@ -87,7 +88,7 @@ async def set_children_extrinsic(
if not Confirm.ask(
f"Do you want to revoke all children hotkeys for hotkey {hotkey} on netuid {netuid}?"
):
- return False, "Operation Cancelled"
+ return False, "Operation Cancelled", None
else:
if not Confirm.ask(
"Do you want to set children hotkeys:\n[bold white]{}[/bold white]?".format(
@@ -97,11 +98,11 @@ async def set_children_extrinsic(
)
)
):
- return False, "Operation Cancelled"
+ return False, "Operation Cancelled", None
# Decrypt coldkey.
if not (unlock_status := unlock_key(wallet, print_out=False)).success:
- return False, unlock_status.message
+ return False, unlock_status.message, ""
with console.status(
f":satellite: {operation} on [white]{subtensor.network}[/white] ..."
@@ -120,7 +121,7 @@ async def set_children_extrinsic(
"netuid": netuid,
},
)
- success, error_message = await subtensor.sign_and_send_extrinsic(
+ success, error_message, ext_receipt = await subtensor.sign_and_send_extrinsic(
call, wallet, wait_for_inclusion, wait_for_finalization
)
@@ -128,17 +129,20 @@ async def set_children_extrinsic(
return (
True,
f"Not waiting for finalization or inclusion. {operation} initiated.",
+ None,
)
if success:
- if wait_for_inclusion:
- console.print(":white_heavy_check_mark: [green]Included[/green]")
+ ext_id = await ext_receipt.get_extrinsic_identifier()
+ await print_extrinsic_id(ext_receipt)
+ modifier = "included"
if wait_for_finalization:
console.print(":white_heavy_check_mark: [green]Finalized[/green]")
- return True, f"Successfully {operation.lower()} and Finalized."
+ modifier = "finalized"
+ return True, f"{operation} successfully {modifier}.", ext_id
else:
err_console.print(f":cross_mark: [red]Failed[/red]: {error_message}")
- return False, error_message
+ return False, error_message, None
async def set_childkey_take_extrinsic(
@@ -150,7 +154,7 @@ async def set_childkey_take_extrinsic(
wait_for_inclusion: bool = True,
wait_for_finalization: bool = False,
prompt: bool = True,
-) -> tuple[bool, str]:
+) -> tuple[bool, str, Optional[str]]:
"""
Sets childkey take.
@@ -165,7 +169,7 @@ async def set_childkey_take_extrinsic(
`True`, or returns `False` if the extrinsic fails to be finalized within the timeout.
:param: prompt: If `True`, the call waits for confirmation from the user before proceeding.
- :return: A tuple containing a success flag and an optional error message.
+ :return: A tuple containing a success flag, an optional error message, and an optional extrinsic identifier
"""
# Ask before moving on.
@@ -173,11 +177,11 @@ async def set_childkey_take_extrinsic(
if not Confirm.ask(
f"Do you want to set childkey take to: [bold white]{take * 100}%[/bold white]?"
):
- return False, "Operation Cancelled"
+ return False, "Operation Cancelled", None
# Decrypt coldkey.
if not (unlock_status := unlock_key(wallet, print_out=False)).success:
- return False, unlock_status.message
+ return False, unlock_status.message, None
with console.status(
f":satellite: Setting childkey take on [white]{subtensor.network}[/white] ..."
@@ -186,7 +190,7 @@ async def set_childkey_take_extrinsic(
if 0 <= take <= 0.18:
take_u16 = float_to_u16(take)
else:
- return False, "Invalid take value"
+ return False, "Invalid take value", None
call = await subtensor.substrate.compose_call(
call_module="SubtensorModule",
@@ -197,7 +201,11 @@ async def set_childkey_take_extrinsic(
"netuid": netuid,
},
)
- success, error_message = await subtensor.sign_and_send_extrinsic(
+ (
+ success,
+ error_message,
+ ext_receipt,
+ ) = await subtensor.sign_and_send_extrinsic(
call, wallet, wait_for_inclusion, wait_for_finalization
)
@@ -205,30 +213,34 @@ async def set_childkey_take_extrinsic(
return (
True,
"Not waiting for finalization or inclusion. Set childkey take initiated.",
+ None,
)
if success:
- if wait_for_inclusion:
- console.print(":white_heavy_check_mark: [green]Included[/green]")
+ ext_id = await ext_receipt.get_extrinsic_identifier()
+ await print_extrinsic_id(ext_receipt)
+ modifier = "included"
if wait_for_finalization:
+ modifier = "finalized"
console.print(":white_heavy_check_mark: [green]Finalized[/green]")
# bittensor.logging.success(
# prefix="Setting childkey take",
# suffix="Finalized: " + str(success),
# )
- return True, "Successfully set childkey take and Finalized."
+ return True, f"Successfully {modifier} childkey take", ext_id
else:
console.print(f":cross_mark: [red]Failed[/red]: {error_message}")
# bittensor.logging.warning(
# prefix="Setting childkey take",
# suffix="Failed: " + str(error_message),
# )
- return False, error_message
+ return False, error_message, None
except SubstrateRequestException as e:
return (
False,
f"Exception occurred while setting childkey take: {format_error_message(e)}",
+ None,
)
@@ -519,7 +531,7 @@ async def set_children(
children_with_proportions = list(zip(proportions, children))
successes = {}
if netuid is not None:
- success, message = await set_children_extrinsic(
+ success, message, ext_id = await set_children_extrinsic(
subtensor=subtensor,
wallet=wallet,
netuid=netuid,
@@ -534,6 +546,7 @@ async def set_children(
"error": message,
"completion_block": None,
"set_block": None,
+ "extrinsic_identifier": ext_id,
}
# Result
if success:
@@ -561,7 +574,7 @@ async def set_children(
if netuid_ == 0: # dont include root network
continue
console.print(f"Setting children on netuid {netuid_}.")
- success, message = await set_children_extrinsic(
+ success, message, ext_id = await set_children_extrinsic(
subtensor=subtensor,
wallet=wallet,
netuid=netuid_,
@@ -579,6 +592,7 @@ async def set_children(
"error": message,
"completion_block": completion_block,
"set_block": current_block,
+ "extrinsic_identifier": ext_id,
}
console.print(
f"Your childkey request for netuid {netuid_} has been submitted. It will be completed around "
@@ -605,7 +619,7 @@ async def revoke_children(
"""
dict_output = {}
if netuid is not None:
- success, message = await set_children_extrinsic(
+ success, message, ext_id = await set_children_extrinsic(
subtensor=subtensor,
wallet=wallet,
netuid=netuid,
@@ -620,6 +634,7 @@ async def revoke_children(
"error": message,
"set_block": None,
"completion_block": None,
+ "extrinsic_identifier": ext_id,
}
# Result
@@ -644,10 +659,10 @@ async def revoke_children(
if netuid_ == 0: # dont include root network
continue
console.print(f"Revoking children from netuid {netuid_}.")
- success, message = await set_children_extrinsic(
+ success, message, ext_id = await set_children_extrinsic(
subtensor=subtensor,
wallet=wallet,
- netuid=netuid,
+ netuid=netuid, # TODO should this be able to allow netuid = None ?
hotkey=get_hotkey_pub_ss58(wallet),
children_with_proportions=[],
prompt=prompt,
@@ -659,6 +674,7 @@ async def revoke_children(
"error": message,
"set_block": None,
"completion_block": None,
+ "extrinsic_identifier": ext_id,
}
if success:
current_block, completion_block = await get_childkey_completion_block(
@@ -688,12 +704,12 @@ async def childkey_take(
wait_for_inclusion: bool = True,
wait_for_finalization: bool = True,
prompt: bool = True,
-) -> list[tuple[Optional[int], bool]]:
+) -> list[tuple[Optional[int], bool, Optional[str]]]:
"""
Get or Set childkey take.
Returns:
- List of (netuid, success) for specified netuid (or all) and their success in setting take
+ List of (netuid, success, extrinsic identifier) for specified netuid (or all) and their success in setting take
"""
def validate_take_value(take_value: float) -> bool:
@@ -741,9 +757,11 @@ async def chk_all_subnets(ss58):
console.print(table)
- async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool:
+ async def set_chk_take_subnet(
+ subnet: int, chk_take: float
+ ) -> tuple[bool, Optional[str]]:
"""Set the childkey take for a single subnet"""
- success, message = await set_childkey_take_extrinsic(
+ success, message, ext_id = await set_childkey_take_extrinsic(
subtensor=subtensor,
wallet=wallet,
netuid=subnet,
@@ -759,12 +777,12 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool:
console.print(
f"The childkey take for {get_hotkey_pub_ss58(wallet)} is now set to {take * 100:.2f}%."
)
- return True
+ return True, ext_id
else:
console.print(
f":cross_mark:[red] Unable to set childkey take.[/red] {message}"
)
- return False
+ return False, ext_id
# Print childkey take for other user and return (dont offer to change take rate)
wallet_hk = get_hotkey_pub_ss58(wallet)
@@ -778,7 +796,7 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool:
console.print(
f"Hotkey {hotkey} not associated with wallet {wallet.name}."
)
- return [(netuid, False)]
+ return [(netuid, False, None)]
else:
# show child hotkey take on all subnets
await chk_all_subnets(hotkey)
@@ -786,12 +804,12 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool:
console.print(
f"Hotkey {hotkey} not associated with wallet {wallet.name}."
)
- return [(netuid, False)]
+ return [(netuid, False, None)]
# Validate child SS58 addresses
if not take:
if not Confirm.ask("Would you like to change the child take?"):
- return [(netuid, False)]
+ return [(netuid, False, None)]
new_take_value = -1.0
while not validate_take_value(new_take_value):
new_take_value = FloatPrompt.ask(
@@ -800,22 +818,21 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool:
take = new_take_value
else:
if not validate_take_value(take):
- return [(netuid, False)]
+ return [(netuid, False, None)]
if netuid:
- return [(netuid, await set_chk_take_subnet(subnet=netuid, chk_take=take))]
+ success, ext_id = await set_chk_take_subnet(subnet=netuid, chk_take=take)
+ return [(netuid, success, ext_id)]
else:
new_take_netuids = IntPrompt.ask(
"Enter netuid (leave blank for all)", default=None, show_default=True
)
if new_take_netuids:
- return [
- (
- new_take_netuids,
- await set_chk_take_subnet(subnet=new_take_netuids, chk_take=take),
- )
- ]
+ success, ext_id = await set_chk_take_subnet(
+ subnet=new_take_netuids, chk_take=take
+ )
+ return [(new_take_netuids, success, ext_id)]
else:
netuids = await subtensor.get_all_subnet_netuids()
@@ -823,8 +840,8 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool:
for netuid_ in netuids:
if netuid_ == 0:
continue
- console.print(f"Sending to netuid {netuid_} take of {take * 100:.2f}%")
- result = await set_childkey_take_extrinsic(
+ console.print(f"Setting take of {take * 100:.2f}% on netuid {netuid_}.")
+ result, _, ext_id = await set_childkey_take_extrinsic(
subtensor=subtensor,
wallet=wallet,
netuid=netuid_,
@@ -834,7 +851,7 @@ async def set_chk_take_subnet(subnet: int, chk_take: float) -> bool:
wait_for_inclusion=True,
wait_for_finalization=False,
)
- output_list.append((netuid_, result))
+ output_list.append((netuid_, result, ext_id))
console.print(
f":white_heavy_check_mark: [green]Sent childkey take of {take * 100:.2f}% to all subnets.[/green]"
)
diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py
index b4360ffdf..1443d611e 100644
--- a/bittensor_cli/src/commands/stake/move.py
+++ b/bittensor_cli/src/commands/stake/move.py
@@ -17,6 +17,7 @@
get_subnet_name,
unlock_key,
get_hotkey_pub_ss58,
+ print_extrinsic_id,
)
if TYPE_CHECKING:
@@ -436,12 +437,12 @@ async def move_stake(
era: int,
interactive_selection: bool = False,
prompt: bool = True,
-) -> bool:
+) -> tuple[bool, str]:
if interactive_selection:
try:
selection = await stake_move_transfer_selection(subtensor, wallet)
except ValueError:
- return False
+ return False, ""
origin_hotkey = selection["origin_hotkey"]
origin_netuid = selection["origin_netuid"]
amount = selection["amount"]
@@ -472,7 +473,7 @@ async def move_stake(
f"in Netuid: "
f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{origin_netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]"
)
- return False
+ return False, ""
console.print(
f"\nOrigin Netuid: "
@@ -507,7 +508,7 @@ async def move_stake(
f" < Moving amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]"
f"{amount_to_move_as_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]"
)
- return False
+ return False, ""
call = await subtensor.substrate.compose_call(
call_module="SubtensorModule",
@@ -545,13 +546,13 @@ async def move_stake(
extrinsic_fee=extrinsic_fee,
)
except ValueError:
- return False
+ return False, ""
if not Confirm.ask("Would you like to continue?"):
- return False
+ return False, ""
# Perform moving operation.
if not unlock_key(wallet).success:
- return False
+ return False, ""
with console.status(
f"\n:satellite: Moving [blue]{amount_to_move_as_balance}[/blue] from [blue]{origin_hotkey}[/blue] on netuid: "
f"[blue]{origin_netuid}[/blue] \nto "
@@ -563,17 +564,19 @@ async def move_stake(
response = await subtensor.substrate.submit_extrinsic(
extrinsic, wait_for_inclusion=True, wait_for_finalization=False
)
+ await print_extrinsic_id(response)
+ ext_id = await response.get_extrinsic_identifier()
if not prompt:
console.print(":white_heavy_check_mark: [green]Sent[/green]")
- return True
+ return True, ext_id
else:
if not await response.is_success:
err_console.print(
f"\n:cross_mark: [red]Failed[/red] with error:"
f" {format_error_message(await response.error_message)}"
)
- return False
+ return False, ""
else:
console.print(
":white_heavy_check_mark: [dark_sea_green3]Stake moved.[/dark_sea_green3]"
@@ -605,7 +608,7 @@ async def move_stake(
f"Destination Stake:\n [blue]{destination_stake_balance}[/blue] :arrow_right: "
f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_destination_stake_balance}"
)
- return True
+ return True, ext_id
async def transfer_stake(
@@ -620,7 +623,7 @@ async def transfer_stake(
interactive_selection: bool = False,
stake_all: bool = False,
prompt: bool = True,
-) -> bool:
+) -> tuple[bool, str]:
"""Transfers stake from one network to another.
Args:
@@ -653,11 +656,11 @@ async def transfer_stake(
)
if not dest_exists:
err_console.print(f"[red]Subnet {dest_netuid} does not exist[/red]")
- return False
+ return False, ""
if not origin_exists:
err_console.print(f"[red]Subnet {origin_netuid} does not exist[/red]")
- return False
+ return False, ""
# Get current stake balances
with console.status(f"Retrieving stake data from {subtensor.network}..."):
@@ -676,7 +679,7 @@ async def transfer_stake(
err_console.print(
f"[red]No stake found for hotkey: {origin_hotkey} on netuid: {origin_netuid}[/red]"
)
- return False
+ return False, ""
if amount:
amount_to_transfer = Balance.from_tao(amount).set_unit(origin_netuid)
@@ -694,7 +697,7 @@ async def transfer_stake(
f"Stake balance: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{current_stake}[/{COLOR_PALETTE.S.STAKE_AMOUNT}] < "
f"Transfer amount: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{amount_to_transfer}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]"
)
- return False
+ return False, ""
call = await subtensor.substrate.compose_call(
call_module="SubtensorModule",
@@ -732,14 +735,14 @@ async def transfer_stake(
extrinsic_fee=extrinsic_fee,
)
except ValueError:
- return False
+ return False, ""
if not Confirm.ask("Would you like to continue?"):
- return False
+ return False, ""
# Perform transfer operation
if not unlock_key(wallet).success:
- return False
+ return False, ""
with console.status("\n:satellite: Transferring stake ..."):
extrinsic = await subtensor.substrate.create_signed_extrinsic(
@@ -749,17 +752,19 @@ async def transfer_stake(
response = await subtensor.substrate.submit_extrinsic(
extrinsic, wait_for_inclusion=True, wait_for_finalization=False
)
+ ext_id = await response.get_extrinsic_identifier()
+ await print_extrinsic_id(extrinsic)
if not prompt:
console.print(":white_heavy_check_mark: [green]Sent[/green]")
- return True
+ return True, ext_id
if not await response.is_success:
err_console.print(
f":cross_mark: [red]Failed[/red] with error: "
f"{format_error_message(await response.error_message)}"
)
- return False
+ return False, ""
# Get and display new stake balances
new_stake, new_dest_stake = await asyncio.gather(
@@ -783,7 +788,7 @@ async def transfer_stake(
f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: "
f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_dest_stake}"
)
- return True
+ return True, ext_id
async def swap_stake(
@@ -798,7 +803,7 @@ async def swap_stake(
prompt: bool = True,
wait_for_inclusion: bool = True,
wait_for_finalization: bool = False,
-) -> bool:
+) -> tuple[bool, str]:
"""Swaps stake between subnets while keeping the same coldkey-hotkey pair ownership.
Args:
@@ -833,11 +838,11 @@ async def swap_stake(
)
if not dest_exists:
err_console.print(f"[red]Subnet {destination_netuid} does not exist[/red]")
- return False
+ return False, ""
if not origin_exists:
err_console.print(f"[red]Subnet {origin_netuid} does not exist[/red]")
- return False
+ return False, ""
# Get current stake balances
with console.status(f"Retrieving stake data from {subtensor.network}..."):
@@ -864,7 +869,7 @@ async def swap_stake(
f"Stake balance: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{current_stake}[/{COLOR_PALETTE.S.STAKE_AMOUNT}] < "
f"Swap amount: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{amount_to_swap}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]"
)
- return False
+ return False, ""
call = await subtensor.substrate.compose_call(
call_module="SubtensorModule",
@@ -901,14 +906,14 @@ async def swap_stake(
extrinsic_fee=extrinsic_fee,
)
except ValueError:
- return False
+ return False, ""
if not Confirm.ask("Would you like to continue?"):
- return False
+ return False, ""
# Perform swap operation
if not unlock_key(wallet).success:
- return False
+ return False, ""
with console.status(
f"\n:satellite: Swapping stake from netuid [blue]{origin_netuid}[/blue] "
@@ -923,17 +928,19 @@ async def swap_stake(
wait_for_inclusion=wait_for_inclusion,
wait_for_finalization=wait_for_finalization,
)
+ ext_id = await response.get_extrinsic_identifier()
+ await print_extrinsic_id(response)
if not prompt:
console.print(":white_heavy_check_mark: [green]Sent[/green]")
- return True
+ return True, ext_id
if not await response.is_success:
err_console.print(
f":cross_mark: [red]Failed[/red] with error: "
f"{format_error_message(await response.error_message)}"
)
- return False
+ return False, ""
# Get and display new stake balances
new_stake, new_dest_stake = await asyncio.gather(
@@ -957,4 +964,4 @@ async def swap_stake(
f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: "
f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_dest_stake}"
)
- return True
+ return True, ext_id
diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py
index ecec77fa5..b4b6bbeb1 100644
--- a/bittensor_cli/src/commands/stake/remove.py
+++ b/bittensor_cli/src/commands/stake/remove.py
@@ -4,6 +4,7 @@
from typing import TYPE_CHECKING, Optional
+from async_substrate_interface import AsyncExtrinsicReceipt
from bittensor_wallet import Wallet
from rich.prompt import Confirm, Prompt
from rich.table import Table
@@ -23,6 +24,7 @@
unlock_key,
json_console,
get_hotkey_pub_ss58,
+ print_extrinsic_id,
)
if TYPE_CHECKING:
@@ -134,7 +136,8 @@ async def unstake(
skip_remaining_subnets = False
if len(netuids) > 1 and not amount:
console.print(
- "[dark_sea_green3]Tip: Enter 'q' any time to stop going over remaining subnets and process current unstakes.\n"
+ "[dark_sea_green3]Tip: Enter 'q' any time to stop going over "
+ "remaining subnets and process current unstakes.\n"
)
# Iterate over hotkeys and netuids to collect unstake operations
@@ -335,7 +338,8 @@ async def unstake(
func = _unstake_extrinsic
specific_args = {"current_stake": op["current_stake_balance"]}
- suc = await func(**common_args, **specific_args)
+ suc, ext_receipt = await func(**common_args, **specific_args)
+ ext_id = await ext_receipt.get_extrinsic_identifier() if suc else None
successes.append(
{
@@ -343,6 +347,7 @@ async def unstake(
"hotkey_ss58": op["hotkey_ss58"],
"unstake_amount": op["amount_to_unstake"].tao,
"success": suc,
+ "extrinsic_identifier": ext_id,
}
)
@@ -350,7 +355,7 @@ async def unstake(
f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]Unstaking operations completed."
)
if json_output:
- json_console.print(json.dumps(successes))
+ json_console.print_json(data=successes)
return True
@@ -533,7 +538,7 @@ async def unstake_all(
successes = {}
with console.status("Unstaking all stakes...") as status:
for hotkey_ss58 in hotkey_ss58s:
- successes[hotkey_ss58] = await _unstake_all_extrinsic(
+ success, ext_receipt = await _unstake_all_extrinsic(
wallet=wallet,
subtensor=subtensor,
hotkey_ss58=hotkey_ss58,
@@ -542,6 +547,11 @@ async def unstake_all(
status=status,
era=era,
)
+ ext_id = await ext_receipt.get_extrinsic_identifier() if successes else None
+ successes[hotkey_ss58] = {
+ "success": success,
+ "extrinsic_identifier": ext_id,
+ }
if json_output:
return json_console.print(json.dumps({"success": successes}))
@@ -556,7 +566,7 @@ async def _unstake_extrinsic(
hotkey_ss58: str,
status=None,
era: int = 3,
-) -> bool:
+) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]:
"""Execute a standard unstake extrinsic.
Args:
@@ -604,8 +614,9 @@ async def _unstake_extrinsic(
f"{failure_prelude} with error: "
f"{format_error_message(await response.error_message)}"
)
- return False
+ return False, None
# Fetch latest balance and stake
+ await print_extrinsic_id(response)
block_hash = await subtensor.substrate.get_chain_head()
new_balance, new_stake = await asyncio.gather(
subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash),
@@ -625,11 +636,11 @@ async def _unstake_extrinsic(
f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]"
f" Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}"
)
- return True
+ return True, response
except Exception as e:
err_out(f"{failure_prelude} with error: {str(e)}")
- return False
+ return False, None
async def _safe_unstake_extrinsic(
@@ -642,7 +653,7 @@ async def _safe_unstake_extrinsic(
allow_partial_stake: bool,
status=None,
era: int = 3,
-) -> bool:
+) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]:
"""Execute a safe unstake extrinsic with price limit.
Args:
@@ -708,14 +719,14 @@ async def _safe_unstake_extrinsic(
)
else:
err_out(f"\n{failure_prelude} with error: {format_error_message(e)}")
- return False
+ return False, None
if not await response.is_success:
err_out(
f"\n{failure_prelude} with error: {format_error_message(await response.error_message)}"
)
- return False
-
+ return False, None
+ await print_extrinsic_id(response)
block_hash = await subtensor.substrate.get_chain_head()
new_balance, new_stake = await asyncio.gather(
subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash),
@@ -745,7 +756,7 @@ async def _safe_unstake_extrinsic(
f"Subnet: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] "
f"Stake:\n [blue]{current_stake}[/blue] :arrow_right: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_stake}"
)
- return True
+ return True, response
async def _unstake_all_extrinsic(
@@ -756,7 +767,7 @@ async def _unstake_all_extrinsic(
unstake_all_alpha: bool,
status=None,
era: int = 3,
-) -> None:
+) -> tuple[bool, Optional[AsyncExtrinsicReceipt]]:
"""Execute an unstake all extrinsic.
Args:
@@ -774,7 +785,7 @@ async def _unstake_all_extrinsic(
if status:
status.update(
- f"\n:satellite: {'Unstaking all Alpha stakes' if unstake_all_alpha else 'Unstaking all stakes'} from {hotkey_name} ..."
+ f"\n:satellite: Unstaking all {'Alpha ' if unstake_all_alpha else ''}stakes from {hotkey_name} ..."
)
block_hash = await subtensor.substrate.get_chain_head()
@@ -817,7 +828,9 @@ async def _unstake_all_extrinsic(
f"{failure_prelude} with error: "
f"{format_error_message(await response.error_message)}"
)
- return
+ return False, None
+ else:
+ await print_extrinsic_id(response)
# Fetch latest balance and stake
block_hash = await subtensor.substrate.get_chain_head()
@@ -855,9 +868,11 @@ async def _unstake_all_extrinsic(
f"[blue]{previous_root_stake}[/blue] :arrow_right: "
f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{new_root_stake}"
)
+ return True, response
except Exception as e:
err_out(f"{failure_prelude} with error: {str(e)}")
+ return False, None
async def _get_extrinsic_fee(
diff --git a/bittensor_cli/src/commands/subnets/mechanisms.py b/bittensor_cli/src/commands/subnets/mechanisms.py
new file mode 100644
index 000000000..bf329513a
--- /dev/null
+++ b/bittensor_cli/src/commands/subnets/mechanisms.py
@@ -0,0 +1,498 @@
+import asyncio
+import math
+from typing import TYPE_CHECKING, Optional
+
+from bittensor_wallet import Wallet
+from rich.prompt import Confirm, Prompt
+from rich.table import Column, Table
+from rich import box
+
+from bittensor_cli.src import COLOR_PALETTE
+from bittensor_cli.src.commands import sudo
+from bittensor_cli.src.bittensor.utils import (
+ console,
+ err_console,
+ json_console,
+ U16_MAX,
+ print_extrinsic_id,
+)
+
+if TYPE_CHECKING:
+ from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface
+
+
+async def count(
+ subtensor: "SubtensorInterface",
+ netuid: int,
+ json_output: bool = False,
+) -> Optional[int]:
+ """Display how many mechanisms exist for the provided subnet."""
+
+ block_hash = await subtensor.substrate.get_chain_head()
+ if not await subtensor.subnet_exists(netuid=netuid, block_hash=block_hash):
+ err_console.print(f"[red]Subnet {netuid} does not exist[/red]")
+ if json_output:
+ json_console.print_json(
+ data={"success": False, "error": f"Subnet {netuid} does not exist"}
+ )
+ return None
+
+ with console.status(
+ f":satellite:Retrieving mechanism count from {subtensor.network}...",
+ spinner="aesthetic",
+ ):
+ mechanism_count = await subtensor.get_subnet_mechanisms(
+ netuid, block_hash=block_hash
+ )
+ if not mechanism_count:
+ if json_output:
+ json_console.print_json(
+ data={
+ "netuid": netuid,
+ "count": None,
+ "error": "Failed to get mechanism count",
+ }
+ )
+ else:
+ err_console.print(
+ "Subnet mechanism count: [red]Failed to get mechanism count[/red]"
+ )
+ return None
+
+ if json_output:
+ json_console.print_json(
+ data={
+ "netuid": netuid,
+ "count": mechanism_count,
+ "error": "",
+ }
+ )
+ else:
+ console.print(
+ f"[blue]Subnet {netuid}[/blue] currently has [blue]{mechanism_count}[/blue] mechanism"
+ f"{'s' if mechanism_count != 1 else ''}."
+ f"\n[dim](Tip: 1 mechanism means there are no mechanisms beyond the main subnet)[/dim]"
+ )
+
+ return mechanism_count
+
+
+async def get_emission_split(
+ subtensor: "SubtensorInterface",
+ netuid: int,
+ json_output: bool = False,
+) -> Optional[dict]:
+ """Display the emission split across mechanisms for a subnet."""
+
+ count_ = await subtensor.get_subnet_mechanisms(netuid)
+ if count_ == 1:
+ console.print(
+ f"Subnet {netuid} only has the primary mechanism (mechanism 0). No emission split to display."
+ )
+ if json_output:
+ json_console.print_json(
+ data={
+ "success": False,
+ "error": "Subnet only has the primary mechanism (mechanism 0). No emission split to display.",
+ }
+ )
+ return None
+
+ emission_split = await subtensor.get_mechanism_emission_split(netuid) or []
+
+ even_distribution = False
+ total_sum = sum(emission_split)
+ if total_sum == 0 and count_ > 0:
+ even_distribution = True
+ base, remainder = divmod(U16_MAX, count_)
+ emission_split = [base for _ in range(count_)]
+ if remainder:
+ emission_split[0] += remainder
+ total_sum = sum(emission_split)
+
+ emission_percentages = (
+ [round((value / total_sum) * 100, 6) for value in emission_split]
+ if total_sum > 0
+ else [0.0 for _ in emission_split]
+ )
+
+ data = {
+ "netuid": netuid,
+ "raw_count": count_,
+ "visible_count": max(count_ - 1, 0),
+ "split": emission_split if count_ else [],
+ "percentages": emission_percentages if count_ else [],
+ "even_distribution": even_distribution,
+ }
+
+ if json_output:
+ json_console.print_json(data=data)
+ else:
+ table = Table(
+ Column(
+ "[bold white]Mechanism Index[/]",
+ justify="center",
+ style=COLOR_PALETTE.G.NETUID,
+ ),
+ Column(
+ "[bold white]Weight (u16)[/]",
+ justify="right",
+ style=COLOR_PALETTE.STAKE.STAKE_ALPHA,
+ ),
+ Column(
+ "[bold white]Share (%)[/]",
+ justify="right",
+ style=COLOR_PALETTE.POOLS.EMISSION,
+ ),
+ title=f"\n[{COLOR_PALETTE.G.HEADER}]Subnet {netuid} • Emission split[/]\n"
+ f"[{COLOR_PALETTE.G.SUBHEAD}]Network: {subtensor.network}[/{COLOR_PALETTE.G.SUBHEAD}]",
+ box=box.SIMPLE,
+ show_footer=True,
+ border_style="bright_black",
+ )
+
+ total_weight = sum(emission_split)
+ share_percent = (total_weight / U16_MAX) * 100 if U16_MAX else 0
+
+ for idx, value in enumerate(emission_split):
+ share = (
+ emission_percentages[idx] if idx < len(emission_percentages) else 0.0
+ )
+ table.add_row(str(idx), str(value), f"{share:.6f}")
+
+ table.add_row(
+ "[dim]Total[/dim]",
+ f"[{COLOR_PALETTE.STAKE.STAKE_ALPHA}]{total_weight}[/{COLOR_PALETTE.STAKE.STAKE_ALPHA}]",
+ f"[{COLOR_PALETTE.POOLS.EMISSION}]{share_percent:.6f}[/{COLOR_PALETTE.POOLS.EMISSION}]",
+ )
+
+ console.print(table)
+ footer = "[dim]Totals are expressed as a fraction of 65535 (U16_MAX).[/dim]"
+ if even_distribution:
+ footer += (
+ "\n[dim]No custom split found; displaying an even distribution.[/dim]"
+ )
+ console.print(footer)
+
+ return data
+
+
+async def set_emission_split(
+ subtensor: "SubtensorInterface",
+ wallet: Wallet,
+ netuid: int,
+ new_emission_split: Optional[str],
+ wait_for_inclusion: bool,
+ wait_for_finalization: bool,
+ prompt: bool,
+ json_output: bool,
+) -> bool:
+ """Set the emission split across mechanisms for a subnet."""
+
+ mech_count, existing_split = await asyncio.gather(
+ subtensor.get_subnet_mechanisms(netuid),
+ subtensor.get_mechanism_emission_split(netuid),
+ )
+
+ if mech_count == 0:
+ message = (
+ f"Subnet {netuid} does not currently contain any mechanisms to configure."
+ )
+ if json_output:
+ json_console.print_json(data={"success": False, "error": message})
+ else:
+ err_console.print(message)
+ return False
+
+ if not json_output:
+ await get_emission_split(
+ subtensor=subtensor,
+ netuid=netuid,
+ json_output=False,
+ )
+
+ existing_split = [int(value) for value in existing_split]
+ if len(existing_split) < mech_count:
+ existing_split.extend([0] * (mech_count - len(existing_split)))
+
+ if new_emission_split is not None:
+ try:
+ weights = [
+ float(item.strip())
+ for item in new_emission_split.split(",")
+ if item.strip() != ""
+ ]
+ except ValueError:
+ message = (
+ "Invalid `--split` values. Provide a comma-separated list of numbers."
+ )
+ if json_output:
+ json_console.print_json(data={"success": False, "error": message})
+ else:
+ err_console.print(message)
+ return False
+ else:
+ if not prompt:
+ err_console.print(
+ "Split values not supplied with `--no-prompt` flag. Cannot continue."
+ )
+ return False
+
+ weights: list[float] = []
+ total_existing = sum(existing_split) or 1
+ console.print("\n[dim]You either provide U16 values or percentages.[/dim]")
+ for idx in range(mech_count):
+ current_value = existing_split[idx]
+ current_percent = (
+ (current_value / total_existing) * 100 if total_existing else 0
+ )
+ label = (
+ "[blue]Main Mechanism (1)[/blue]"
+ if idx == 0
+ else f"[blue]Mechanism {idx + 1}[/blue]"
+ )
+ response = Prompt.ask(
+ (
+ f"Relative weight for {label} "
+ f"[{COLOR_PALETTE.STAKE.STAKE_ALPHA}](current: {current_value} ~ {current_percent:.2f}%)[/{COLOR_PALETTE.STAKE.STAKE_ALPHA}]"
+ )
+ )
+ try:
+ weights.append(float(response))
+ except ValueError:
+ err_console.print("Invalid number provided. Aborting.")
+ return False
+
+ if len(weights) != mech_count:
+ message = f"Expected {mech_count} weight values, received {len(weights)}."
+ if json_output:
+ json_console.print_json(data={"success": False, "error": message})
+ else:
+ err_console.print(message)
+ return False
+
+ if any(value < 0 for value in weights):
+ message = "Weights must be non-negative."
+ if json_output:
+ json_console.print_json(data={"success": False, "error": message})
+ else:
+ err_console.print(message)
+ return False
+
+ try:
+ normalized_weights, fractions = _normalize_emission_weights(weights)
+ except ValueError as exc:
+ message = str(exc)
+ if json_output:
+ json_console.print_json(data={"success": False, "error": message})
+ else:
+ err_console.print(message)
+ return False
+
+ if normalized_weights == existing_split:
+ message = ":white_heavy_check_mark: [dark_sea_green3]Emission split unchanged.[/dark_sea_green3]"
+ if json_output:
+ json_console.print_json(
+ data={
+ "success": True,
+ "message": "Emission split unchanged.",
+ "split": normalized_weights,
+ "percentages": [round(value * 100, 6) for value in fractions],
+ "extrinsic_identifier": None,
+ }
+ )
+ else:
+ console.print(message)
+ return True
+
+ if not json_output:
+ table = Table(
+ Column(
+ "[bold white]Mechanism Index[/]",
+ justify="center",
+ style=COLOR_PALETTE.G.NETUID,
+ ),
+ Column(
+ "[bold white]Weight (u16)[/]",
+ justify="right",
+ style=COLOR_PALETTE.STAKE.STAKE_ALPHA,
+ ),
+ Column(
+ "[bold white]Share (%)[/]",
+ justify="right",
+ style=COLOR_PALETTE.POOLS.EMISSION,
+ ),
+ title=(
+ f"\n[{COLOR_PALETTE.G.HEADER}]Proposed emission split[/{COLOR_PALETTE.G.HEADER}]\n"
+ f"[{COLOR_PALETTE.G.SUBHEAD}]Subnet {netuid}[/{COLOR_PALETTE.G.SUBHEAD}]"
+ ),
+ box=box.SIMPLE,
+ show_footer=True,
+ border_style="bright_black",
+ )
+
+ total_weight = sum(normalized_weights)
+ total_share_percent = (total_weight / U16_MAX) * 100 if U16_MAX else 0
+
+ for idx, weight in enumerate(normalized_weights):
+ share_percent = fractions[idx] * 100 if idx < len(fractions) else 0.0
+ table.add_row(str(idx), str(weight), f"{share_percent:.6f}")
+
+ table.add_row("", "", "", style="dim")
+ table.add_row(
+ "[dim]Total[/dim]",
+ f"[{COLOR_PALETTE.STAKE.STAKE_ALPHA}]{total_weight}[/{COLOR_PALETTE.STAKE.STAKE_ALPHA}]",
+ f"[{COLOR_PALETTE.POOLS.EMISSION}]{total_share_percent:.6f}[/{COLOR_PALETTE.POOLS.EMISSION}]",
+ )
+
+ console.print(table)
+
+ if not Confirm.ask("Proceed with these emission weights?", default=True):
+ console.print(":cross_mark: Aborted!")
+ return False
+
+ success, err_msg, ext_id = await set_mechanism_emission(
+ wallet=wallet,
+ subtensor=subtensor,
+ netuid=netuid,
+ split=normalized_weights,
+ wait_for_inclusion=wait_for_inclusion,
+ wait_for_finalization=wait_for_finalization,
+ json_output=json_output,
+ )
+
+ if json_output:
+ json_console.print_json(
+ data={
+ "success": success,
+ "err_msg": err_msg,
+ "split": normalized_weights,
+ "percentages": [round(value * 100, 6) for value in fractions],
+ "extrinsic_identifier": ext_id,
+ }
+ )
+
+ return success
+
+
+def _normalize_emission_weights(values: list[float]) -> tuple[list[int], list[float]]:
+ total = sum(values)
+ if total <= 0:
+ raise ValueError("Sum of emission weights must be greater than zero.")
+
+ fractions = [value / total for value in values]
+ scaled = [fraction * U16_MAX for fraction in fractions]
+ base = [math.floor(value) for value in scaled]
+ remainder = int(U16_MAX - sum(base))
+
+ if remainder > 0:
+ fractional_parts = [value - math.floor(value) for value in scaled]
+ order = sorted(
+ range(len(base)), key=lambda idx_: fractional_parts[idx_], reverse=True
+ )
+ idx = 0
+ length = len(order)
+ while remainder > 0 and length > 0:
+ base[order[idx % length]] += 1
+ remainder -= 1
+ idx += 1
+
+ return [int(value) for value in base], fractions
+
+
+async def set_mechanism_count(
+ wallet: Wallet,
+ subtensor: "SubtensorInterface",
+ netuid: int,
+ mechanism_count: int,
+ previous_count: int,
+ wait_for_inclusion: bool,
+ wait_for_finalization: bool,
+ json_output: bool,
+) -> tuple[bool, str, Optional[str]]:
+ """Set the number of mechanisms for a subnet."""
+
+ if mechanism_count < 1:
+ err_msg = "Mechanism count must be greater than or equal to one."
+ if not json_output:
+ err_console.print(err_msg)
+ return False, err_msg, None
+
+ if not await subtensor.subnet_exists(netuid):
+ err_msg = f"Subnet with netuid {netuid} does not exist."
+ if not json_output:
+ err_console.print(err_msg)
+ return False, err_msg, None
+
+ if not Confirm.ask(
+ f"Subnet [blue]{netuid}[/blue] currently has [blue]{previous_count}[/blue] mechanism"
+ f"{'s' if previous_count != 1 else ''}."
+ f" Set it to [blue]{mechanism_count}[/blue]?"
+ ):
+ return False, "User cancelled", None
+
+ success, err_msg, ext_receipt = await sudo.set_mechanism_count_extrinsic(
+ subtensor=subtensor,
+ wallet=wallet,
+ netuid=netuid,
+ mech_count=mechanism_count,
+ wait_for_inclusion=wait_for_inclusion,
+ wait_for_finalization=wait_for_finalization,
+ )
+ ext_id = await ext_receipt.get_extrinsic_identifier() if success else None
+
+ if json_output:
+ return success, err_msg, ext_id
+
+ if success:
+ await print_extrinsic_id(ext_receipt)
+ console.print(
+ ":white_heavy_check_mark: "
+ f"[dark_sea_green3]Mechanism count set to {mechanism_count} for subnet {netuid}[/dark_sea_green3]"
+ )
+ else:
+ err_console.print(f":cross_mark: [red]{err_msg}[/red]")
+
+ return success, err_msg, ext_id
+
+
+async def set_mechanism_emission(
+ wallet: Wallet,
+ subtensor: "SubtensorInterface",
+ netuid: int,
+ split: list[int],
+ wait_for_inclusion: bool,
+ wait_for_finalization: bool,
+ json_output: bool,
+) -> tuple[bool, str, Optional[str]]:
+ """Set the emission split for mechanisms within a subnet."""
+
+ if not split:
+ err_msg = "Emission split must include at least one weight."
+ if not json_output:
+ err_console.print(err_msg)
+ return False, err_msg, None
+
+ success, err_msg, ext_receipt = await sudo.set_mechanism_emission_extrinsic(
+ subtensor=subtensor,
+ wallet=wallet,
+ netuid=netuid,
+ split=split,
+ wait_for_inclusion=wait_for_inclusion,
+ wait_for_finalization=wait_for_finalization,
+ )
+ ext_id = await ext_receipt.get_extrinsic_identifier() if success else None
+
+ if json_output:
+ return success, err_msg, ext_id
+
+ if success:
+ await print_extrinsic_id(ext_receipt)
+ console.print(
+ ":white_heavy_check_mark: "
+ f"[dark_sea_green3]Emission split updated for subnet {netuid}[/dark_sea_green3]"
+ )
+ else:
+ err_console.print(f":cross_mark: [red]{err_msg}[/red]")
+
+ return success, err_msg, ext_id
diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py
index d8571f3f6..664657986 100644
--- a/bittensor_cli/src/commands/subnets/subnets.py
+++ b/bittensor_cli/src/commands/subnets/subnets.py
@@ -37,6 +37,7 @@
blocks_to_duration,
json_console,
get_hotkey_pub_ss58,
+ print_extrinsic_id,
)
if TYPE_CHECKING:
@@ -54,7 +55,7 @@ async def register_subnetwork_extrinsic(
wait_for_inclusion: bool = False,
wait_for_finalization: bool = True,
prompt: bool = False,
-) -> tuple[bool, Optional[int]]:
+) -> tuple[bool, Optional[int], Optional[str]]:
"""Registers a new subnetwork.
wallet (bittensor.wallet):
@@ -66,9 +67,11 @@ async def register_subnetwork_extrinsic(
prompt (bool):
If true, the call waits for confirmation from the user before proceeding.
Returns:
- success (bool):
- Flag is ``true`` if extrinsic was finalized or included in the block.
- If we did not wait for finalization / inclusion, the response is ``true``.
+ tuple including:
+ success: Flag is `True` if extrinsic was finalized or included in the block.
+ If we did not wait for finalization/inclusion, the response is `True`.
+ error_message: Optional error message.
+ extrinsic_identifier: Optional extrinsic identifier, if the extrinsic was included.
"""
async def _find_event_attributes_in_extrinsic_receipt(
@@ -103,7 +106,7 @@ async def _find_event_attributes_in_extrinsic_receipt(
f"[{COLOR_PALETTE['POOLS']['TAO']}]{sn_burn_cost}[{COLOR_PALETTE['POOLS']['TAO']}] "
f"to register a subnet."
)
- return False, None
+ return False, None, None
if prompt:
console.print(
@@ -112,7 +115,7 @@ async def _find_event_attributes_in_extrinsic_receipt(
if not Confirm.ask(
f"Do you want to burn [{COLOR_PALETTE['POOLS']['TAO']}]{sn_burn_cost} to register a subnet?"
):
- return False, None
+ return False, None, None
call_params = {
"hotkey": get_hotkey_pub_ss58(wallet),
@@ -157,10 +160,10 @@ async def _find_event_attributes_in_extrinsic_receipt(
f"[red]Error:[/red] Identity field [white]{field}[/white] must be <= {max_size} bytes.\n"
f"Value '{value.decode()}' is {len(value)} bytes."
)
- return False, None
+ return False, None, None
if not unlock_key(wallet).success:
- return False, None
+ return False, None, None
with console.status(":satellite: Registering subnet...", spinner="earth"):
substrate = subtensor.substrate
@@ -181,24 +184,26 @@ async def _find_event_attributes_in_extrinsic_receipt(
# We only wait here if we expect finalization.
if not wait_for_finalization and not wait_for_inclusion:
- return True, None
+ return True, None, None
if not await response.is_success:
err_console.print(
f":cross_mark: [red]Failed[/red]: {format_error_message(await response.error_message)}"
)
await asyncio.sleep(0.5)
- return False, None
+ return False, None, None
# Successful registration, final check for membership
else:
attributes = await _find_event_attributes_in_extrinsic_receipt(
response, "NetworkAdded"
)
+ await print_extrinsic_id(response)
+ ext_id = await response.get_extrinsic_identifier()
console.print(
f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}"
)
- return True, int(attributes[0])
+ return True, int(attributes[0]), ext_id
# commands
@@ -216,8 +221,12 @@ async def subnets_list(
"""List all subnet netuids in the network."""
async def fetch_subnet_data():
- block_number_ = await subtensor.substrate.get_block_number(None)
- subnets_ = await subtensor.all_subnets()
+ block_hash = await subtensor.substrate.get_chain_head()
+ subnets_, mechanisms, block_number_ = await asyncio.gather(
+ subtensor.all_subnets(block_hash=block_hash),
+ subtensor.get_all_subnet_mechanisms(block_hash=block_hash),
+ subtensor.substrate.get_block_number(block_hash=block_hash),
+ )
# Sort subnets by market cap, keeping the root subnet in the first position
root_subnet = next(s for s in subnets_ if s.netuid == 0)
@@ -227,7 +236,7 @@ async def fetch_subnet_data():
reverse=True,
)
sorted_subnets = [root_subnet] + other_subnets
- return sorted_subnets, block_number_
+ return sorted_subnets, block_number_, mechanisms
def calculate_emission_stats(
subnets_: list, block_number_: int
@@ -315,10 +324,15 @@ def define_table(
justify="left",
overflow="fold",
)
+ defined_table.add_column(
+ "[bold white]Mechanisms",
+ style=COLOR_PALETTE["GENERAL"]["SUBHEADING_EXTRA_1"],
+ justify="center",
+ )
return defined_table
# Non-live mode
- def _create_table(subnets_, block_number_):
+ def _create_table(subnets_, block_number_, mechanisms):
rows = []
_, percentage_string = calculate_emission_stats(subnets_, block_number_)
@@ -398,6 +412,8 @@ def _create_table(subnets_, block_number_):
else:
tempo_cell = "-/-"
+ mechanisms_cell = str(mechanisms.get(netuid, 1))
+
rows.append(
(
netuid_cell, # Netuid
@@ -409,6 +425,7 @@ def _create_table(subnets_, block_number_):
alpha_out_cell, # Stake α_out
supply_cell, # Supply
tempo_cell, # Tempo k/n
+ mechanisms_cell, # Mechanism count
)
)
@@ -430,7 +447,7 @@ def _create_table(subnets_, block_number_):
defined_table.add_row(*row)
return defined_table
- def dict_table(subnets_, block_number_) -> dict:
+ def dict_table(subnets_, block_number_, mechanisms) -> dict:
subnet_rows = {}
total_tao_emitted, _ = calculate_emission_stats(subnets_, block_number_)
total_emissions = 0.0
@@ -470,6 +487,7 @@ def dict_table(subnets_, block_number_) -> dict:
"alpha_out": alpha_out,
"supply": supply,
"tempo": tempo,
+ "mechanisms": mechanisms.get(netuid, 1),
}
output = {
"total_tao_emitted": total_tao_emitted,
@@ -482,7 +500,7 @@ def dict_table(subnets_, block_number_) -> dict:
return output
# Live mode
- def create_table_live(subnets_, previous_data_, block_number_):
+ def create_table_live(subnets_, previous_data_, block_number_, mechanisms):
def format_cell(
value, previous_value, unit="", unit_first=False, precision=4, millify=False
):
@@ -718,6 +736,7 @@ def format_liquidity_cell(
alpha_out_cell, # Stake α_out
supply_cell, # Supply
tempo_cell, # Tempo k/n
+ str(mechanisms.get(netuid, 1)), # Mechanisms
)
)
@@ -764,7 +783,7 @@ def format_liquidity_cell(
with Live(console=console, screen=True, auto_refresh=True) as live:
try:
while True:
- subnets, block_number = await fetch_subnet_data()
+ subnets, block_number, mechanisms = await fetch_subnet_data()
# Update block numbers
previous_block = current_block
@@ -776,7 +795,7 @@ def format_liquidity_cell(
)
table, current_data = create_table_live(
- subnets, previous_data, block_number
+ subnets, previous_data, block_number, mechanisms
)
previous_data = current_data
progress.reset(progress_task)
@@ -802,11 +821,13 @@ def format_liquidity_cell(
pass # Ctrl + C
else:
# Non-live mode
- subnets, block_number = await fetch_subnet_data()
+ subnets, block_number, mechanisms = await fetch_subnet_data()
if json_output:
- json_console.print(json.dumps(dict_table(subnets, block_number)))
+ json_console.print(
+ json.dumps(dict_table(subnets, block_number, mechanisms))
+ )
else:
- table = _create_table(subnets, block_number)
+ table = _create_table(subnets, block_number, mechanisms)
console.print(table)
return
@@ -872,6 +893,8 @@ def format_liquidity_cell(
async def show(
subtensor: "SubtensorInterface",
netuid: int,
+ mechanism_id: Optional[int] = None,
+ mechanism_count: Optional[int] = None,
sort: bool = False,
max_rows: Optional[int] = None,
delegate_selection: bool = False,
@@ -1085,43 +1108,57 @@ async def show_root():
)
return selected_hotkey
- async def show_subnet(netuid_: int):
+ async def show_subnet(
+ netuid_: int,
+ mechanism_id: Optional[int],
+ mechanism_count: Optional[int],
+ ):
if not await subtensor.subnet_exists(netuid=netuid):
err_console.print(f"[red]Subnet {netuid} does not exist[/red]")
return False
+
block_hash = await subtensor.substrate.get_chain_head()
(
subnet_info,
- subnet_state,
identities,
old_identities,
current_burn_cost,
) = await asyncio.gather(
subtensor.subnet(netuid=netuid_, block_hash=block_hash),
- subtensor.get_subnet_state(netuid=netuid_, block_hash=block_hash),
subtensor.query_all_identities(block_hash=block_hash),
subtensor.get_delegate_identities(block_hash=block_hash),
subtensor.get_hyperparameter(
param_name="Burn", netuid=netuid_, block_hash=block_hash
),
)
- if subnet_state is None:
- print_error(f"Subnet {netuid_} does not exist")
+
+ selected_mechanism_id = mechanism_id or 0
+
+ metagraph_info = await subtensor.get_mechagraph_info(
+ netuid_, selected_mechanism_id, block_hash=block_hash
+ )
+
+ if metagraph_info is None:
+ print_error(
+ f"Subnet {netuid_} with mechanism: {selected_mechanism_id} does not exist"
+ )
return False
if subnet_info is None:
print_error(f"Subnet {netuid_} does not exist")
return False
- if len(subnet_state.hotkeys) == 0:
+ if len(metagraph_info.hotkeys) == 0:
print_error(f"Subnet {netuid_} is currently empty with 0 UIDs registered.")
return False
# Define table properties
+ mechanism_label = f"Mechanism {selected_mechanism_id}"
+
table = Table(
title=f"[{COLOR_PALETTE['GENERAL']['HEADER']}]Subnet [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{netuid_}"
f"{': ' + get_subnet_name(subnet_info)}"
- f"\nNetwork: [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{subtensor.network}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n",
+ f"\n[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Network: {subtensor.network} • {mechanism_label}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\n",
show_footer=True,
show_edge=False,
header_style="bold white",
@@ -1133,33 +1170,11 @@ async def show_subnet(netuid_: int):
)
# For table footers
- alpha_sum = sum(
- [
- subnet_state.alpha_stake[idx].tao
- for idx in range(len(subnet_state.alpha_stake))
- ]
- )
- stake_sum = sum(
- [
- subnet_state.total_stake[idx].tao
- for idx in range(len(subnet_state.total_stake))
- ]
- )
- tao_sum = sum(
- [
- subnet_state.tao_stake[idx].tao * TAO_WEIGHT
- for idx in range(len(subnet_state.tao_stake))
- ]
- )
- dividends_sum = sum(
- subnet_state.dividends[idx] for idx in range(len(subnet_state.dividends))
- )
- emission_sum = sum(
- [
- subnet_state.emission[idx].tao
- for idx in range(len(subnet_state.emission))
- ]
- )
+ alpha_sum = sum(stake.tao for stake in metagraph_info.alpha_stake)
+ stake_sum = sum(stake.tao for stake in metagraph_info.total_stake)
+ tao_sum = sum((stake * TAO_WEIGHT).tao for stake in metagraph_info.tao_stake)
+ dividends_sum = sum(metagraph_info.dividends)
+ emission_sum = sum(emission.tao for emission in metagraph_info.emission)
owner_hotkeys = await subtensor.get_owned_hotkeys(subnet_info.owner_coldkey)
if subnet_info.owner_hotkey not in owner_hotkeys:
@@ -1174,7 +1189,7 @@ async def show_subnet(netuid_: int):
break
sorted_indices = sorted(
- range(len(subnet_state.hotkeys)),
+ range(len(metagraph_info.hotkeys)),
key=lambda i: (
# If sort is True, sort only by UIDs
i
@@ -1183,11 +1198,11 @@ async def show_subnet(netuid_: int):
# Otherwise
# Sort by owner status first
not (
- subnet_state.coldkeys[i] == subnet_info.owner_coldkey
- or subnet_state.hotkeys[i] in owner_hotkeys
+ metagraph_info.coldkeys[i] == subnet_info.owner_coldkey
+ or metagraph_info.hotkeys[i] in owner_hotkeys
),
# Then sort by stake amount (higher stakes first)
- -subnet_state.total_stake[i].tao,
+ -metagraph_info.total_stake[i].tao,
)
),
)
@@ -1196,10 +1211,10 @@ async def show_subnet(netuid_: int):
json_out_rows = []
for idx in sorted_indices:
# Get identity for this uid
- coldkey_identity = identities.get(subnet_state.coldkeys[idx], {}).get(
+ coldkey_identity = identities.get(metagraph_info.coldkeys[idx], {}).get(
"name", ""
)
- hotkey_identity = old_identities.get(subnet_state.hotkeys[idx])
+ hotkey_identity = old_identities.get(metagraph_info.hotkeys[idx])
uid_identity = (
coldkey_identity
if coldkey_identity
@@ -1207,8 +1222,8 @@ async def show_subnet(netuid_: int):
)
if (
- subnet_state.coldkeys[idx] == subnet_info.owner_coldkey
- or subnet_state.hotkeys[idx] in owner_hotkeys
+ metagraph_info.coldkeys[idx] == subnet_info.owner_coldkey
+ or metagraph_info.hotkeys[idx] in owner_hotkeys
):
if uid_identity == "~":
uid_identity = (
@@ -1220,44 +1235,44 @@ async def show_subnet(netuid_: int):
)
# Modify tao stake with TAO_WEIGHT
- tao_stake = subnet_state.tao_stake[idx] * TAO_WEIGHT
+ tao_stake = metagraph_info.tao_stake[idx] * TAO_WEIGHT
rows.append(
(
str(idx), # UID
- f"{subnet_state.total_stake[idx].tao:.4f} {subnet_info.symbol}"
+ f"{metagraph_info.total_stake[idx].tao:.4f} {subnet_info.symbol}"
if verbose
- else f"{millify_tao(subnet_state.total_stake[idx])} {subnet_info.symbol}", # Stake
- f"{subnet_state.alpha_stake[idx].tao:.4f} {subnet_info.symbol}"
+ else f"{millify_tao(metagraph_info.total_stake[idx])} {subnet_info.symbol}", # Stake
+ f"{metagraph_info.alpha_stake[idx].tao:.4f} {subnet_info.symbol}"
if verbose
- else f"{millify_tao(subnet_state.alpha_stake[idx])} {subnet_info.symbol}", # Alpha Stake
+ else f"{millify_tao(metagraph_info.alpha_stake[idx])} {subnet_info.symbol}", # Alpha Stake
f"τ {tao_stake.tao:.4f}"
if verbose
else f"τ {millify_tao(tao_stake)}", # Tao Stake
- f"{subnet_state.dividends[idx]:.6f}", # Dividends
- f"{subnet_state.incentives[idx]:.6f}", # Incentive
- f"{Balance.from_tao(subnet_state.emission[idx].tao).set_unit(netuid_).tao:.6f} {subnet_info.symbol}", # Emissions
- f"{subnet_state.hotkeys[idx][:6]}"
+ f"{metagraph_info.dividends[idx]:.6f}", # Dividends
+ f"{metagraph_info.incentives[idx]:.6f}", # Incentive
+ f"{Balance.from_tao(metagraph_info.emission[idx].tao).set_unit(netuid_).tao:.6f} {subnet_info.symbol}", # Emissions
+ f"{metagraph_info.hotkeys[idx][:6]}"
if not verbose
- else f"{subnet_state.hotkeys[idx]}", # Hotkey
- f"{subnet_state.coldkeys[idx][:6]}"
+ else f"{metagraph_info.hotkeys[idx]}", # Hotkey
+ f"{metagraph_info.coldkeys[idx][:6]}"
if not verbose
- else f"{subnet_state.coldkeys[idx]}", # Coldkey
+ else f"{metagraph_info.coldkeys[idx]}", # Coldkey
uid_identity, # Identity
)
)
json_out_rows.append(
{
"uid": idx,
- "stake": subnet_state.total_stake[idx].tao,
- "alpha_stake": subnet_state.alpha_stake[idx].tao,
+ "stake": metagraph_info.total_stake[idx].tao,
+ "alpha_stake": metagraph_info.alpha_stake[idx].tao,
"tao_stake": tao_stake.tao,
- "dividends": subnet_state.dividends[idx],
- "incentive": subnet_state.incentives[idx],
- "emissions": Balance.from_tao(subnet_state.emission[idx].tao)
+ "dividends": metagraph_info.dividends[idx],
+ "incentive": metagraph_info.incentives[idx],
+ "emissions": Balance.from_tao(metagraph_info.emission[idx].tao)
.set_unit(netuid_)
.tao,
- "hotkey": subnet_state.hotkeys[idx],
- "coldkey": subnet_state.coldkeys[idx],
+ "hotkey": metagraph_info.hotkeys[idx],
+ "coldkey": metagraph_info.coldkeys[idx],
"identity": uid_identity,
}
)
@@ -1353,8 +1368,16 @@ async def show_subnet(netuid_: int):
if current_burn_cost
else Balance(0)
)
+ total_mechanisms = mechanism_count if mechanism_count is not None else 1
+
output_dict = {
"netuid": netuid_,
+ "mechanism_id": selected_mechanism_id,
+ **(
+ {"mechanism_count": mechanism_count}
+ if mechanism_count is not None
+ else {}
+ ),
"name": subnet_name_display,
"owner": subnet_info.owner_coldkey,
"owner_identity": owner_identity,
@@ -1372,8 +1395,21 @@ async def show_subnet(netuid_: int):
if json_output:
json_console.print(json.dumps(output_dict))
+ mech_line = (
+ f"\n Mechanism ID: [{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_1']}]#{selected_mechanism_id}"
+ f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_1']}]"
+ if total_mechanisms > 1
+ else ""
+ )
+ total_mech_line = (
+ f"\n Total mechanisms: [{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_2']}]"
+ f"{total_mechanisms}[/{COLOR_PALETTE['GENERAL']['SUBHEADING_EXTRA_2']}]"
+ )
+
console.print(
f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]Subnet {netuid_}{subnet_name_display}[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]"
+ f"{mech_line}"
+ f"{total_mech_line}"
f"\n Owner: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{subnet_info.owner_coldkey}{' (' + owner_identity + ')' if owner_identity else ''}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]"
f"\n Rate: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{subnet_info.price.tao:.4f} τ/{subnet_info.symbol}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]"
f"\n Emission: [{COLOR_PALETTE['GENERAL']['HOTKEY']}]τ {subnet_info.emission.tao:,.4f}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]"
@@ -1419,7 +1455,7 @@ async def show_subnet(netuid_: int):
# Check if the UID exists in the subnet
if uid in [int(row[0]) for row in rows]:
row_data = next(row for row in rows if int(row[0]) == uid)
- hotkey = subnet_state.hotkeys[uid]
+ hotkey = metagraph_info.hotkeys[uid]
identity = "" if row_data[9] == "~" else row_data[9]
identity_str = f" ({identity})" if identity else ""
console.print(
@@ -1439,7 +1475,7 @@ async def show_subnet(netuid_: int):
result = await show_root()
return result
else:
- result = await show_subnet(netuid)
+ result = await show_subnet(netuid, mechanism_id, mechanism_count)
return result
@@ -1486,13 +1522,17 @@ async def create(
"""Register a subnetwork"""
# Call register command.
- success, netuid = await register_subnetwork_extrinsic(
+ success, netuid, ext_id = await register_subnetwork_extrinsic(
subtensor, wallet, subnet_identity, prompt=prompt
)
if json_output:
# technically, netuid can be `None`, but only if not wait for finalization/inclusion. However, as of present
# (2025/04/03), we always use the default `wait_for_finalization=True`, so it will always have a netuid.
- json_console.print(json.dumps({"success": success, "netuid": netuid}))
+ json_console.print(
+ json.dumps(
+ {"success": success, "netuid": netuid, "extrinsic_identifier": ext_id}
+ )
+ )
return success
if success and prompt:
# Prompt for user to set identity.
@@ -1584,9 +1624,11 @@ async def register(
err_console.print(f"[red]Subnet {netuid} does not exist[/red]")
if json_output:
json_console.print(
- json.dumps(
- {"success": False, "error": f"Subnet {netuid} does not exist"}
- )
+ data={
+ "success": False,
+ "msg": f"Subnet {netuid} does not exist",
+ "extrinsic_identifier": None,
+ }
)
return
@@ -1604,9 +1646,12 @@ async def register(
# Check balance is sufficient
if balance < current_recycle:
- err_console.print(
- f"[red]Insufficient balance {balance} to register neuron. Current recycle is {current_recycle} TAO[/red]"
- )
+ err_msg = f"Insufficient balance {balance} to register neuron. Current recycle is {current_recycle} TAO"
+ err_console.print(f"[red]{err_msg}[/red]")
+ if json_output:
+ json_console.print_json(
+ data={"success": False, "msg": err_msg, "extrinsic_identifier": None}
+ )
return
if prompt and not json_output:
@@ -1671,9 +1716,9 @@ async def register(
return
if netuid == 0:
- success, msg = await root_register_extrinsic(subtensor, wallet=wallet)
+ success, msg, ext_id = await root_register_extrinsic(subtensor, wallet=wallet)
else:
- success, msg = await burned_register_extrinsic(
+ success, msg, ext_id = await burned_register_extrinsic(
subtensor,
wallet=wallet,
netuid=netuid,
@@ -1681,7 +1726,9 @@ async def register(
era=era,
)
if json_output:
- json_console.print(json.dumps({"success": success, "msg": msg}))
+ json_console.print(
+ json.dumps({"success": success, "msg": msg, "extrinsic_identifier": ext_id})
+ )
else:
if not success:
err_console.print(f"Failure: {msg}")
@@ -2207,12 +2254,12 @@ async def set_identity(
netuid: int,
subnet_identity: dict,
prompt: bool = False,
-) -> bool:
+) -> tuple[bool, Optional[str]]:
"""Set identity information for a subnet"""
if not await subtensor.subnet_exists(netuid):
err_console.print(f"Subnet {netuid} does not exist")
- return False
+ return False, None
identity_data = {
"netuid": netuid,
@@ -2227,13 +2274,13 @@ async def set_identity(
}
if not unlock_key(wallet).success:
- return False
+ return False, None
if prompt:
if not Confirm.ask(
"Are you sure you want to set subnet's identity? This is subject to a fee."
):
- return False
+ return False, None
call = await subtensor.substrate.compose_call(
call_module="SubtensorModule",
@@ -2245,12 +2292,15 @@ async def set_identity(
" :satellite: [dark_sea_green3]Setting subnet identity on-chain...",
spinner="earth",
):
- success, err_msg = await subtensor.sign_and_send_extrinsic(call, wallet)
+ success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic(
+ call, wallet
+ )
if not success:
err_console.print(f"[red]:cross_mark: Failed![/red] {err_msg}")
- return False
-
+ return False, None
+ ext_id = await ext_receipt.get_extrinsic_identifier()
+ await print_extrinsic_id(ext_receipt)
console.print(
":white_heavy_check_mark: [dark_sea_green3]Successfully set subnet identity\n"
)
@@ -2275,7 +2325,7 @@ async def set_identity(
table.add_row(key, str(value) if value else "~")
console.print(table)
- return True
+ return True, ext_id
async def get_identity(
@@ -2433,6 +2483,7 @@ async def start_subnet(
)
if await response.is_success:
+ await print_extrinsic_id(response)
console.print(
f":white_heavy_check_mark: [green]Successfully started subnet {netuid}'s emission schedule.[/green]"
)
@@ -2467,7 +2518,9 @@ async def set_symbol(
if not await subtensor.subnet_exists(netuid):
err = f"Subnet {netuid} does not exist."
if json_output:
- json_console.print_json(data={"success": False, "message": err})
+ json_console.print_json(
+ data={"success": False, "message": err, "extrinsic_identifier": None}
+ )
else:
err_console.print(err)
return False
@@ -2503,16 +2556,26 @@ async def set_symbol(
wait_for_inclusion=True,
)
if await response.is_success:
+ ext_id = await response.get_extrinsic_identifier()
+ await print_extrinsic_id(response)
message = f"Successfully updated SN{netuid}'s symbol to {symbol}."
if json_output:
- json_console.print_json(data={"success": True, "message": message})
+ json_console.print_json(
+ data={
+ "success": True,
+ "message": message,
+ "extrinsic_identifier": ext_id,
+ }
+ )
else:
console.print(f":white_heavy_check_mark:[dark_sea_green3] {message}\n")
return True
else:
err = format_error_message(await response.error_message)
if json_output:
- json_console.print_json(data={"success": False, "message": err})
+ json_console.print_json(
+ data={"success": False, "message": err, "extrinsic_identifier": None}
+ )
else:
err_console.print(f":cross_mark: [red]Failed[/red]: {err}")
return False
diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py
index e6ac31185..955f52435 100644
--- a/bittensor_cli/src/commands/sudo.py
+++ b/bittensor_cli/src/commands/sudo.py
@@ -2,6 +2,7 @@
import json
from typing import TYPE_CHECKING, Union, Optional
+from async_substrate_interface import AsyncExtrinsicReceipt
from bittensor_wallet import Wallet
from rich import box
from rich.table import Column, Table
@@ -27,6 +28,7 @@
string_to_u16,
string_to_u64,
get_hotkey_pub_ss58,
+ print_extrinsic_id,
)
if TYPE_CHECKING:
@@ -169,6 +171,84 @@ def requires_bool(metadata, param_name, pallet: str = DEFAULT_PALLET) -> bool:
raise ValueError(f"{param_name} not found in pallet.")
+async def set_mechanism_count_extrinsic(
+ subtensor: "SubtensorInterface",
+ wallet: "Wallet",
+ netuid: int,
+ mech_count: int,
+ wait_for_inclusion: bool = True,
+ wait_for_finalization: bool = True,
+) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]:
+ """Sets the number of mechanisms for a subnet via AdminUtils."""
+
+ unlock_result = unlock_key(wallet)
+ if not unlock_result.success:
+ return False, unlock_result.message, None
+
+ substrate = subtensor.substrate
+ call_params = {"netuid": netuid, "mechanism_count": mech_count}
+
+ with console.status(
+ f":satellite: Setting mechanism count to [white]{mech_count}[/white] on "
+ f"[{COLOR_PALETTE.G.SUBHEAD}]{netuid}[/{COLOR_PALETTE.G.SUBHEAD}] ...",
+ spinner="earth",
+ ):
+ call = await substrate.compose_call(
+ call_module=DEFAULT_PALLET,
+ call_function="sudo_set_mechanism_count",
+ call_params=call_params,
+ )
+ success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic(
+ call,
+ wallet,
+ wait_for_inclusion=wait_for_inclusion,
+ wait_for_finalization=wait_for_finalization,
+ )
+
+ if not success:
+ return False, err_msg, None
+
+ return True, "", ext_receipt
+
+
+async def set_mechanism_emission_extrinsic(
+ subtensor: "SubtensorInterface",
+ wallet: "Wallet",
+ netuid: int,
+ split: list[int],
+ wait_for_inclusion: bool = True,
+ wait_for_finalization: bool = True,
+) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]:
+ """Sets the emission split for a subnet's mechanisms via AdminUtils."""
+
+ unlock_result = unlock_key(wallet)
+ if not unlock_result.success:
+ return False, unlock_result.message, None
+
+ substrate = subtensor.substrate
+
+ with console.status(
+ f":satellite: Setting emission split for subnet {netuid}...",
+ spinner="earth",
+ ):
+ call = await substrate.compose_call(
+ call_module=DEFAULT_PALLET,
+ call_function="sudo_set_mechanism_emission_split",
+ call_params={"netuid": netuid, "maybe_split": split},
+ )
+ success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic(
+ call,
+ wallet,
+ wait_for_inclusion=wait_for_inclusion,
+ wait_for_finalization=wait_for_finalization,
+ )
+
+ if not success:
+ return False, err_msg, None
+
+ return True, "", ext_receipt
+
+
async def set_hyperparameter_extrinsic(
subtensor: "SubtensorInterface",
wallet: "Wallet",
@@ -178,7 +258,7 @@ async def set_hyperparameter_extrinsic(
wait_for_inclusion: bool = False,
wait_for_finalization: bool = True,
prompt: bool = True,
-) -> tuple[bool, str]:
+) -> tuple[bool, str, Optional[str]]:
"""Sets a hyperparameter for a specific subnetwork.
:param subtensor: initialized SubtensorInterface object
@@ -191,8 +271,11 @@ async def set_hyperparameter_extrinsic(
:param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning `True`,
or returns `False` if the extrinsic fails to be finalized within the timeout.
- :return: success: `True` if extrinsic was finalized or included in the block. If we did not wait for
+ :return: tuple including:
+ success: `True` if extrinsic was finalized or included in the block. If we did not wait for
finalization/inclusion, the response is `True`.
+ message: error message if the extrinsic failed
+ extrinsic_identifier: optional extrinsic identifier if the extrinsic was included
"""
print_verbose("Confirming subnet owner")
subnet_owner = await subtensor.query(
@@ -205,10 +288,10 @@ async def set_hyperparameter_extrinsic(
":cross_mark: [red]This wallet doesn't own the specified subnet.[/red]"
)
err_console.print(err_msg)
- return False, err_msg
+ return False, err_msg, None
if not (ulw := unlock_key(wallet)).success:
- return False, ulw.message
+ return False, ulw.message, None
arbitrary_extrinsic = False
@@ -227,7 +310,7 @@ async def set_hyperparameter_extrinsic(
if not Confirm.ask(
"This hyperparam is only settable by root sudo users. If you are not, this will fail. Please confirm"
):
- return False, "This hyperparam is only settable by root sudo users"
+ return False, "This hyperparam is only settable by root sudo users", None
substrate = subtensor.substrate
msg_value = value if not arbitrary_extrinsic else call_params
@@ -259,7 +342,7 @@ async def set_hyperparameter_extrinsic(
"Not enough values provided in the list for all parameters"
)
err_console.print(err_msg)
- return False, err_msg
+ return False, err_msg, None
call_params.update(
{name: val for name, val in zip(non_netuid_fields, value)}
@@ -287,25 +370,28 @@ async def set_hyperparameter_extrinsic(
)
else:
call = call_
- success, err_msg = await subtensor.sign_and_send_extrinsic(
+ success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic(
call, wallet, wait_for_inclusion, wait_for_finalization
)
if not success:
err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}")
- return False, err_msg
- elif arbitrary_extrinsic:
- console.print(
- f":white_heavy_check_mark: "
- f"[dark_sea_green3]Hyperparameter {parameter} values changed to {call_params}[/dark_sea_green3]"
- )
- return True, ""
- # Successful registration, final check for membership
+ return False, err_msg, None
else:
- console.print(
- f":white_heavy_check_mark: "
- f"[dark_sea_green3]Hyperparameter {parameter} changed to {value}[/dark_sea_green3]"
- )
- return True, ""
+ ext_id = await ext_receipt.get_extrinsic_identifier()
+ await print_extrinsic_id(ext_receipt)
+ if arbitrary_extrinsic:
+ console.print(
+ f":white_heavy_check_mark: "
+ f"[dark_sea_green3]Hyperparameter {parameter} values changed to {call_params}[/dark_sea_green3]"
+ )
+ return True, "", ext_id
+ # Successful registration, final check for membership
+ else:
+ console.print(
+ f":white_heavy_check_mark: "
+ f"[dark_sea_green3]Hyperparameter {parameter} changed to {value}[/dark_sea_green3]"
+ )
+ return True, "", ext_id
async def _get_senate_members(
@@ -506,7 +592,7 @@ async def vote_senate_extrinsic(
"approve": vote,
},
)
- success, err_msg = await subtensor.sign_and_send_extrinsic(
+ success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic(
call, wallet, wait_for_inclusion, wait_for_finalization
)
if not success:
@@ -515,6 +601,7 @@ async def vote_senate_extrinsic(
return False
# Successful vote, final check for data
else:
+ await print_extrinsic_id(ext_receipt)
if vote_data := await subtensor.get_vote_data(proposal_hash):
hotkey_ss58 = get_hotkey_pub_ss58(wallet)
if (
@@ -538,7 +625,7 @@ async def set_take_extrinsic(
wallet: Wallet,
delegate_ss58: str,
take: float = 0.0,
-) -> bool:
+) -> tuple[bool, Optional[str]]:
"""
Set delegate hotkey take
@@ -563,7 +650,7 @@ async def set_take_extrinsic(
if take_u16 == current_take_u16:
console.print("Nothing to do, take hasn't changed")
- return True
+ return True, None
if current_take_u16 < take_u16:
console.print(
@@ -581,7 +668,9 @@ async def set_take_extrinsic(
"take": take_u16,
},
)
- success, err = await subtensor.sign_and_send_extrinsic(call, wallet)
+ success, err, ext_receipt = await subtensor.sign_and_send_extrinsic(
+ call, wallet
+ )
else:
console.print(
@@ -599,15 +688,20 @@ async def set_take_extrinsic(
"take": take_u16,
},
)
- success, err = await subtensor.sign_and_send_extrinsic(call, wallet)
+ success, err, ext_receipt = await subtensor.sign_and_send_extrinsic(
+ call, wallet
+ )
if not success:
err_console.print(err)
+ ext_id = None
else:
console.print(
- ":white_heavy_check_mark: [dark_sea_green_3]Finalized[/dark_sea_green_3]"
+ ":white_heavy_check_mark: [dark_sea_green_3]Success[/dark_sea_green_3]"
)
- return success
+ ext_id = await ext_receipt.get_extrinsic_identifier()
+ await print_extrinsic_id(ext_receipt)
+ return success, ext_id
# commands
@@ -621,7 +715,7 @@ async def sudo_set_hyperparameter(
param_value: Optional[str],
prompt: bool,
json_output: bool,
-) -> tuple[bool, str]:
+) -> tuple[bool, str, Optional[str]]:
"""Set subnet hyperparameters."""
is_allowed_value, value = allowed_value(param_name, param_value)
if not is_allowed_value:
@@ -630,17 +724,17 @@ async def sudo_set_hyperparameter(
f"Value is {param_value} but must be {value}"
)
err_console.print(err_msg)
- return False, err_msg
- success, err_msg = await set_hyperparameter_extrinsic(
+ return False, err_msg, None
+ success, err_msg, ext_id = await set_hyperparameter_extrinsic(
subtensor, wallet, netuid, param_name, value, prompt=prompt
)
if json_output:
- return success, err_msg
+ return success, err_msg, ext_id
if success:
console.print("\n")
print_verbose("Fetching hyperparameters")
await get_hyperparameters(subtensor, netuid=netuid)
- return success, err_msg
+ return success, err_msg, ext_id
async def get_hyperparameters(
@@ -907,13 +1001,13 @@ async def display_current_take(subtensor: "SubtensorInterface", wallet: Wallet)
async def set_take(
wallet: Wallet, subtensor: "SubtensorInterface", take: float
-) -> bool:
+) -> tuple[bool, Optional[str]]:
"""Set delegate take."""
- async def _do_set_take() -> bool:
+ async def _do_set_take() -> tuple[bool, Optional[str]]:
if take > 0.18 or take < 0:
err_console.print("ERROR: Take value should not exceed 18% or be below 0%")
- return False
+ return False, None
block_hash = await subtensor.substrate.get_chain_head()
hotkey_ss58 = get_hotkey_pub_ss58(wallet)
@@ -926,32 +1020,97 @@ async def _do_set_take() -> bool:
f" any subnet. Please register using [{COLOR_PALETTE.G.SUBHEAD}]`btcli subnets register`"
f"[{COLOR_PALETTE.G.SUBHEAD}] and try again."
)
- return False
+ return False, None
- result: bool = await set_take_extrinsic(
+ result: tuple[bool, Optional[str]] = await set_take_extrinsic(
subtensor=subtensor,
wallet=wallet,
delegate_ss58=hotkey_ss58,
take=take,
)
+ success, ext_id = result
- if not result:
+ if not success:
err_console.print("Could not set the take")
- return False
+ return False, None
else:
new_take = await get_current_take(subtensor, wallet)
console.print(
f"New take is [{COLOR_PALETTE.P.RATE}]{new_take * 100.0:.2f}%"
)
- return True
+ return True, ext_id
console.print(
f"Setting take on [{COLOR_PALETTE.G.LINKS}]network: {subtensor.network}"
)
if not unlock_key(wallet, "hot").success and unlock_key(wallet, "cold").success:
- return False
+ return False, None
- result_ = await _do_set_take()
+ return await _do_set_take()
- return result_
+
+async def trim(
+ wallet: Wallet,
+ subtensor: "SubtensorInterface",
+ netuid: int,
+ max_n: int,
+ period: int,
+ prompt: bool,
+ json_output: bool,
+) -> bool:
+ """
+ Trims a subnet's UIDs to a specified amount
+ """
+ print_verbose("Confirming subnet owner")
+ subnet_owner = await subtensor.query(
+ module="SubtensorModule",
+ storage_function="SubnetOwner",
+ params=[netuid],
+ )
+ if subnet_owner != wallet.coldkeypub.ss58_address:
+ err_msg = "This wallet doesn't own the specified subnet."
+ if json_output:
+ json_console.print_json(data={"success": False, "message": err_msg})
+ else:
+ err_console.print(f":cross_mark: [red]{err_msg}[/red]")
+ return False
+ if prompt and not json_output:
+ if not Confirm.ask(
+ f"You are about to trim UIDs on SN{netuid} to a limit of {max_n}",
+ default=False,
+ ):
+ err_console.print(":cross_mark: [red]User aborted.[/red]")
+ call = await subtensor.substrate.compose_call(
+ call_module="AdminUtils",
+ call_function="sudo_trim_to_max_allowed_uids",
+ call_params={"netuid": netuid, "max_n": max_n},
+ )
+ success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic(
+ call=call, wallet=wallet, era={"period": period}
+ )
+ if not success:
+ if json_output:
+ json_console.print_json(
+ data={
+ "success": False,
+ "message": err_msg,
+ "extrinsic_identifier": None,
+ }
+ )
+ else:
+ err_console.print(f":cross_mark: [red]{err_msg}[/red]")
+ return False
+ else:
+ ext_id = await ext_receipt.get_extrinsic_identifier()
+ msg = f"Successfully trimmed UIDs on SN{netuid} to {max_n}"
+ if json_output:
+ json_console.print_json(
+ data={"success": True, "message": msg, "extrinsic_identifier": ext_id}
+ )
+ else:
+ await print_extrinsic_id(ext_receipt)
+ console.print(
+ f":white_heavy_check_mark: [dark_sea_green3]{msg}[/dark_sea_green3]"
+ )
+ return True
diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py
index 3b13a7fca..63edd7ef3 100644
--- a/bittensor_cli/src/commands/wallets.py
+++ b/bittensor_cli/src/commands/wallets.py
@@ -49,6 +49,7 @@
blocks_to_duration,
decode_account_id,
get_hotkey_pub_ss58,
+ print_extrinsic_id,
)
@@ -98,7 +99,7 @@ async def associate_hotkey(
)
with console.status(":satellite: Associating hotkey on-chain..."):
- success, err_msg = await subtensor.sign_and_send_extrinsic(
+ success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic(
call,
wallet,
wait_for_inclusion=True,
@@ -116,6 +117,7 @@ async def associate_hotkey(
f"wallet [blue]{wallet.name}[/blue], "
f"SS58: [{COLORS.GENERAL.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.GENERAL.CK}]"
)
+ await print_extrinsic_id(ext_receipt)
return True
@@ -811,13 +813,22 @@ async def wallet_history(wallet: Wallet):
console.print(table)
-async def wallet_list(wallet_path: str, json_output: bool):
+async def wallet_list(
+ wallet_path: str, json_output: bool, wallet_name: Optional[str] = None
+):
"""Lists wallets."""
wallets = utils.get_coldkey_wallets_for_path(wallet_path)
print_verbose(f"Using wallets path: {wallet_path}")
if not wallets:
err_console.print(f"[red]No wallets found in dir: {wallet_path}[/red]")
+ if wallet_name:
+ wallets = [wallet for wallet in wallets if wallet.name == wallet_name]
+ if not wallets:
+ err_console.print(
+ f"[red]Wallet '{wallet_name}' not found in dir: {wallet_path}[/red]"
+ )
+
root = Tree("Wallets")
main_data_dict = {"wallets": []}
for wallet in wallets:
@@ -874,7 +885,12 @@ async def wallet_list(wallet_path: str, json_output: bool):
if not wallets:
print_verbose(f"No wallets found in path: {wallet_path}")
- root.add("[bold red]No wallets found.")
+ message = (
+ "[bold red]No wallets found."
+ if not wallet_name
+ else f"[bold red]Wallet '{wallet_name}' not found."
+ )
+ root.add(message)
if json_output:
json_console.print(json.dumps(main_data_dict))
else:
@@ -1481,7 +1497,7 @@ async def transfer(
json_output: bool,
):
"""Transfer token of amount to destination."""
- result = await transfer_extrinsic(
+ result, ext_receipt = await transfer_extrinsic(
subtensor=subtensor,
wallet=wallet,
destination=destination,
@@ -1491,8 +1507,13 @@ async def transfer(
era=era,
prompt=prompt,
)
+ ext_id = (await ext_receipt.get_extrinsic_identifier()) if result else None
if json_output:
- json_console.print(json.dumps({"success": result}))
+ json_console.print(
+ json.dumps({"success": result, "extrinsic_identifier": ext_id})
+ )
+ else:
+ await print_extrinsic_id(ext_receipt)
return result
@@ -1682,15 +1703,23 @@ async def swap_hotkey(
json_output: bool,
):
"""Swap your hotkey for all registered axons on the network."""
- result = await swap_hotkey_extrinsic(
+ result, ext_receipt = await swap_hotkey_extrinsic(
subtensor,
original_wallet,
new_wallet,
netuid=netuid,
prompt=prompt,
)
+ if result:
+ ext_id = await ext_receipt.get_extrinsic_identifier()
+ else:
+ ext_id = None
if json_output:
- json_console.print(json.dumps({"success": result}))
+ json_console.print(
+ json.dumps({"success": result, "extrinsic_identifier": ext_id})
+ )
+ else:
+ await print_extrinsic_id(ext_receipt)
return result
@@ -1731,7 +1760,7 @@ async def set_id(
github_repo: str,
prompt: bool,
json_output: bool = False,
-):
+) -> bool:
"""Create a new or update existing identity on-chain."""
output_dict = {"success": False, "identity": None, "error": ""}
identity_data = {
@@ -1756,16 +1785,20 @@ async def set_id(
with console.status(
" :satellite: [dark_sea_green3]Updating identity on-chain...", spinner="earth"
):
- success, err_msg = await subtensor.sign_and_send_extrinsic(call, wallet)
+ success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic(
+ call, wallet
+ )
if not success:
err_console.print(f"[red]:cross_mark: Failed![/red] {err_msg}")
output_dict["error"] = err_msg
if json_output:
json_console.print(json.dumps(output_dict))
- return
+ return False
else:
console.print(":white_heavy_check_mark: [dark_sea_green3]Success!")
+ ext_id = await ext_receipt.get_extrinsic_identifier()
+ await print_extrinsic_id(ext_receipt)
output_dict["success"] = True
identity = await subtensor.query_identity(wallet.coldkeypub.ss58_address)
@@ -1774,9 +1807,12 @@ async def set_id(
for key, value in identity.items():
table.add_row(key, str(value) if value else "~")
output_dict["identity"] = identity
- console.print(table)
+ output_dict["extrinsic_identifier"] = ext_id
if json_output:
json_console.print(json.dumps(output_dict))
+ else:
+ console.print(table)
+ return True
async def get_id(
@@ -2016,9 +2052,9 @@ async def schedule_coldkey_swap(
},
),
)
-
+ swap_info = None
with console.status(":satellite: Scheduling coldkey swap on-chain..."):
- success, err_msg = await subtensor.sign_and_send_extrinsic(
+ success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic(
call,
wallet,
wait_for_inclusion=True,
@@ -2033,13 +2069,29 @@ async def schedule_coldkey_swap(
console.print(
":white_heavy_check_mark: [green]Successfully scheduled coldkey swap"
)
+ await print_extrinsic_id(ext_receipt)
+ for event in await ext_receipt.triggered_events:
+ if (
+ event.get("event", {}).get("module_id") == "SubtensorModule"
+ and event.get("event", {}).get("event_id") == "ColdkeySwapScheduled"
+ ):
+ attributes = event["event"].get("attributes", {})
+ old_coldkey = decode_account_id(attributes["old_coldkey"][0])
- swap_info = await find_coldkey_swap_extrinsic(
- subtensor=subtensor,
- start_block=block_pre_call,
- end_block=block_post_call,
- wallet_ss58=wallet.coldkeypub.ss58_address,
- )
+ if old_coldkey == wallet.coldkeypub.ss58_address:
+ swap_info = {
+ "block_num": block_pre_call,
+ "dest_coldkey": decode_account_id(attributes["new_coldkey"][0]),
+ "execution_block": attributes["execution_block"],
+ }
+
+ if not swap_info:
+ swap_info = await find_coldkey_swap_extrinsic(
+ subtensor=subtensor,
+ start_block=block_pre_call,
+ end_block=block_post_call,
+ wallet_ss58=wallet.coldkeypub.ss58_address,
+ )
if not swap_info:
console.print(
diff --git a/bittensor_cli/src/commands/weights.py b/bittensor_cli/src/commands/weights.py
index 63e3b72f3..b61bbd81f 100644
--- a/bittensor_cli/src/commands/weights.py
+++ b/bittensor_cli/src/commands/weights.py
@@ -2,7 +2,7 @@
import json
import os
from datetime import datetime, timedelta
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Optional
from bittensor_wallet import Wallet
import numpy as np
@@ -16,6 +16,7 @@
format_error_message,
json_console,
get_hotkey_pub_ss58,
+ print_extrinsic_id,
)
from bittensor_cli.src.bittensor.extrinsics.root import (
convert_weights_and_uids_for_emit,
@@ -54,7 +55,7 @@ def __init__(
self.wait_for_inclusion = wait_for_inclusion
self.wait_for_finalization = wait_for_finalization
- async def set_weights_extrinsic(self) -> tuple[bool, str]:
+ async def set_weights_extrinsic(self) -> tuple[bool, str, Optional[str]]:
"""
Sets the inter-neuronal weights for the specified neuron. This process involves specifying the
influence or trust a neuron places on other neurons in the network, which is a fundamental aspect
@@ -80,7 +81,7 @@ async def set_weights_extrinsic(self) -> tuple[bool, str]:
f"Do you want to set weights:\n[bold white]"
f" weights: {formatted_weight_vals}\n uids: {weight_uids}[/bold white ]?"
):
- return False, "Prompt refused."
+ return False, "Prompt refused.", None
# Check if the commit-reveal mechanism is active for the given netuid.
if bool(
@@ -104,7 +105,7 @@ async def commit_weights(
self,
uids: list[int],
weights: list[int],
- ) -> tuple[bool, str]:
+ ) -> tuple[bool, Optional[str], Optional[str]]:
"""
Commits a hash of the neuron's weights to the Bittensor blockchain using the provided wallet.
This action serves as a commitment or snapshot of the neuron's current weight distribution.
@@ -121,12 +122,6 @@ async def commit_weights(
enhancing transparency and accountability within the Bittensor network.
"""
- # _logger.info(
- # "Committing weights with params: netuid={}, uids={}, weights={}, version_key={}".format(
- # netuid, uids, weights, version_key
- # )
- # )
-
# Generate the hash of the weights
commit_hash = generate_weight_hash(
address=get_hotkey_pub_ss58(self.wallet),
@@ -139,18 +134,21 @@ async def commit_weights(
# _logger.info("Commit Hash: {}".format(commit_hash))
try:
- success, message = await self.do_commit_weights(commit_hash=commit_hash)
+ success, message, ext_id = await self.do_commit_weights(
+ commit_hash=commit_hash
+ )
except SubstrateRequestException as e:
err_console.print(f"Error committing weights: {format_error_message(e)}")
# bittensor.logging.error(f"Error committing weights: {e}")
success = False
message = "No attempt made. Perhaps it is too soon to commit weights!"
+ ext_id = None
- return success, message
+ return success, message, ext_id
async def _commit_reveal(
self, weight_uids: list[int], weight_vals: list[int]
- ) -> tuple[bool, str]:
+ ) -> tuple[bool, str, Optional[str]]:
interval = int(
await self.subtensor.get_hyperparameter(
param_name="get_commit_reveal_period",
@@ -165,7 +163,7 @@ async def _commit_reveal(
self.salt = list(os.urandom(salt_length))
# Attempt to commit the weights to the blockchain.
- commit_success, commit_msg = await self.commit_weights(
+ commit_success, commit_msg, ext_id = await self.commit_weights(
uids=weight_uids,
weights=weight_vals,
)
@@ -209,36 +207,42 @@ async def _commit_reveal(
console.print(f":cross_mark: [red]Failed[/red]: error:{commit_msg}")
# bittensor.logging.error(msg=commit_msg, prefix="Set weights with hash commit",
# suffix=f"Failed: {commit_msg}")
- return False, f"Failed to commit weights hash. {commit_msg}"
+ return False, f"Failed to commit weights hash. {commit_msg}", None
- async def reveal(self, weight_uids, weight_vals) -> tuple[bool, str]:
+ async def reveal(self, weight_uids, weight_vals) -> tuple[bool, str, Optional[str]]:
# Attempt to reveal the weights using the salt.
- success, msg = await self.reveal_weights_extrinsic(weight_uids, weight_vals)
+ success, msg, ext_id = await self.reveal_weights_extrinsic(
+ weight_uids, weight_vals
+ )
if success:
if not self.wait_for_finalization and not self.wait_for_inclusion:
- return True, "Not waiting for finalization or inclusion."
+ return True, "Not waiting for finalization or inclusion.", ext_id
console.print(
":white_heavy_check_mark: [green]Weights hash revealed on chain[/green]"
)
# bittensor.logging.success(prefix="Weights hash revealed", suffix=str(msg))
- return True, "Successfully revealed previously commited weights hash."
+ return (
+ True,
+ "Successfully revealed previously commited weights hash.",
+ ext_id,
+ )
else:
# bittensor.logging.error(
# msg=msg,
# prefix=f"Failed to reveal previously commited weights hash for salt: {salt}",
# suffix="Failed: ",
# )
- return False, "Failed to reveal weights."
+ return False, "Failed to reveal weights.", None
async def _set_weights_without_commit_reveal(
self,
weight_uids,
weight_vals,
- ) -> tuple[bool, str]:
- async def _do_set_weights():
+ ) -> tuple[bool, str, Optional[str]]:
+ async def _do_set_weights() -> tuple[bool, str, Optional[str]]:
call = await self.subtensor.substrate.compose_call(
call_module="SubtensorModule",
call_function="set_weights",
@@ -262,37 +266,39 @@ async def _do_set_weights():
wait_for_finalization=self.wait_for_finalization,
)
except SubstrateRequestException as e:
- return False, format_error_message(e)
+ return False, format_error_message(e), None
# We only wait here if we expect finalization.
if not self.wait_for_finalization and not self.wait_for_inclusion:
- return True, "Not waiting for finalization or inclusion."
+ return True, "Not waiting for finalization or inclusion.", None
if await response.is_success:
- return True, "Successfully set weights."
+ ext_id_ = await response.get_extrinsic_identifier()
+ await print_extrinsic_id(response)
+ return True, "Successfully set weights.", ext_id_
else:
- return False, format_error_message(await response.error_message)
+ return False, format_error_message(await response.error_message), None
with console.status(
f":satellite: Setting weights on [white]{self.subtensor.network}[/white] ..."
):
- success, error_message = await _do_set_weights()
+ success, error_message, ext_id = await _do_set_weights()
if not self.wait_for_finalization and not self.wait_for_inclusion:
- return True, "Not waiting for finalization or inclusion."
+ return True, "Not waiting for finalization or inclusion.", None
if success:
console.print(":white_heavy_check_mark: [green]Finalized[/green]")
# bittensor.logging.success(prefix="Set weights", suffix="Finalized: " + str(success))
- return True, "Successfully set weights and finalized."
+ return True, "Successfully set weights and finalized.", ext_id
else:
# bittensor.logging.error(msg=error_message, prefix="Set weights", suffix="Failed: ")
- return False, error_message
+ return False, error_message, None
async def reveal_weights_extrinsic(
self, weight_uids, weight_vals
- ) -> tuple[bool, str]:
+ ) -> tuple[bool, str, Optional[str]]:
if self.prompt and not Confirm.ask("Would you like to reveal weights?"):
- return False, "User cancelled the operation."
+ return False, "User cancelled the operation.", None
call = await self.subtensor.substrate.compose_call(
call_module="SubtensorModule",
@@ -316,28 +322,36 @@ async def reveal_weights_extrinsic(
wait_for_finalization=self.wait_for_finalization,
)
except SubstrateRequestException as e:
- return False, format_error_message(e)
+ return False, format_error_message(e), None
if not self.wait_for_finalization and not self.wait_for_inclusion:
- success, error_message = True, ""
+ success, error_message, ext_id = True, "", None
else:
if await response.is_success:
- success, error_message = True, ""
+ success, error_message, ext_id = (
+ True,
+ "",
+ await response.get_extrinsic_identifier(),
+ )
+ await print_extrinsic_id(response)
else:
- success, error_message = (
+ success, error_message, ext_id = (
False,
format_error_message(await response.error_message),
+ None,
)
if success:
# bittensor.logging.info("Successfully revealed weights.")
- return True, "Successfully revealed weights."
+ return True, "Successfully revealed weights.", ext_id
else:
# bittensor.logging.error(f"Failed to reveal weights: {error_message}")
- return False, error_message
+ return False, error_message, ext_id
- async def do_commit_weights(self, commit_hash):
+ async def do_commit_weights(
+ self, commit_hash
+ ) -> tuple[bool, Optional[str], Optional[str]]:
call = await self.subtensor.substrate.compose_call(
call_module="SubtensorModule",
call_function="commit_weights",
@@ -357,12 +371,14 @@ async def do_commit_weights(self, commit_hash):
)
if not self.wait_for_finalization and not self.wait_for_inclusion:
- return True, None
+ return True, None, None
if await response.is_success:
- return True, None
+ ext_id = await response.get_extrinsic_identifier()
+ await print_extrinsic_id(response)
+ return True, None, ext_id
else:
- return False, await response.error_message
+ return False, await response.error_message, None
# commands
@@ -399,9 +415,13 @@ async def reveal_weights(
extrinsic = SetWeightsExtrinsic(
subtensor, wallet, netuid, uids_, weights_, list(salt_), version, prompt=prompt
)
- success, message = await extrinsic.reveal(weight_uids, weight_vals)
+ success, message, ext_id = await extrinsic.reveal(weight_uids, weight_vals)
if json_output:
- json_console.print(json.dumps({"success": success, "message": message}))
+ json_console.print(
+ json.dumps(
+ {"success": success, "message": message, "extrinsic_identifier": ext_id}
+ )
+ )
else:
if success:
console.print("Weights revealed successfully")
@@ -436,9 +456,13 @@ async def commit_weights(
extrinsic = SetWeightsExtrinsic(
subtensor, wallet, netuid, uids_, weights_, list(salt_), version, prompt=prompt
)
- success, message = await extrinsic.set_weights_extrinsic()
+ success, message, ext_id = await extrinsic.set_weights_extrinsic()
if json_output:
- json_console.print(json.dumps({"success": success, "message": message}))
+ json_console.print(
+ json.dumps(
+ {"success": success, "message": message, "extrinsic_identifier": ext_id}
+ )
+ )
else:
if success:
console.print("Weights set successfully")
diff --git a/pyproject.toml b/pyproject.toml
index b7c11b579..234acba94 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "bittensor-cli"
-version = "9.11.2"
+version = "9.12.0"
description = "Bittensor CLI"
readme = "README.md"
authors = [
diff --git a/tests/e2e_tests/test_hyperparams_setting.py b/tests/e2e_tests/test_hyperparams_setting.py
index 3af86c140..24f83bdfe 100644
--- a/tests/e2e_tests/test_hyperparams_setting.py
+++ b/tests/e2e_tests/test_hyperparams_setting.py
@@ -62,7 +62,7 @@ def test_hyperparams_setting(local_chain, wallet_setup):
result_output = json.loads(result.stdout)
assert result_output["success"] is True
assert result_output["netuid"] == netuid
- print(result_output)
+ assert isinstance(result_output["extrinsic_identifier"], str)
# Fetch the hyperparameters of the subnet
hyperparams = exec_command_alice(
@@ -119,5 +119,57 @@ def test_hyperparams_setting(local_chain, wallet_setup):
)
cmd_json = json.loads(cmd.stdout)
assert cmd_json["success"] is True, (key, new_val, cmd.stdout, cmd_json)
+ assert isinstance(cmd_json["extrinsic_identifier"], str)
print(f"Successfully set hyperparameter {key} to value {new_val}")
+ # also test hidden hyperparam
+ cmd = exec_command_alice(
+ command="sudo",
+ sub_command="set",
+ extra_args=[
+ "--wallet-path",
+ wallet_path_alice,
+ "--network",
+ "ws://127.0.0.1:9945",
+ "--wallet-name",
+ wallet_alice.name,
+ "--wallet-hotkey",
+ wallet_alice.hotkey_str,
+ "--netuid",
+ netuid,
+ "--json-out",
+ "--no-prompt",
+ "--param",
+ "min_allowed_uids",
+ "--value",
+ "110",
+ ],
+ )
+ cmd_json = json.loads(cmd.stdout)
+ assert cmd_json["success"] is True, (cmd.stdout, cmd_json)
+ assert isinstance(cmd_json["extrinsic_identifier"], str)
print("Successfully set hyperparameters")
+ print("Testing trimming UIDs")
+ cmd = exec_command_alice(
+ command="sudo",
+ sub_command="trim",
+ extra_args=[
+ "--wallet-path",
+ wallet_path_alice,
+ "--network",
+ "ws://127.0.0.1:9945",
+ "--wallet-name",
+ wallet_alice.name,
+ "--wallet-hotkey",
+ wallet_alice.hotkey_str,
+ "--netuid",
+ netuid,
+ "--max",
+ "120",
+ "--json-out",
+ "--no-prompt",
+ ],
+ )
+ cmd_json = json.loads(cmd.stdout)
+ assert cmd_json["success"] is True, (cmd.stdout, cmd_json)
+ assert isinstance(cmd_json["extrinsic_identifier"], str)
+ print("Successfully trimmed UIDs")
diff --git a/tests/e2e_tests/test_liquidity.py b/tests/e2e_tests/test_liquidity.py
index 218ef91f0..faaf6e05e 100644
--- a/tests/e2e_tests/test_liquidity.py
+++ b/tests/e2e_tests/test_liquidity.py
@@ -85,6 +85,7 @@ def liquidity_list():
result_output = json.loads(result.stdout)
assert result_output["success"] is True
assert result_output["netuid"] == netuid
+ assert isinstance(result_output["extrinsic_identifier"], str)
# verify no results for list thus far (subnet not yet started)
liquidity_list_result = liquidity_list()
@@ -115,6 +116,7 @@ def liquidity_list():
f"Successfully started subnet {netuid}'s emission schedule"
in start_subnet_emissions.stdout
), start_subnet_emissions.stderr
+ assert "Your extrinsic has been included " in start_subnet_emissions.stdout
liquidity_list_result = liquidity_list()
result_output = json.loads(liquidity_list_result.stdout)
@@ -146,6 +148,7 @@ def liquidity_list():
)
enable_user_liquidity_result = json.loads(enable_user_liquidity.stdout)
assert enable_user_liquidity_result["success"] is True
+ assert isinstance(enable_user_liquidity_result["extrinsic_identifier"], str)
add_liquidity = exec_command_alice(
command="liquidity",
@@ -174,6 +177,7 @@ def liquidity_list():
add_liquidity_result = json.loads(add_liquidity.stdout)
assert add_liquidity_result["success"] is True
assert add_liquidity_result["message"] == ""
+ assert isinstance(add_liquidity_result["extrinsic_identifier"], str)
liquidity_list_result = liquidity_list()
liquidity_list_result = json.loads(liquidity_list_result.stdout)
@@ -212,6 +216,7 @@ def liquidity_list():
)
modify_liquidity_result = json.loads(modify_liquidity.stdout)
assert modify_liquidity_result["success"] is True
+ assert isinstance(modify_liquidity_result["extrinsic_identifier"], str)
liquidity_list_result = json.loads(liquidity_list().stdout)
assert len(liquidity_list_result["positions"]) == 1
@@ -240,6 +245,9 @@ def liquidity_list():
)
removal_result = json.loads(removal.stdout)
assert removal_result[str(liquidity_position["id"])]["success"] is True
+ assert isinstance(
+ removal_result[str(liquidity_position["id"])]["extrinsic_identifier"], str
+ )
liquidity_list_result = json.loads(liquidity_list().stdout)
assert liquidity_list_result["success"] is True
diff --git a/tests/e2e_tests/test_senate.py b/tests/e2e_tests/test_senate.py
index 4cf12bb2b..c4cbebd7c 100644
--- a/tests/e2e_tests/test_senate.py
+++ b/tests/e2e_tests/test_senate.py
@@ -77,6 +77,9 @@ def test_senate(local_chain, wallet_setup):
],
)
assert "✅ Registered" in root_register.stdout, root_register.stderr
+ assert "Your extrinsic has been included " in root_register.stdout, (
+ root_register.stderr
+ )
# Fetch the senate members after registering to root
root_senate_after_reg = exec_command_bob(
@@ -156,6 +159,7 @@ def test_senate(local_chain, wallet_setup):
],
)
assert "✅ Vote cast" in vote_aye.stdout
+ assert "Your extrinsic has been included " in vote_aye.stdout
# Fetch proposals after voting aye
proposals_after_aye = exec_command_bob(
@@ -219,6 +223,7 @@ def test_senate(local_chain, wallet_setup):
],
)
assert "✅ Registered" in root_register.stdout
+ assert "Your extrinsic has been included " in root_register.stdout
# Vote on the proposal by Alice (vote nay)
vote_nay = exec_command_alice(
@@ -240,6 +245,7 @@ def test_senate(local_chain, wallet_setup):
],
)
assert "✅ Vote cast" in vote_nay.stdout
+ assert "Your extrinsic has been included " in vote_nay.stdout
# Fetch proposals after voting
proposals_after_nay = exec_command_bob(
diff --git a/tests/e2e_tests/test_staking_sudo.py b/tests/e2e_tests/test_staking_sudo.py
index 9034c51da..4096c87b0 100644
--- a/tests/e2e_tests/test_staking_sudo.py
+++ b/tests/e2e_tests/test_staking_sudo.py
@@ -1,6 +1,7 @@
import asyncio
import json
import re
+from typing import Union
from bittensor_cli.src.bittensor.balances import Balance
from .utils import turn_off_hyperparam_freeze_window
@@ -105,6 +106,7 @@ def test_staking(local_chain, wallet_setup):
result_output = json.loads(result.stdout)
assert result_output["success"] is True
assert result_output["netuid"] == netuid
+ assert isinstance(result_output["extrinsic_identifier"], str)
# Register another subnet with sudo as Alice
result_for_second_repo = exec_command_alice(
@@ -142,6 +144,7 @@ def test_staking(local_chain, wallet_setup):
result_output_second = json.loads(result_for_second_repo.stdout)
assert result_output_second["success"] is True
assert result_output_second["netuid"] == multiple_netuids[1]
+ assert isinstance(result_output_second["extrinsic_identifier"], str)
# Register Alice in netuid = 2 using her hotkey
register_subnet = exec_command_alice(
@@ -162,6 +165,7 @@ def test_staking(local_chain, wallet_setup):
],
)
assert "✅ Already Registered" in register_subnet.stdout
+ assert "Your extrinsic has been included" not in register_subnet.stdout
register_subnet_json = exec_command_alice(
command="subnets",
@@ -184,6 +188,7 @@ def test_staking(local_chain, wallet_setup):
register_subnet_json_output = json.loads(register_subnet_json.stdout)
assert register_subnet_json_output["success"] is True
assert register_subnet_json_output["msg"] == "Already registered"
+ assert register_subnet_json_output["extrinsic_identifier"] is None
# set identity
set_identity = exec_command_alice(
@@ -222,6 +227,7 @@ def test_staking(local_chain, wallet_setup):
)
set_identity_output = json.loads(set_identity.stdout)
assert set_identity_output["success"] is True
+ assert isinstance(set_identity_output["extrinsic_identifier"], str)
get_identity = exec_command_alice(
"subnets",
@@ -271,6 +277,7 @@ def test_staking(local_chain, wallet_setup):
set_symbol_output["message"]
== f"Successfully updated SN{netuid}'s symbol to シ."
)
+ assert isinstance(set_identity_output["extrinsic_identifier"], str)
get_s_price = exec_command_alice(
"subnets",
@@ -314,6 +321,9 @@ def test_staking(local_chain, wallet_setup):
f"Successfully started subnet {netuid_}'s emission schedule"
in start_subnet_emissions.stdout
), start_subnet_emissions.stderr
+ assert "Your extrinsic has been included" in start_subnet_emissions.stdout, (
+ start_subnet_emissions.stdout
+ )
# Add stake to Alice's hotkey
add_stake_single = exec_command_alice(
@@ -341,6 +351,9 @@ def test_staking(local_chain, wallet_setup):
],
)
assert "✅ Finalized" in add_stake_single.stdout, add_stake_single.stderr
+ assert "Your extrinsic has been included" in add_stake_single.stdout, (
+ add_stake_single.stdout
+ )
# Execute stake show for Alice's wallet
show_stake_adding_single = exec_command_alice(
@@ -408,6 +421,9 @@ def test_staking(local_chain, wallet_setup):
],
)
assert "✅ Finalized" in remove_stake.stdout
+ assert "Your extrinsic has been included" in remove_stake.stdout, (
+ remove_stake.stdout
+ )
add_stake_multiple = exec_command_alice(
command="stake",
@@ -436,18 +452,15 @@ def test_staking(local_chain, wallet_setup):
)
add_stake_multiple_output = json.loads(add_stake_multiple.stdout)
for netuid_ in multiple_netuids:
- assert (
- add_stake_multiple_output["staking_success"][str(netuid_)][
- wallet_alice.hotkey.ss58_address
- ]
- is True
- )
- assert (
- add_stake_multiple_output["error_messages"][str(netuid_)][
+
+ def line(key: str) -> Union[str, bool]:
+ return add_stake_multiple_output[key][str(netuid_)][
wallet_alice.hotkey.ss58_address
]
- == ""
- )
+
+ assert line("staking_success") is True
+ assert line("error_messages") == ""
+ assert isinstance(line("extrinsic_ids"), str)
# Fetch the hyperparameters of the subnet
hyperparams = exec_command_alice(
@@ -507,6 +520,9 @@ def test_staking(local_chain, wallet_setup):
assert (
"✅ Hyperparameter max_burn changed to 10000000000" in change_hyperparams.stdout
)
+ assert "Your extrinsic has been included" in change_hyperparams.stdout, (
+ change_hyperparams.stdout
+ )
# Fetch the hyperparameters again to verify
updated_hyperparams = exec_command_alice(
@@ -576,6 +592,7 @@ def test_staking(local_chain, wallet_setup):
assert change_yuma3_hyperparam_json["success"] is True, (
change_yuma3_hyperparam.stdout
)
+ assert isinstance(change_yuma3_hyperparam_json["extrinsic_identifier"], str)
changed_yuma3_hyperparam = exec_command_alice(
command="sudo",
@@ -626,3 +643,4 @@ def test_staking(local_chain, wallet_setup):
change_arbitrary_hyperparam.stdout,
change_arbitrary_hyperparam.stderr,
)
+ assert isinstance(change_yuma3_hyperparam_json["extrinsic_identifier"], str)
diff --git a/tests/e2e_tests/test_unstaking.py b/tests/e2e_tests/test_unstaking.py
index 4b7ca0765..d4e3eef76 100644
--- a/tests/e2e_tests/test_unstaking.py
+++ b/tests/e2e_tests/test_unstaking.py
@@ -89,6 +89,7 @@ def test_unstaking(local_chain, wallet_setup):
],
)
assert "✅ Registered subnetwork with netuid: 2" in result.stdout
+ assert "Your extrinsic has been included" in result.stdout, result.stdout
# Create second subnet (netuid = 3)
result = exec_command_alice(
@@ -123,6 +124,7 @@ def test_unstaking(local_chain, wallet_setup):
],
)
assert "✅ Registered subnetwork with netuid: 3" in result.stdout
+ assert "Your extrinsic has been included" in result.stdout, result.stdout
# Start emission schedule for subnets
start_call_netuid_0 = exec_command_alice(
@@ -144,6 +146,9 @@ def test_unstaking(local_chain, wallet_setup):
"Successfully started subnet 0's emission schedule."
in start_call_netuid_0.stdout
)
+ assert "Your extrinsic has been included" in start_call_netuid_0.stdout, (
+ start_call_netuid_0.stdout
+ )
start_call_netuid_2 = exec_command_alice(
command="subnets",
sub_command="start",
@@ -163,6 +168,7 @@ def test_unstaking(local_chain, wallet_setup):
"Successfully started subnet 2's emission schedule."
in start_call_netuid_2.stdout
)
+ assert "Your extrinsic has been included" in start_call_netuid_2.stdout
start_call_netuid_3 = exec_command_alice(
command="subnets",
@@ -183,6 +189,7 @@ def test_unstaking(local_chain, wallet_setup):
"Successfully started subnet 3's emission schedule."
in start_call_netuid_3.stdout
)
+ assert "Your extrinsic has been included" in start_call_netuid_3.stdout
# Register Bob in one subnet
register_result = exec_command_bob(
command="subnets",
@@ -204,6 +211,9 @@ def test_unstaking(local_chain, wallet_setup):
],
)
assert "✅ Registered" in register_result.stdout, register_result.stderr
+ assert "Your extrinsic has been included" in register_result.stdout, (
+ register_result.stdout
+ )
# Add stake to subnets
for netuid in [0, 2, 3]:
@@ -232,6 +242,9 @@ def test_unstaking(local_chain, wallet_setup):
],
)
assert "✅ Finalized" in stake_result.stdout, stake_result.stderr
+ assert "Your extrinsic has been included" in stake_result.stdout, (
+ stake_result.stdout
+ )
stake_list = exec_command_bob(
command="stake",
@@ -279,6 +292,9 @@ def test_unstaking(local_chain, wallet_setup):
],
)
assert "✅ Finalized" in partial_unstake_netuid_2.stdout
+ assert "Your extrinsic has been included" in partial_unstake_netuid_2.stdout, (
+ partial_unstake_netuid_2.stdout
+ )
# Verify partial unstake
stake_list = exec_command_bob(
@@ -348,6 +364,9 @@ def test_unstaking(local_chain, wallet_setup):
assert (
"✅ Finalized: Successfully unstaked all Alpha stakes" in unstake_alpha.stdout
)
+ assert "Your extrinsic has been included" in unstake_alpha.stdout, (
+ unstake_alpha.stdout
+ )
# Add stake again to subnets
for netuid in [0, 2, 3]:
@@ -376,6 +395,7 @@ def test_unstaking(local_chain, wallet_setup):
],
)
assert "✅ Finalized" in stake_result.stdout
+ assert "Your extrinsic has been included" in stake_result.stdout
# Remove all stakes
unstake_all = exec_command_bob(
@@ -397,4 +417,5 @@ def test_unstaking(local_chain, wallet_setup):
],
)
assert "✅ Finalized: Successfully unstaked all stakes from" in unstake_all.stdout
+ assert "Your extrinsic has been included" in unstake_all.stdout, unstake_all.stdout
print("Passed unstaking tests 🎉")
diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py
index e6a4bb22d..9dd3ea4f1 100644
--- a/tests/e2e_tests/test_wallet_interactions.py
+++ b/tests/e2e_tests/test_wallet_interactions.py
@@ -443,17 +443,19 @@ def test_wallet_identities(local_chain, wallet_setup):
assert "✅ Success!" in set_id.stdout
set_id_output = set_id.stdout.splitlines()
- assert alice_identity["name"] in set_id_output[6]
- assert alice_identity["url"] in set_id_output[7]
- assert alice_identity["github_repo"] in set_id_output[8]
- assert alice_identity["image"] in set_id_output[9]
- assert alice_identity["discord"] in set_id_output[10]
- assert alice_identity["description"] in set_id_output[11]
- assert alice_identity["additional"] in set_id_output[12]
+ assert "Your extrinsic has been included as" in set_id_output[1]
+
+ assert alice_identity["name"] in set_id_output[7]
+ assert alice_identity["url"] in set_id_output[8]
+ assert alice_identity["github_repo"] in set_id_output[9]
+ assert alice_identity["image"] in set_id_output[10]
+ assert alice_identity["discord"] in set_id_output[11]
+ assert alice_identity["description"] in set_id_output[12]
+ assert alice_identity["additional"] in set_id_output[13]
# TODO: Currently coldkey + hotkey are the same for test wallets.
# Maybe we can add a new key to help in distinguishing
- assert wallet_alice.coldkeypub.ss58_address in set_id_output[5]
+ assert wallet_alice.coldkeypub.ss58_address in set_id_output[6]
# Execute btcli get-identity using hotkey
get_identity = exec_command_alice(