Skip to content
Closed
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
81 changes: 81 additions & 0 deletions bittensor_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,11 @@ def edit_help(cls, option_name: str, help_text: str):
show_default=False,
help="Enable or disable MEV protection [dim](default: enabled)[/dim].",
)
relay = typer.Option(
None,
"--relay",
help="Coldkey wallet name or SS58 that signs the outer MEV Shield transaction (requires MEV protection).",
)
json_output = typer.Option(
False,
"--json-output",
Expand Down Expand Up @@ -2279,6 +2284,38 @@ def is_valid_proxy_name_or_ss58(
raise typer.BadParameter(f"Invalid SS58 address: {address}")
return address

def resolve_relay_wallet(
self, relay: Optional[str], wallet_base_path: str
) -> Wallet:
"""
Resolve ``--relay`` to a Wallet whose coldkey signs the outer MEV Shield extrinsic.

``relay`` may be a coldkey SS58 present under ``wallet_base_path`` or a wallet directory name.
"""
relay = relay.strip() if relay else ""
if not relay:
print_error("Relay wallet name or SS58 is empty.")
raise typer.Exit(1)
if is_valid_ss58_address(relay):
from bittensor_cli.src.commands.wallets import _get_wallet_by_ss58

w = _get_wallet_by_ss58(wallet_base_path, relay)
if w is None:
print_error(
f"No wallet under {wallet_base_path} has coldkey {relay}. "
"Import the relay coldkey or pass a wallet name."
)
raise typer.Exit(1)
return w
w = Wallet(name=relay, path=wallet_base_path)
valid = utils.is_valid_wallet(w)
if not valid[0]:
print_error(
f"Relay wallet '{relay}' does not exist under {wallet_base_path}."
)
raise typer.Exit(1)
return w

def ask_rate_tolerance(
self,
rate_tolerance: Optional[float],
Expand Down Expand Up @@ -4772,6 +4809,7 @@ def stake_add(
quiet: bool = Options.quiet,
verbose: bool = Options.verbose,
json_output: bool = Options.json_output,
relay: Optional[str] = Options.relay,
):
"""
Stake TAO to one or more hotkeys on specific netuids with your coldkey.
Expand Down Expand Up @@ -4812,6 +4850,11 @@ def stake_add(
"""
netuids = netuids or []
self.verbosity_handler(quiet, verbose, json_output, prompt, decline)
if relay and not mev_protection:
print_error(
"Cannot use --relay without MEV protection (--mev-protection)."
)
raise typer.Exit(1)
proxy = self.is_valid_proxy_name_or_ss58(proxy, False)
safe_staking = self.ask_safe_staking(safe_staking)
if safe_staking:
Expand Down Expand Up @@ -5021,6 +5064,7 @@ def stake_add(
f"period: {period}\n"
f"mev_protection: {mev_protection}\n"
)
relay_wallet = self.resolve_relay_wallet(relay, wallet.path) if relay else None
return self._run_command(
add_stake.stake_add(
wallet=wallet,
Expand All @@ -5041,6 +5085,7 @@ def stake_add(
era=period,
proxy=proxy,
mev_protection=mev_protection,
relay_wallet=relay_wallet,
)
)

Expand Down Expand Up @@ -5106,6 +5151,7 @@ def stake_remove(
quiet: bool = Options.quiet,
verbose: bool = Options.verbose,
json_output: bool = Options.json_output,
relay: Optional[str] = Options.relay,
):
"""
Unstake TAO from one or more hotkeys and transfer them back to the user's coldkey wallet.
Expand Down Expand Up @@ -5141,6 +5187,11 @@ def stake_remove(
• [blue]--partial[/blue]: Complete partial unstake if rates exceed tolerance
"""
self.verbosity_handler(quiet, verbose, json_output, prompt, decline)
if relay and not mev_protection:
print_error(
"Cannot use --relay without MEV protection (--mev-protection)."
)
raise typer.Exit(1)
proxy = self.is_valid_proxy_name_or_ss58(proxy, False)
if not unstake_all and not unstake_all_alpha:
safe_staking = self.ask_safe_staking(safe_staking)
Expand Down Expand Up @@ -5315,6 +5366,9 @@ def stake_remove(
f"era: {period}\n"
f"mev_protection: {mev_protection}"
)
relay_wallet = (
self.resolve_relay_wallet(relay, wallet.path) if relay else None
)
return self._run_command(
remove_stake.unstake_all(
wallet=wallet,
Expand All @@ -5331,6 +5385,7 @@ def stake_remove(
era=period,
mev_protection=mev_protection,
proxy=proxy,
relay_wallet=relay_wallet,
)
)
elif (
Expand Down Expand Up @@ -5387,6 +5442,7 @@ def stake_remove(
f"mev_protection: {mev_protection}\n"
)

relay_wallet = self.resolve_relay_wallet(relay, wallet.path) if relay else None
return self._run_command(
remove_stake.unstake(
wallet=wallet,
Expand All @@ -5408,6 +5464,7 @@ def stake_remove(
era=period,
proxy=proxy,
mev_protection=mev_protection,
relay_wallet=relay_wallet,
)
)

Expand Down Expand Up @@ -5460,6 +5517,7 @@ def stake_move(
quiet: bool = Options.quiet,
verbose: bool = Options.verbose,
json_output: bool = Options.json_output,
relay: Optional[str] = Options.relay,
):
"""
Move staked TAO between hotkeys while keeping the same coldkey ownership.
Expand All @@ -5486,6 +5544,11 @@ def stake_move(
[green]$[/green] btcli stake move --no-mev-protection
"""
self.verbosity_handler(quiet, verbose, json_output, prompt, decline)
if relay and not mev_protection:
print_error(
"Cannot use --relay without MEV protection (--mev-protection)."
)
raise typer.Exit(1)
proxy = self.is_valid_proxy_name_or_ss58(proxy, False)
print_protection_warnings(
mev_protection=mev_protection,
Expand Down Expand Up @@ -5595,6 +5658,7 @@ def stake_move(
f"proxy: {proxy}\n"
f"mev_protection: {mev_protection}\n"
)
relay_wallet = self.resolve_relay_wallet(relay, wallet.path) if relay else None
result, ext_id = self._run_command(
move_stake.move_stake(
subtensor=self.initialize_chain(network),
Expand All @@ -5612,6 +5676,7 @@ def stake_move(
quiet=quiet,
proxy=proxy,
mev_protection=mev_protection,
relay_wallet=relay_wallet,
)
)
if json_output:
Expand Down Expand Up @@ -5660,6 +5725,7 @@ def stake_transfer(
quiet: bool = Options.quiet,
verbose: bool = Options.verbose,
json_output: bool = Options.json_output,
relay: Optional[str] = Options.relay,
):
"""
Transfer stake between coldkeys while keeping the same hotkey ownership.
Expand Down Expand Up @@ -5697,6 +5763,11 @@ def stake_transfer(
[green]$[/green] btcli stake transfer --origin-netuid 1 --dest-netuid 2 --amount 100 --no-mev-protection
"""
self.verbosity_handler(quiet, verbose, json_output, prompt, decline)
if relay and not mev_protection:
print_error(
"Cannot use --relay without MEV protection (--mev-protection)."
)
raise typer.Exit(1)
proxy = self.is_valid_proxy_name_or_ss58(proxy, False)
print_protection_warnings(
mev_protection=mev_protection,
Expand Down Expand Up @@ -5802,6 +5873,7 @@ def stake_transfer(
f"mev_protection: {mev_protection}"
f"proxy: {proxy}"
)
relay_wallet = self.resolve_relay_wallet(relay, wallet.path) if relay else None
result, ext_id = self._run_command(
move_stake.transfer_stake(
wallet=wallet,
Expand All @@ -5819,6 +5891,7 @@ def stake_transfer(
quiet=quiet,
proxy=proxy,
mev_protection=mev_protection,
relay_wallet=relay_wallet,
)
)
if json_output:
Expand Down Expand Up @@ -5872,6 +5945,7 @@ def stake_swap(
quiet: bool = Options.quiet,
verbose: bool = Options.verbose,
json_output: bool = Options.json_output,
relay: Optional[str] = Options.relay,
):
"""
Swap stake between different subnets while keeping the same coldkey-hotkey pair ownership.
Expand Down Expand Up @@ -5903,6 +5977,11 @@ def stake_swap(
[green]$[/green] btcli stake swap --origin-netuid 1 --dest-netuid 2 --amount 100 --unsafe
"""
self.verbosity_handler(quiet, verbose, json_output, prompt, decline)
if relay and not mev_protection:
print_error(
"Cannot use --relay without MEV protection (--mev-protection)."
)
raise typer.Exit(1)
proxy = self.is_valid_proxy_name_or_ss58(proxy, False)
console.print(
"[dim]This command moves stake from one subnet to another subnet while keeping "
Expand Down Expand Up @@ -5959,6 +6038,7 @@ def stake_swap(
f"wait_for_finalization: {wait_for_finalization}\n"
f"mev_protection: {mev_protection}\n"
)
relay_wallet = self.resolve_relay_wallet(relay, wallet.path) if relay else None
result, ext_id = self._run_command(
move_stake.swap_stake(
wallet=wallet,
Expand All @@ -5979,6 +6059,7 @@ def stake_swap(
wait_for_inclusion=wait_for_inclusion,
wait_for_finalization=wait_for_finalization,
mev_protection=mev_protection,
relay_wallet=relay_wallet,
)
)
if json_output:
Expand Down
28 changes: 25 additions & 3 deletions bittensor_cli/src/bittensor/subtensor_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -1159,6 +1159,7 @@ async def sign_and_send_extrinsic(
sign_with: Literal["coldkey", "hotkey", "coldkeypub"] = "coldkey",
announce_only: bool = False,
mev_protection: bool = False,
relay_wallet: Optional[Wallet] = None,
) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]:
"""
Helper method to sign and submit an extrinsic call to chain.
Expand All @@ -1175,14 +1176,17 @@ async def sign_and_send_extrinsic(
be used with `mev_protection=True`.
:param mev_protection: If set, uses Mev Protection on the extrinsic, thus encrypting it. Cannot be
used with `announce_only=True`.
:param relay_wallet: If set with ``mev_protection``, signs the outer ``MevShield.submit_encrypted``
extrinsic with this wallet's coldkey instead of ``wallet``.

:return: (success, error message or inner extrinsic hash (if using mev_protection), extrinsic receipt | None)
"""

async def create_signed(call_to_sign, n):
async def create_signed(call_to_sign, n, signing_keypair: Optional[Keypair] = None):
kp = signing_keypair if signing_keypair is not None else keypair
kwargs = {
"call": call_to_sign,
"keypair": keypair,
"keypair": kp,
"nonce": n,
}
if era is not None:
Expand All @@ -1194,6 +1198,10 @@ async def create_signed(call_to_sign, n):
"Cannot use announce-only and mev-protection. Calls should be announced without mev protection,"
"and executed with them."
)
if relay_wallet is not None and not mev_protection:
raise ValueError(
"Relay requires MEV protection; remove relay or enable MEV protection."
)
if proxy is not None:
if announce_only:
call_to_announce = call
Expand Down Expand Up @@ -1236,7 +1244,17 @@ async def create_signed(call_to_sign, n):
inner_extrinsic = await create_signed(call, next_nonce)
inner_hash = f"0x{inner_extrinsic.extrinsic_hash.hex()}"
shield_call = await encrypt_extrinsic(self, inner_extrinsic)
extrinsic = await create_signed(shield_call, nonce)
if relay_wallet is not None:
relay_keypair = relay_wallet.coldkey
outer_nonce = await self.substrate.get_account_next_index(
relay_keypair.ss58_address
)
extrinsic = await create_signed(
shield_call, outer_nonce, signing_keypair=relay_keypair
)
else:
outer_nonce = nonce if nonce is not None else call_args["nonce"]
extrinsic = await create_signed(shield_call, outer_nonce)
else:
extrinsic = await self.substrate.create_signed_extrinsic(**call_args)
try:
Expand Down Expand Up @@ -1304,6 +1322,7 @@ async def sign_and_send_batch_extrinsic(
announce_only: bool = False,
mev_protection: bool = False,
block_hash: Optional[str] = None,
relay_wallet: Optional[Wallet] = None,
) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]:
"""
Wraps multiple extrinsic calls into a single Utility.batch_all transaction
Expand All @@ -1325,6 +1344,7 @@ async def sign_and_send_batch_extrinsic(
:param announce_only: make the call as a proxy announcement.
:param mev_protection: encrypt the extrinsic via MEV Shield.
:param block_hash: cached block hash for compose_call. Fetched if None.
:param relay_wallet: If set with ``mev_protection``, signs the outer MEV Shield extrinsic with this wallet.

:return: (success, error message or inner hash, extrinsic receipt | None)
"""
Expand All @@ -1344,6 +1364,7 @@ async def sign_and_send_batch_extrinsic(
sign_with=sign_with,
announce_only=announce_only,
mev_protection=mev_protection,
relay_wallet=relay_wallet,
)

if block_hash is None:
Expand All @@ -1367,6 +1388,7 @@ async def sign_and_send_batch_extrinsic(
sign_with=sign_with,
announce_only=announce_only,
mev_protection=mev_protection,
relay_wallet=relay_wallet,
)

async def get_children(self, hotkey, netuid) -> tuple[bool, list, str]:
Expand Down
7 changes: 7 additions & 0 deletions bittensor_cli/src/commands/stake/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ async def stake_add(
era: int,
mev_protection: bool,
proxy: Optional[str],
relay_wallet: Optional[Wallet] = None,
):
"""
Args:
Expand All @@ -72,6 +73,7 @@ async def stake_add(
era: Blocks for which the transaction should be valid.
proxy: Optional proxy to use for staking.
mev_protection: If true, will encrypt the extrinsic behind the mev protection shield.
relay_wallet: If set with MEV protection, signs the outer MEV Shield extrinsic with this coldkey.

Returns:
bool: True if stake operation is successful, False otherwise
Expand Down Expand Up @@ -150,6 +152,7 @@ async def safe_stake_extrinsic(
era={"period": era},
proxy=proxy,
mev_protection=mev_protection,
relay_wallet=relay_wallet,
)
if not success_:
if "Custom error: 8" in err_msg:
Expand Down Expand Up @@ -248,6 +251,7 @@ async def stake_extrinsic(
era={"period": era},
proxy=proxy,
mev_protection=mev_protection,
relay_wallet=relay_wallet,
)
if not success_:
err_msg = f"{failure_prelude} with error: {err_msg}"
Expand Down Expand Up @@ -481,6 +485,8 @@ async def stake_extrinsic(
return
if not unlock_key(wallet).success:
return
if relay_wallet is not None and not unlock_key(relay_wallet).success:
return

total_ops = len(operations)
use_batch = total_ops > 1
Expand Down Expand Up @@ -538,6 +544,7 @@ async def stake_extrinsic(
proxy=proxy,
mev_protection=mev_protection,
block_hash=batch_block_hash,
relay_wallet=relay_wallet,
)

if success and mev_protection:
Expand Down
Loading