diff --git a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py index eadce8863..4a95dbb73 100644 --- a/bittensor_cli/src/bittensor/extrinsics/mev_shield.py +++ b/bittensor_cli/src/bittensor/extrinsics/mev_shield.py @@ -1,76 +1,47 @@ -import asyncio import hashlib from typing import TYPE_CHECKING, Optional from async_substrate_interface import AsyncExtrinsicReceipt -from bittensor_drand import encrypt_mlkem768, mlkem_kdf_id -from bittensor_cli.src.bittensor.utils import encode_account_id, format_error_message +from bittensor_drand import encrypt_mlkem768 +from bittensor_cli.src.bittensor.utils import format_error_message if TYPE_CHECKING: - from bittensor_wallet import Wallet - from scalecodec import GenericCall + from bittensor_wallet import Keypair + from scalecodec import GenericCall, GenericExtrinsic from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface -async def encrypt_call( +async def encrypt_extrinsic( subtensor: "SubtensorInterface", - wallet: "Wallet", - call: "GenericCall", + signed_extrinsic: "GenericExtrinsic", ) -> "GenericCall": """ - Encrypt a call using MEV Shield. + Encrypt a signed extrinsic using MEV Shield. - Takes any call and returns a MevShield.submit_encrypted call - that can be submitted like any regular extrinsic. + Takes a pre-signed extrinsic and returns a MevShield.submit_encrypted call. Args: subtensor: The SubtensorInterface instance for chain queries. - wallet: The wallet whose coldkey will sign the inner payload. - call: The call to encrypt. + signed_extrinsic: The signed extrinsic to encrypt. Returns: - A MevShield.submit_encrypted call. + A MevShield.submit_encrypted call to be signed with the current nonce. Raises: ValueError: If MEV Shield NextKey is not available on chain. """ - next_key_result, genesis_hash = await asyncio.gather( - subtensor.get_mev_shield_next_key(), - subtensor.substrate.get_block_hash(0), - ) - if next_key_result is None: + ml_kem_768_public_key = await subtensor.get_mev_shield_next_key() + if ml_kem_768_public_key is None: raise ValueError("MEV Shield NextKey not available on chain") - ml_kem_768_public_key = next_key_result - - # Create payload_core: signer (32B) + next_key (32B) + SCALE(call) - signer_bytes = encode_account_id(wallet.coldkey.ss58_address) - scale_call_bytes = bytes(call.data.data) - next_key = hashlib.blake2b(next_key_result, digest_size=32).digest() - - payload_core = signer_bytes + next_key + scale_call_bytes - - mev_shield_version = mlkem_kdf_id() - genesis_hash_clean = ( - genesis_hash[2:] if genesis_hash.startswith("0x") else genesis_hash - ) - genesis_hash_bytes = bytes.fromhex(genesis_hash_clean) - - # Sign: coldkey.sign(b"mev-shield:v1" + genesis_hash + payload_core) - message_to_sign = ( - b"mev-shield:" + mev_shield_version + genesis_hash_bytes + payload_core - ) - signature = wallet.coldkey.sign(message_to_sign) - - # Plaintext: payload_core + b"\x01" + signature - plaintext = payload_core + b"\x01" + signature + plaintext = bytes(signed_extrinsic.data.data) # Encrypt using ML-KEM-768 ciphertext = encrypt_mlkem768(ml_kem_768_public_key, plaintext) # Commitment: blake2_256(payload_core) - commitment_hash = hashlib.blake2b(payload_core, digest_size=32).digest() + commitment_hash = hashlib.blake2b(plaintext, digest_size=32).digest() commitment_hex = "0x" + commitment_hash.hex() # Create the MevShield.submit_encrypted call @@ -105,31 +76,37 @@ async def extract_mev_shield_id(response: "AsyncExtrinsicReceipt") -> Optional[s return None -async def wait_for_mev_execution( +async def wait_for_extrinsic_by_hash( subtensor: "SubtensorInterface", - wrapper_id: str, + extrinsic_hash: str, + shield_id: str, submit_block_hash: str, - timeout_blocks: int = 4, + timeout_blocks: int = 2, status=None, ) -> tuple[bool, Optional[str], Optional[AsyncExtrinsicReceipt]]: """ - Wait for MEV Shield inner call execution. + Wait for the result of a MeV Shield encrypted extrinsic. - After submit_encrypted succeeds, the block author will decrypt and execute - the inner call via execute_revealed. This function polls for the - DecryptedExecuted or DecryptedRejected event. + After submit_encrypted succeeds, the block author will decrypt and submit + the inner extrinsic directly. This function polls subsequent blocks looking + for either: + - an extrinsic matching the provided hash (success) + OR + - a markDecryptionFailed extrinsic with matching shield ID (failure) Args: subtensor: SubtensorInterface instance. - wrapper_id: The ID from EncryptedSubmitted event. - submit_block_number: Block number where submit_encrypted was included. - timeout_blocks: Max blocks to wait (default 4). + extrinsic_hash: The hash of the inner extrinsic to find. + shield_id: The wrapper ID from EncryptedSubmitted event (for detecting decryption failures). + submit_block_hash: Block hash where submit_encrypted was included. + timeout_blocks: Max blocks to wait (default 2). status: Optional rich.Status object for progress updates. Returns: Tuple of (success: bool, error: Optional[str], receipt: Optional[AsyncExtrinsicReceipt]). - - (True, None, receipt) if DecryptedExecuted was found. - - (False, error_message, None) if the call failed or timeout. + - (True, None, receipt) if extrinsic was found and succeeded. + - (False, error_message, receipt) if extrinsic was found but failed. + - (False, "Timeout...", None) if not found within timeout. """ async def _noop(_): @@ -154,42 +131,45 @@ async def _noop(_): block_hash = await subtensor.substrate.get_block_hash(current_block) extrinsics = await subtensor.substrate.get_extrinsics(block_hash) - # Find executeRevealed extrinsic & match ids - execute_revealed_index = None + result_idx = None for idx, extrinsic in enumerate(extrinsics): - call = extrinsic.value.get("call", {}) - call_module = call.get("call_module") - call_function = call.get("call_function") + # Success: Inner extrinsic executed + if f"0x{extrinsic.extrinsic_hash.hex()}" == extrinsic_hash: + result_idx = idx + break - if call_module == "MevShield" and call_function in ( - "execute_revealed", - "mark_decryption_failed", + # Failure: Decryption failed + call = extrinsic.value.get("call", {}) + if ( + call.get("call_module") == "MevShield" + and call.get("call_function") == "mark_decryption_failed" ): call_args = call.get("call_args", []) for arg in call_args: - if arg.get("name") == "id": - extrinsic_wrapper_id = arg.get("value") - if extrinsic_wrapper_id == wrapper_id: - execute_revealed_index = idx - break - - if execute_revealed_index is not None: + if arg.get("name") == "id" and arg.get("value") == shield_id: + result_idx = idx + break + if result_idx is not None: break - if execute_revealed_index is None: - current_block += 1 - continue + if result_idx is not None: + receipt = AsyncExtrinsicReceipt( + substrate=subtensor.substrate, + block_hash=block_hash, + block_number=current_block, + extrinsic_idx=result_idx, + ) - receipt = AsyncExtrinsicReceipt( - substrate=subtensor.substrate, - block_hash=block_hash, - extrinsic_idx=execute_revealed_index, - ) + if not await receipt.is_success: + error_msg = format_error_message(await receipt.error_message) + return False, error_msg, receipt - if not await receipt.is_success: - error_msg = format_error_message(await receipt.error_message) - return False, error_msg, None + return True, None, receipt - return True, None, receipt + current_block += 1 - return False, "Timeout waiting for MEV Shield execution", None + return ( + False, + "Failed to find outcome of the shield extrinsic (The protected extrinsic wasn't decrypted)", + None, + ) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 70af13cdc..10004797f 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -34,6 +34,7 @@ from bittensor_cli.src import DelegatesDetails from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float from bittensor_cli.src import Constants, defaults, TYPE_REGISTRY +from bittensor_cli.src.bittensor.extrinsics.mev_shield import encrypt_extrinsic from bittensor_cli.src.bittensor.utils import ( format_error_message, console, @@ -1178,6 +1179,7 @@ async def sign_and_send_extrinsic( nonce: Optional[int] = None, sign_with: Literal["coldkey", "hotkey", "coldkeypub"] = "coldkey", announce_only: bool = False, + mev_protection: bool = False, ) -> tuple[bool, str, Optional[AsyncExtrinsicReceipt]]: """ Helper method to sign and submit an extrinsic call to chain. @@ -1190,10 +1192,29 @@ async def sign_and_send_extrinsic( :param proxy: The real account used to create the proxy. None if not using a proxy for this call. :param nonce: The nonce used to submit this extrinsic call. :param sign_with: Determine which of the wallet's keypairs to use to sign the extrinsic call. - :param announce_only: If set, makes the call as an announcement, rather than making the call. + :param announce_only: If set, makes the call as an announcement, rather than making the call. Cannot + 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`. - :return: (success, error message, extrinsic receipt | None) + :return: (success, error message or inner extrinsic hash (if using mev_protection), extrinsic receipt | None) """ + + async def create_signed(call_to_sign, n): + kwargs = { + "call": call_to_sign, + "keypair": keypair, + "nonce": n, + } + if era is not None: + kwargs["era"] = era + return await self.substrate.create_signed_extrinsic(**kwargs) + + if announce_only and mev_protection: + raise ValueError( + "Cannot use announce-only and mev-protection. Calls should be announced without mev protection," + "and executed with them." + ) if proxy is not None: if announce_only: call_to_announce = call @@ -1225,7 +1246,17 @@ async def sign_and_send_extrinsic( call_args["nonce"] = await self.substrate.get_account_next_index( keypair.ss58_address ) - extrinsic = await self.substrate.create_signed_extrinsic(**call_args) + inner_hash = "" + if mev_protection: + next_nonce = await self.substrate.get_account_next_index( + keypair.ss58_address + ) + 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) + else: + extrinsic = await self.substrate.create_signed_extrinsic(**call_args) try: response = await self.substrate.submit_extrinsic( extrinsic, @@ -1234,7 +1265,7 @@ 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, "", response + return True, inner_hash, response if await response.is_success: if announce_only: block = await self.substrate.get_block_number(response.block_hash) @@ -1251,7 +1282,7 @@ async def sign_and_send_extrinsic( console.print( f"Added entry {call_to_announce.call_hash} at block {block} to your ProxyAnnouncements address book." ) - return True, "", response + return True, inner_hash, response else: return False, format_error_message(await response.error_message), None except SubstrateRequestException as e: @@ -2378,7 +2409,7 @@ async def get_all_subnet_ema_tao_inflow( ema_map[netuid] = Balance.from_rao(0) else: _, raw_ema_value = value - ema_value = fixed_to_float(raw_ema_value) + ema_value = int(fixed_to_float(raw_ema_value)) ema_map[netuid] = Balance.from_rao(ema_value) return ema_map @@ -2409,13 +2440,13 @@ async def get_subnet_ema_tao_inflow( if not value: return Balance.from_rao(0) _, raw_ema_value = value - ema_value = fixed_to_float(raw_ema_value) + ema_value = int(fixed_to_float(raw_ema_value)) return Balance.from_rao(ema_value) async def get_mev_shield_next_key( self, block_hash: Optional[str] = None, - ) -> Optional[tuple[bytes, int]]: + ) -> bytes: """ Get the next MEV Shield public key and epoch from chain storage. @@ -2443,7 +2474,7 @@ async def get_mev_shield_next_key( async def get_mev_shield_current_key( self, block_hash: Optional[str] = None, - ) -> Optional[tuple[bytes, int]]: + ) -> bytes: """ Get the current MEV Shield public key and epoch from chain storage. diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py index 36e7ad7f3..126854aff 100644 --- a/bittensor_cli/src/commands/stake/add.py +++ b/bittensor_cli/src/commands/stake/add.py @@ -11,9 +11,8 @@ from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( - encrypt_call, extract_mev_shield_id, - wait_for_mev_execution, + wait_for_extrinsic_by_hash, ) from bittensor_cli.src.bittensor.utils import ( console, @@ -141,14 +140,13 @@ async def safe_stake_extrinsic( }, ), ) - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success_, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, nonce=next_nonce, era={"period": era}, proxy=proxy, + mev_protection=mev_protection, ) if not success_: if "Custom error: 8" in err_msg: @@ -164,16 +162,20 @@ async def safe_stake_extrinsic( return False, err_msg, None else: if mev_protection: + inner_hash = err_msg mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status_ - ) - if not mev_success: - status_.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, err_msg, None + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, + status=status_, + ) + if not mev_success: + status_.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, err_msg, None if json_output: # the rest of this checking is not necessary if using json_output return True, "", response @@ -239,14 +241,13 @@ async def stake_extrinsic( failure_prelude = ( f":cross_mark: [red]Failed[/red] to stake {amount} on Netuid {netuid_i}" ) - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success_, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, nonce=next_nonce, era={"period": era}, proxy=proxy, + mev_protection=mev_protection, ) if not success_: err_msg = f"{failure_prelude} with error: {err_msg}" @@ -254,16 +255,20 @@ async def stake_extrinsic( return False, err_msg, None else: if mev_protection: + inner_hash = err_msg mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status_ - ) - if not mev_success: - status_.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, err_msg, None + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, + status=status_, + ) + if not mev_success: + status_.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, err_msg, None if json_output: # the rest of this is not necessary if using json_output return True, "", response diff --git a/bittensor_cli/src/commands/stake/move.py b/bittensor_cli/src/commands/stake/move.py index c38fc0893..8f9255f70 100644 --- a/bittensor_cli/src/commands/stake/move.py +++ b/bittensor_cli/src/commands/stake/move.py @@ -9,9 +9,8 @@ from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( - encrypt_call, extract_mev_shield_id, - wait_for_mev_execution, + wait_for_extrinsic_by_hash, ) from bittensor_cli.src.bittensor.utils import ( console, @@ -548,13 +547,15 @@ async def move_stake( "alpha_amount": amount_to_move_as_balance.rao, }, ) - sim_swap, extrinsic_fee = await asyncio.gather( + sim_swap, extrinsic_fee, next_nonce = await asyncio.gather( subtensor.sim_swap( origin_netuid=origin_netuid, destination_netuid=destination_netuid, amount=amount_to_move_as_balance.rao, ), subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy), + # TODO verify if this should be proxy or signer + subtensor.substrate.get_account_next_index(wallet.coldkeypub.ss58_address), ) # Display stake movement details @@ -586,28 +587,31 @@ async def move_stake( f"[blue]{origin_netuid}[/blue] \nto " f"[blue]{destination_hotkey}[/blue] on netuid: [blue]{destination_netuid}[/blue] ..." ) as status: - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success_, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, era={"period": era}, proxy=proxy, + mev_protection=mev_protection, + nonce=next_nonce, ) - if mev_protection: - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") - return False, "" - ext_id = await response.get_extrinsic_identifier() if response else "" if success_: + if mev_protection: + inner_hash = err_msg + mev_shield_id = await extract_mev_shield_id(response) + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, + status=status, + ) + if not mev_success: + status.stop() + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, "" await print_extrinsic_id(response) if not prompt: console.print(":white_heavy_check_mark: [green]Sent[/green]") @@ -757,13 +761,16 @@ async def transfer_stake( "alpha_amount": amount_to_transfer.rao, }, ) - sim_swap, extrinsic_fee = await asyncio.gather( + sim_swap, extrinsic_fee, next_nonce = await asyncio.gather( subtensor.sim_swap( origin_netuid=origin_netuid, destination_netuid=dest_netuid, amount=amount_to_transfer.rao, ), subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy), + subtensor.substrate.get_account_next_index( + proxy or wallet.coldkeypub.ss58_address + ), ) # Display stake movement details @@ -793,59 +800,63 @@ async def transfer_stake( return False, "" with console.status("\n:satellite: Transferring stake ...") as status: - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success_, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, era={"period": era}, proxy=proxy, + mev_protection=mev_protection, + nonce=next_nonce, ) - if success_: - if mev_protection: - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status + if success_: + if mev_protection: + inner_hash = err_msg + mev_shield_id = await extract_mev_shield_id(response) + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, + status=status, ) if not mev_success: status.stop() err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") return 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, ext_id - else: - # Get and display new stake balances - new_stake, new_dest_stake = await asyncio.gather( - subtensor.get_stake( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=origin_hotkey, - netuid=origin_netuid, - ), - subtensor.get_stake( - coldkey_ss58=dest_coldkey_ss58, - hotkey_ss58=origin_hotkey, - netuid=dest_netuid, - ), - ) + 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, ext_id + else: + # Get and display new stake balances + new_stake, new_dest_stake = await asyncio.gather( + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=origin_hotkey, + netuid=origin_netuid, + ), + subtensor.get_stake( + coldkey_ss58=dest_coldkey_ss58, + hotkey_ss58=origin_hotkey, + netuid=dest_netuid, + ), + ) - console.print( - f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE.S.AMOUNT}]{new_stake}" - ) - console.print( - f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE.S.AMOUNT}]{new_dest_stake}" - ) - return True, ext_id + console.print( + f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.AMOUNT}]{new_stake}" + ) + console.print( + f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.AMOUNT}]{new_dest_stake}" + ) + return True, ext_id - else: - err_console.print(f":cross_mark: [red]Failed[/red] with error: {err_msg}") - return False, "" + else: + err_console.print(f":cross_mark: [red]Failed[/red] with error: {err_msg}") + return False, "" async def swap_stake( @@ -946,13 +957,16 @@ async def swap_stake( "alpha_amount": amount_to_swap.rao, }, ) - sim_swap, extrinsic_fee = await asyncio.gather( + sim_swap, extrinsic_fee, next_nonce = await asyncio.gather( subtensor.sim_swap( origin_netuid=origin_netuid, destination_netuid=destination_netuid, amount=amount_to_swap.rao, ), subtensor.get_extrinsic_fee(call, wallet.coldkeypub, proxy=proxy), + subtensor.substrate.get_account_next_index( + proxy or wallet.coldkeypub.ss58_address + ), ) # Display stake movement details @@ -985,8 +999,6 @@ async def swap_stake( f"\n:satellite: Swapping stake from netuid [blue]{origin_netuid}[/blue] " f"to netuid [blue]{destination_netuid}[/blue]..." ) as status: - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success_, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, @@ -994,51 +1006,56 @@ async def swap_stake( proxy=proxy, wait_for_finalization=wait_for_finalization, wait_for_inclusion=wait_for_inclusion, + mev_protection=mev_protection, + nonce=next_nonce, ) - if mev_protection: - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status + ext_id = await response.get_extrinsic_identifier() + + if success_: + if mev_protection: + inner_hash = err_msg + mev_shield_id = await extract_mev_shield_id(response) + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, + status=status, ) if not mev_success: status.stop() err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") return False, "" + await print_extrinsic_id(response) + if not prompt: + console.print(":white_heavy_check_mark: [green]Sent[/green]") + return True, await response.get_extrinsic_identifier() + else: + # Get and display new stake balances + new_stake, new_dest_stake = await asyncio.gather( + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=origin_netuid, + ), + subtensor.get_stake( + coldkey_ss58=wallet.coldkeypub.ss58_address, + hotkey_ss58=hotkey_ss58, + netuid=destination_netuid, + ), + ) - ext_id = await response.get_extrinsic_identifier() + console.print( + f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.AMOUNT}]{new_stake}" + ) + console.print( + f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " + f"[{COLOR_PALETTE.S.AMOUNT}]{new_dest_stake}" + ) + return True, ext_id - if success_: - await print_extrinsic_id(response) - if not prompt: - console.print(":white_heavy_check_mark: [green]Sent[/green]") - return True, await response.get_extrinsic_identifier() else: - # Get and display new stake balances - new_stake, new_dest_stake = await asyncio.gather( - subtensor.get_stake( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - netuid=origin_netuid, - ), - subtensor.get_stake( - coldkey_ss58=wallet.coldkeypub.ss58_address, - hotkey_ss58=hotkey_ss58, - netuid=destination_netuid, - ), - ) - - console.print( - f"Origin Stake:\n [blue]{current_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE.S.AMOUNT}]{new_stake}" - ) - console.print( - f"Destination Stake:\n [blue]{current_dest_stake}[/blue] :arrow_right: " - f"[{COLOR_PALETTE.S.AMOUNT}]{new_dest_stake}" - ) - return True, ext_id - - else: - err_console.print(f":cross_mark: [red]Failed[/red] with error: {err_msg}") - return False, "" + err_console.print(f":cross_mark: [red]Failed[/red] with error: {err_msg}") + return False, "" diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py index 15cf134a3..85ce18ff5 100644 --- a/bittensor_cli/src/commands/stake/remove.py +++ b/bittensor_cli/src/commands/stake/remove.py @@ -11,9 +11,8 @@ from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( - encrypt_call, extract_mev_shield_id, - wait_for_mev_execution, + wait_for_extrinsic_by_hash, ) from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.utils import ( @@ -612,8 +611,9 @@ async def _unstake_extrinsic( f"\n:satellite: Unstaking {amount} from {hotkey_ss58} on netuid: {netuid} ..." ) - current_balance, call = await asyncio.gather( + current_balance, next_nonce, call = await asyncio.gather( subtensor.get_balance(coldkey_ss58), + subtensor.substrate.get_account_next_index(coldkey_ss58), subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="remove_stake", @@ -625,24 +625,30 @@ async def _unstake_extrinsic( ), ) - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) - success, err_msg, response = await subtensor.sign_and_send_extrinsic( - call=call, wallet=wallet, era={"period": era}, proxy=proxy + # TODO I think this should handle announce-only + call=call, + wallet=wallet, + era={"period": era}, + proxy=proxy, + mev_protection=mev_protection, + nonce=next_nonce, ) if success: if mev_protection: + inner_hash = err_msg mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, None + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, + status=status, + ) + if not mev_success: + status.stop() + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, None await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( @@ -734,28 +740,29 @@ async def _safe_unstake_extrinsic( block_hash=block_hash, ), ) - - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, nonce=next_nonce, era={"period": era}, proxy=proxy, + mev_protection=mev_protection, ) if success: if mev_protection: + inner_hash = err_msg mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, None + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, + status=status, + ) + if not mev_success: + status.stop() + err_console.print(f"\n:cross_mark: [red]Failed[/red]: {mev_error}") + return False, None await print_extrinsic_id(response) block_hash = await subtensor.substrate.get_chain_head() new_balance, new_stake = await asyncio.gather( @@ -851,21 +858,22 @@ async def _unstake_all_extrinsic( previous_root_stake = None call_function = "unstake_all_alpha" if unstake_all_alpha else "unstake_all" - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function=call_function, - call_params={"hotkey": hotkey_ss58}, + call, next_nonce = await asyncio.gather( + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function=call_function, + call_params={"hotkey": hotkey_ss58}, + ), + subtensor.substrate.get_account_next_index(coldkey_ss58), ) - - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) - try: success_, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, era={"period": era}, + nonce=next_nonce, proxy=proxy, + mev_protection=mev_protection, ) if not success_: @@ -873,16 +881,20 @@ async def _unstake_all_extrinsic( return False, None if mev_protection: + inner_hash = err_msg mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status - ) - if not mev_success: - status.stop() - err_msg = f"{failure_prelude}: {mev_error}" - err_out("\n" + err_msg) - return False, None + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, + status=status, + ) + if not mev_success: + status.stop() + err_msg = f"{failure_prelude}: {mev_error}" + err_out("\n" + err_msg) + return False, None await print_extrinsic_id(response) diff --git a/bittensor_cli/src/commands/subnets/subnets.py b/bittensor_cli/src/commands/subnets/subnets.py index 9ed457c3d..509e36957 100644 --- a/bittensor_cli/src/commands/subnets/subnets.py +++ b/bittensor_cli/src/commands/subnets/subnets.py @@ -19,9 +19,8 @@ ) from bittensor_cli.src.bittensor.extrinsics.root import root_register_extrinsic from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( - encrypt_call, extract_mev_shield_id, - wait_for_mev_execution, + wait_for_extrinsic_by_hash, ) from rich.live import Live from bittensor_cli.src.bittensor.minigraph import MiniGraph @@ -233,38 +232,46 @@ async def _find_event_attributes_in_extrinsic_receipt( if not unlock_key(wallet).success: return False, None, None + coldkey_ss58 = proxy or wallet.coldkeypub.ss58_address + with console.status(":satellite: Registering subnet...", spinner="earth") as status: substrate = subtensor.substrate - # create extrinsic call - call = await substrate.compose_call( - call_module="SubtensorModule", - call_function=call_function, - call_params=call_params, + call, next_nonce = await asyncio.gather( + substrate.compose_call( + call_module="SubtensorModule", + call_function=call_function, + call_params=call_params, + ), + substrate.get_account_next_index(coldkey_ss58), ) - if mev_protection: - call = await encrypt_call(subtensor, wallet, call) success, err_msg, response = await subtensor.sign_and_send_extrinsic( call=call, wallet=wallet, wait_for_inclusion=wait_for_inclusion, wait_for_finalization=wait_for_finalization, proxy=proxy, + nonce=next_nonce, + mev_protection=mev_protection, ) - # We only wait here if we expect finalization. - if not wait_for_finalization and not wait_for_inclusion: - return True, None, None + # We only wait here if we expect finalization. + if not wait_for_finalization and not wait_for_inclusion: + return True, None, None - if not success: - err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") - return False, None, None - else: - # Check for MEV shield execution - if mev_protection: - mev_shield_id = await extract_mev_shield_id(response) - if mev_shield_id: - mev_success, mev_error, response = await wait_for_mev_execution( - subtensor, mev_shield_id, response.block_hash, status=status + if not success: + err_console.print(f":cross_mark: [red]Failed[/red]: {err_msg}") + return False, None, None + else: + # Check for MEV shield execution + if mev_protection: + inner_hash = err_msg + mev_shield_id = await extract_mev_shield_id(response) + mev_success, mev_error, response = await wait_for_extrinsic_by_hash( + subtensor=subtensor, + extrinsic_hash=inner_hash, + shield_id=mev_shield_id, + submit_block_hash=response.block_hash, + status=status, ) if not mev_success: status.stop() @@ -273,24 +280,24 @@ async def _find_event_attributes_in_extrinsic_receipt( ) return False, None, None - # Successful registration, final check for membership + # Successful registration, final check for membership - attributes = await _find_event_attributes_in_extrinsic_receipt( - response, "NetworkAdded" - ) - await print_extrinsic_id(response) - ext_id = await response.get_extrinsic_identifier() - if not attributes: - console.print( - ":exclamation: [yellow]A possible error has occurred[/yellow]. The extrinsic reports success, but " - "we are unable to locate the 'NetworkAdded' event inside the extrinsic's events." - "" - ) - else: - console.print( - f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" + attributes = await _find_event_attributes_in_extrinsic_receipt( + response, "NetworkAdded" ) - return True, int(attributes[0]), ext_id + await print_extrinsic_id(response) + ext_id = await response.get_extrinsic_identifier() + if not attributes: + console.print( + ":exclamation: [yellow]A possible error has occurred[/yellow]. The extrinsic reports success, but " + "we are unable to locate the 'NetworkAdded' event inside the extrinsic's events." + "" + ) + else: + console.print( + f":white_heavy_check_mark: [dark_sea_green3]Registered subnetwork with netuid: {attributes[0]}" + ) + return True, int(attributes[0]), ext_id # commands