Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions bittensor_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -1164,6 +1164,9 @@ def __init__(self):
self.sudo_app.command("trim", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"])(
self.sudo_trim
)
self.sudo_app.command(
"stake-burn", rich_help_panel=HELP_PANELS["SUDO"]["CONFIG"]
)(self.sudo_stake_burn)

# subnets commands
self.subnets_app.command(
Expand Down Expand Up @@ -1298,6 +1301,7 @@ def __init__(self):
self.sudo_app.command("senate_vote", hidden=True)(self.sudo_senate_vote)
self.sudo_app.command("get_take", hidden=True)(self.sudo_get_take)
self.sudo_app.command("set_take", hidden=True)(self.sudo_set_take)
self.sudo_app.command("buyback", hidden=True)(self.sudo_stake_burn)

# Stake
self.stake_app.command(
Expand Down Expand Up @@ -7382,6 +7386,107 @@ def sudo_trim(
)
)

def sudo_stake_burn(
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_ss58,
netuid: int = Options.netuid,
amount: float = typer.Option(
None,
"--amount",
"-a",
help="Amount of TAO to stake and burn",
prompt="Enter the amount of TAO to stake and burn",
),
proxy: Optional[str] = Options.proxy,
rate_tolerance: Optional[float] = Options.rate_tolerance,
safe_staking: Optional[bool] = Options.safe_staking,
mev_protection: bool = Options.mev_protection,
quiet: bool = Options.quiet,
verbose: bool = Options.verbose,
json_output: bool = Options.json_output,
prompt: bool = Options.prompt,
decline: bool = Options.decline,
period: int = Options.period,
):
"""
Allows subnet owners to buy back alpha on their subnet by staking TAO and immediately burning the acquired alpha.

[bold]Examples:[/bold]
1. Stake and burn 10 TAO on subnet 14:
[green]$[/green] btcli sudo stake-burn --netuid 14 --amount 10
2. Stake and burn 10 TAO on subnet 14 with safe staking and 5% rate tolerance:
[green]$[/green] btcli sudo stake-burn --netuid 14 --amount 10 --tolerance 0.05
3. Stake and burn 10 TAO on subnet 14 with a specific hotkey:
[green]$[/green] btcli sudo stake-burn --netuid 14 --amount 10 --wallet-hotkey <HOTKEY_SS58>
"""
self.verbosity_handler(quiet, verbose, json_output, prompt)
proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only=False)
safe_staking = self.ask_safe_staking(safe_staking)
if safe_staking:
rate_tolerance = self.ask_rate_tolerance(rate_tolerance)

print_protection_warnings(
mev_protection=mev_protection,
safe_staking=safe_staking,
command_name="sudo stake-burn",
)

if not wallet_hotkey:
wallet_hotkey = Prompt.ask(
"Enter the [blue]hotkey[/blue] name or "
"[blue]hotkey ss58 address[/blue] [dim](to use for the stake burn)[/dim]",
default=self.config.get("wallet_hotkey") or defaults.wallet.hotkey,
)

if wallet_hotkey and is_valid_ss58_address(wallet_hotkey):
hotkey_ss58 = wallet_hotkey
wallet = self.wallet_ask(
wallet_name,
wallet_path,
None,
ask_for=[WO.NAME, WO.PATH],
validate=WV.WALLET,
)
else:
wallet = self.wallet_ask(
wallet_name,
wallet_path,
wallet_hotkey,
ask_for=[WO.NAME, WO.PATH, WO.HOTKEY],
validate=WV.WALLET_AND_HOTKEY,
)
hotkey_ss58 = get_hotkey_pub_ss58(wallet)

if amount <= 0:
print_error(f"You entered an incorrect stake and burn amount: {amount}")
raise typer.Exit()

if netuid == 0:
print_error("Cannot stake and burn on the root subnet.")
raise typer.Exit()

self._run_command(
sudo.stake_burn(
subtensor=self.initialize_chain(network),
wallet=wallet,
netuid=netuid,
amount=amount,
hotkey_ss58=hotkey_ss58,
safe_staking=safe_staking,
proxy=proxy,
rate_tolerance=rate_tolerance,
mev_protection=mev_protection,
json_output=json_output,
prompt=prompt,
decline=decline,
quiet=quiet,
period=period,
)
)

# Subnets

def subnets_list(
Expand Down
236 changes: 236 additions & 0 deletions bittensor_cli/src/commands/sudo.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,16 @@
DelegatesDetails,
COLOR_PALETTE,
)
from bittensor_cli.src.bittensor.balances import Balance
from bittensor_cli.src.bittensor.extrinsics.mev_shield import (
extract_mev_shield_id,
wait_for_extrinsic_by_hash,
)
from bittensor_cli.src.bittensor.chain_data import decode_account_id
from bittensor_cli.src.bittensor.utils import (
confirm_action,
console,
create_table,
print_error,
print_success,
print_verbose,
Expand Down Expand Up @@ -94,6 +100,236 @@ def string_to_bool(val) -> Union[bool, Type[ValueError]]:
return ValueError


async def stake_burn(
wallet: Wallet,
subtensor: "SubtensorInterface",
netuid: int,
amount: float,
hotkey_ss58: Optional[str],
safe_staking: bool,
proxy: Optional[str],
rate_tolerance: Optional[float],
mev_protection: bool,
json_output: bool,
prompt: bool,
decline: bool,
quiet: bool,
period: int,
) -> bool:
"""
Perform a stake burn (owner-only).
Stakes TAO into the subnet and immediately burns the acquired alpha.
"""
subnet_owner = await subtensor.query(
module="SubtensorModule",
storage_function="SubnetOwner",
params=[netuid],
)
if subnet_owner != wallet.coldkeypub.ss58_address:
err_msg = (
f"Coldkey {wallet.coldkeypub.ss58_address} does not own subnet {netuid}."
)
if json_output:
json_console.print_json(
data={
"success": False,
"message": err_msg,
"extrinsic_identifier": None,
}
)
else:
print_error(err_msg)
return False

subnet_info = await subtensor.subnet(netuid=netuid)
stake_burn_amount = Balance.from_tao(amount)
rate_tolerance = rate_tolerance if rate_tolerance is not None else 0.0

price_limit: Optional[Balance] = None
if safe_staking:
price_limit = Balance.from_tao(subnet_info.price.tao * (1 + rate_tolerance))

call_params = {
"hotkey": hotkey_ss58,
"netuid": netuid,
"amount": stake_burn_amount.rao,
"limit": price_limit.rao if price_limit else None,
}

call = await subtensor.substrate.compose_call(
call_module="SubtensorModule",
call_function="add_stake_burn",
call_params=call_params,
)

if not json_output:
extrinsic_fee = await subtensor.get_extrinsic_fee(
call, wallet.coldkeypub, proxy=proxy
)
amount_minus_fee = stake_burn_amount - extrinsic_fee
sim_swap = await subtensor.sim_swap(
origin_netuid=0,
destination_netuid=netuid,
amount=amount_minus_fee.rao,
)

received_amount = sim_swap.alpha_amount
current_price_float = subnet_info.price.tao
rate = 1.0 / current_price_float

table = _define_stake_burn_table(
wallet=wallet,
subtensor=subtensor,
safe_staking=safe_staking,
rate_tolerance=rate_tolerance,
)
row = [
str(netuid),
hotkey_ss58,
str(stake_burn_amount),
str(rate) + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ",
str(received_amount.set_unit(netuid)),
str(sim_swap.tao_fee),
str(extrinsic_fee),
]
if safe_staking:
price_with_tolerance = current_price_float * (1 + rate_tolerance)
rate_with_tolerance = 1.0 / price_with_tolerance
rate_with_tolerance_str = (
f"{rate_with_tolerance:.4f} "
f"{Balance.get_unit(netuid)}/{Balance.get_unit(0)} "
)
row.append(rate_with_tolerance_str)

table.add_row(*row)
console.print(table)

if prompt and not confirm_action(
"Would you like to continue?", decline=decline, quiet=quiet
):
print_error("User aborted.")
return False

if not unlock_key(wallet).success:
return False

with console.status(
f":satellite: Performing subnet stake burn on [bold]{netuid}[/bold]...",
spinner="earth",
) as status:
next_nonce = await subtensor.substrate.get_account_next_index(
wallet.coldkeypub.ss58_address
)
success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic(
call=call,
wallet=wallet,
nonce=next_nonce,
era={"period": period},
proxy=proxy,
mev_protection=mev_protection,
)

if not success:
if json_output:
json_console.print_json(
data={
"success": False,
"message": err_msg,
"extrinsic_identifier": None,
}
)
else:
print_error(err_msg, status=status)
return False

if mev_protection:
inner_hash = err_msg
mev_shield_id = await extract_mev_shield_id(ext_receipt)
mev_success, mev_error, ext_receipt = await wait_for_extrinsic_by_hash(
subtensor=subtensor,
extrinsic_hash=inner_hash,
shield_id=mev_shield_id,
submit_block_hash=ext_receipt.block_hash,
status=status,
)
if not mev_success:
status.stop()
if json_output:
json_console.print_json(
data={
"success": False,
"message": mev_error,
"extrinsic_identifier": None,
}
)
else:
print_error(mev_error, status=status)
return False

ext_id = await ext_receipt.get_extrinsic_identifier()

msg = f"Subnet stake burn succeeded on SN{netuid}."
if json_output:
json_console.print_json(
data={"success": True, "message": msg, "extrinsic_identifier": ext_id}
)
else:
await print_extrinsic_id(ext_receipt)
print_success(msg)

return True


def _define_stake_burn_table(
wallet: Wallet,
subtensor: "SubtensorInterface",
safe_staking: bool,
rate_tolerance: float,
) -> Table:
table = create_table(
title=f"\n[{COLOR_PALETTE.G.HEADER}]Subnet Buyback:\n"
f"Wallet: [{COLOR_PALETTE.G.CK}]{wallet.name}[/{COLOR_PALETTE.G.CK}], "
f"Coldkey ss58: [{COLOR_PALETTE.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE.G.CK}]\n"
f"Network: {subtensor.network}[/{COLOR_PALETTE.G.HEADER}]\n",
)
table.add_column("Netuid", justify="center", style="grey89")
table.add_column(
"Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]
)
table.add_column(
"Amount (τ)",
justify="center",
style=COLOR_PALETTE["POOLS"]["TAO"],
)
table.add_column(
"Rate (per τ)",
justify="center",
style=COLOR_PALETTE["POOLS"]["RATE"],
)
table.add_column(
"Est. Burned",
justify="center",
style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"],
)
table.add_column(
"Fee (τ)",
justify="center",
style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"],
)
table.add_column(
"Extrinsic Fee (τ)",
justify="center",
style=COLOR_PALETTE.STAKE.TAO,
)
if safe_staking:
table.add_column(
f"Rate with tolerance: [blue]({rate_tolerance * 100}%)[/blue]",
justify="center",
style=COLOR_PALETTE["POOLS"]["RATE"],
)
return table


def search_metadata(
param_name: str,
value: Union[str, bool, float, list[float]],
Expand Down
Loading
Loading