From 9059da9b3aceaf467e6e9b18a46a3870a92979d6 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 22 Dec 2025 16:44:01 -0800 Subject: [PATCH 01/19] add ColdkeySwapAnnouncementInfo --- bittensor_cli/src/bittensor/chain_data.py | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index cfcc699f5..dc8436c38 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -901,6 +901,38 @@ def _fix_decoded(cls, decoded: Any) -> "ScheduledColdkeySwapInfo": ) +@dataclass +class ColdkeySwapAnnouncementInfo(InfoBase): + """ + Information about a coldkey swap announcement. + + Contains information about a pending coldkey swap announcement when a coldkey + wants to declare its intent to swap to a new coldkey address. + The announcement is made before the actual swap can be executed, + allowing time for verification and security checks. + + The destination coldkey address is stored as a hash. + This is to prevent the actual coldkey address from being exposed + to the network. The hash is computed using the BlakeTwo256 hashing algorithm. + """ + + coldkey: str + execution_block: int + new_coldkey_hash: str + + @classmethod + def _fix_decoded( + cls, coldkey: str, decoded: tuple + ) -> "ColdkeySwapAnnouncementInfo": + execution_block, new_coldkey_hash = decoded + hash_str = "0x" + bytes(new_coldkey_hash[0]).hex() + return cls( + coldkey=coldkey, + execution_block=int(execution_block), + new_coldkey_hash=hash_str, + ) + + @dataclass class SubnetState(InfoBase): netuid: int From 20e14b9542f0ef469f483701fb57baa375afac32 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 22 Dec 2025 16:44:35 -0800 Subject: [PATCH 02/19] removes deprecated ScheduledColdkeySwapInfo --- bittensor_cli/src/bittensor/chain_data.py | 24 +++-------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/bittensor_cli/src/bittensor/chain_data.py b/bittensor_cli/src/bittensor/chain_data.py index dc8436c38..718dbd694 100644 --- a/bittensor_cli/src/bittensor/chain_data.py +++ b/bittensor_cli/src/bittensor/chain_data.py @@ -883,35 +883,17 @@ def alpha_to_tao_with_slippage( return tao_returned, slippage, slippage_pct_float -@dataclass -class ScheduledColdkeySwapInfo(InfoBase): - """Dataclass for scheduled coldkey swap information.""" - - old_coldkey: str - new_coldkey: str - arbitration_block: int - - @classmethod - def _fix_decoded(cls, decoded: Any) -> "ScheduledColdkeySwapInfo": - """Fixes the decoded values.""" - return cls( - old_coldkey=decode_account_id(decoded.get("old_coldkey")), - new_coldkey=decode_account_id(decoded.get("new_coldkey")), - arbitration_block=decoded.get("arbitration_block"), - ) - - @dataclass class ColdkeySwapAnnouncementInfo(InfoBase): """ Information about a coldkey swap announcement. Contains information about a pending coldkey swap announcement when a coldkey - wants to declare its intent to swap to a new coldkey address. - The announcement is made before the actual swap can be executed, + wants to declare its intent to swap to a new coldkey address. + The announcement is made before the actual swap can be executed, allowing time for verification and security checks. - The destination coldkey address is stored as a hash. + The destination coldkey address is stored as a hash. This is to prevent the actual coldkey address from being exposed to the network. The hash is computed using the BlakeTwo256 hashing algorithm. """ From 52d3d142dbd1b67826a90472a4a4d7e47264a67c Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 22 Dec 2025 16:48:25 -0800 Subject: [PATCH 03/19] update wallet_check_ck_swap --- bittensor_cli/cli.py | 63 +++++++++++++++----------------------------- 1 file changed, 21 insertions(+), 42 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 16e68c254..e06c9c9b5 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -3519,62 +3519,58 @@ def wallet_check_ck_swap( wallet_ss58_address: Optional[str] = Options.wallet_ss58_address, wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, - scheduled_block: Optional[int] = typer.Option( - None, - "--block", - help="Block number where the swap was scheduled", - ), show_all: bool = typer.Option( False, "--all", "-a", - help="Show all pending coldkey swaps", + help="Show all pending coldkey swap announcements", ), network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): """ - Check the status of scheduled coldkey swaps. + Check the status of pending coldkey swap announcements. + + Coldkey swaps use a two-step announcement system. Use this command + to check if you have any pending announcements and when they become executable. USAGE - This command can be used in three ways: - 1. Show all pending swaps (--all) - 2. Check status of a specific wallet's swap or SS58 address - 3. Check detailed swap status with block number (--block) + This command can be used in two ways: + + 1. Show all pending announcements (--all) + + 2. Check status of a specific wallet or SS58 address EXAMPLES - Show all pending swaps: + 1. Show all pending swap announcements: + [green]$[/green] btcli wallet swap-check --all - Check specific wallet's swap: + 2. Check specific wallet's announcement: + [green]$[/green] btcli wallet swap-check --wallet-name my_wallet - Check swap using SS58 address: - [green]$[/green] btcli wallet swap-check --ss58 5DkQ4... + 3. Check announcement using SS58 address: - Check swap details with block number: - [green]$[/green] btcli wallet swap-check --wallet-name my_wallet --block 12345 + [green]$[/green] btcli wallet swap-check --ss58 5DkQ4... """ - # TODO add json_output if this ever gets used again (doubtful) self.verbosity_handler(quiet, verbose, json_output=False, prompt=False) self.initialize_chain(network) if show_all: - return self._run_command( - wallets.check_swap_status(self.subtensor, None, None) - ) + return self._run_command(wallets.check_swap_status(self.subtensor, None)) if not wallet_ss58_address: wallet_ss58_address = Prompt.ask( "Enter [blue]wallet name[/blue] or [blue]SS58 address[/blue] [dim]" - "(leave blank to show all pending swaps)[/dim]" + "(leave blank to show all pending announcements)[/dim]" ) if not wallet_ss58_address: return self._run_command( - wallets.check_swap_status(self.subtensor, None, None) + wallets.check_swap_status(self.subtensor, None) ) if is_valid_ss58_address(wallet_ss58_address): @@ -3589,26 +3585,9 @@ def wallet_check_ck_swap( ) ss58_address = wallet.coldkeypub.ss58_address - if not scheduled_block: - block_input = Prompt.ask( - "[blue]Enter the block number[/blue] where the swap was scheduled " - "[dim](optional, press enter to skip)[/dim]", - default="", - ) - if block_input: - try: - scheduled_block = int(block_input) - except ValueError: - print_error("Invalid block number") - raise typer.Exit() - logger.debug( - "args:\n" - f"scheduled_block {scheduled_block}\n" - f"ss58_address {ss58_address}\n" - f"network {network}\n" - ) + logger.debug(f"args:\nss58_address {ss58_address}\nnetwork {network}\n") return self._run_command( - wallets.check_swap_status(self.subtensor, ss58_address, scheduled_block) + wallets.check_swap_status(self.subtensor, ss58_address) ) def wallet_create_wallet( From 52618ce603fb5e4a87ed9b3ec9f8d081f24ad5ef Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 22 Dec 2025 17:24:47 -0800 Subject: [PATCH 04/19] updates check_swap_status --- bittensor_cli/src/commands/wallets.py | 129 +++++++++++++------------- 1 file changed, 62 insertions(+), 67 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 216d7bf10..6d76650a8 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2225,85 +2225,80 @@ async def find_coldkey_swap_extrinsic( async def check_swap_status( subtensor: SubtensorInterface, origin_ss58: Optional[str] = None, - expected_block_number: Optional[int] = None, ) -> None: - """ - Check the status of a coldkey swap. + """Retrieves and displays the status of coldkey swap announcements. Args: - subtensor: Connection to the network - origin_ss58: The SS58 address of the original coldkey - expected_block_number: Optional block number where the swap was scheduled - + subtensor: Connection to the network. + origin_ss58: The SS58 address of the coldkey to check. If None, shows all pending announcements. """ - - if not origin_ss58: - scheduled_swaps = await subtensor.get_scheduled_coldkey_swap() - if not scheduled_swaps: - console.print("[yellow]No pending coldkey swaps found.[/yellow]") - return - - table = Table( - Column( - "Original Coldkey", - justify="Left", - style=COLOR_PALETTE["GENERAL"]["SUBHEADING_MAIN"], - no_wrap=True, - ), - Column("Status", style="dark_sea_green3"), - title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Pending Coldkey Swaps\n", - show_header=True, - show_edge=False, - header_style="bold white", - border_style="bright_black", - style="bold", - title_justify="center", - show_lines=False, - pad_edge=True, + block_hash = await subtensor.substrate.get_chain_head() + if origin_ss58: + announcement, current_block = await asyncio.gather( + subtensor.get_coldkey_swap_announcement(origin_ss58, block_hash=block_hash), + subtensor.substrate.get_block_number(block_hash=block_hash), ) + if not announcement: + console.print( + f"[yellow]No pending swap announcement found for coldkey:[/yellow] " + f"[{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]" + ) + return + announcements = [announcement] - for coldkey in scheduled_swaps: - table.add_row(coldkey, "Pending") - - console.print(table) - console.print( - "\n[dim]Tip: Check specific swap details by providing the original coldkey " - "SS58 address and the block number.[/dim]" - ) - return - chain_reported_completion_block, destination_address = await subtensor.query( - "SubtensorModule", "ColdkeySwapScheduled", [origin_ss58] - ) - destination_address = decode_account_id(destination_address[0]) - if chain_reported_completion_block != 0 and destination_address != GENESIS_ADDRESS: - is_pending = True else: - is_pending = False - - if not is_pending: - console.print( - f"[red]No pending swap found for coldkey:[/red] [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]" + announcements, current_block = await asyncio.gather( + subtensor.get_coldkey_swap_announcements(block_hash=block_hash), + subtensor.substrate.get_block_number(block_hash=block_hash), ) - return + if not announcements: + console.print( + "[yellow]No pending coldkey swap announcements found.[/yellow]" + ) + return - console.print( - f"[green]Found pending swap for coldkey:[/green] [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]" + table = Table( + Column( + "Coldkey", + justify="left", + style=COLOR_PALETTE["GENERAL"]["SUBHEADING_MAIN"], + no_wrap=True, + ), + Column("New Coldkey Hash", justify="left", style="dim", no_wrap=True), + Column("Execution Block", justify="right", style="dark_sea_green3"), + Column("Time Remaining", justify="right", style="yellow"), + Column("Status", justify="center", style="green"), + title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Pending Coldkey Swap Announcements\nCurrent Block: {current_block}\n", + show_header=True, + show_edge=False, + header_style="bold white", + border_style="bright_black", + style="bold", + title_justify="center", + show_lines=False, + pad_edge=True, ) - if expected_block_number is None: - expected_block_number = chain_reported_completion_block - - current_block = await subtensor.substrate.get_block_number() - remaining_blocks = expected_block_number - current_block + for announcement in announcements: + remaining_blocks = announcement.execution_block - current_block + if remaining_blocks <= 0: + status = "Ready" + time_str = "[green]Ready[/green]" + else: + status = "Pending" + time_str = blocks_to_duration(remaining_blocks) + hash_display = f"{announcement.new_coldkey_hash[:12]}...{announcement.new_coldkey_hash[-6:]}" - if remaining_blocks <= 0: - console.print("[green]Swap period has completed![/green]") - return + table.add_row( + announcement.coldkey, + hash_display, + str(announcement.execution_block), + time_str, + status, + ) + console.print(table) console.print( - "\n[green]Coldkey swap details:[/green]" - f"\nOriginal address: [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]" - f"\nDestination address: [{COLORS.G.CK}]{destination_address}[/{COLORS.G.CK}]" - f"\nCompletion block: {chain_reported_completion_block}" - f"\nTime remaining: {blocks_to_duration(remaining_blocks)}" + "\n[dim]To execute a ready swap:[/dim] " + "[green]btcli wallet swap-coldkey execute[/green]" ) From 4b8c7df3077211216f0c85f897ed94cd3504e1d9 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 22 Dec 2025 17:28:34 -0800 Subject: [PATCH 05/19] add get_coldkey_swap_announcements --- .../src/bittensor/subtensor_interface.py | 58 +++++++++++++++---- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index bf2f91a23..bd5220018 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -30,6 +30,7 @@ MetagraphInfo, SimSwapResult, CrowdloanData, + ColdkeySwapAnnouncementInfo, ) from bittensor_cli.src import DelegatesDetails from bittensor_cli.src.bittensor.balances import Balance, fixed_to_float @@ -1805,30 +1806,63 @@ async def sim_swap( destination_netuid, ) - async def get_scheduled_coldkey_swap( + async def get_coldkey_swap_announcements( self, block_hash: Optional[str] = None, reuse_block: bool = False, - ) -> Optional[list[str]]: - """ - Queries the chain to fetch the list of coldkeys that are scheduled for a swap. + ) -> list[ColdkeySwapAnnouncementInfo]: + """Fetches all pending coldkey swap announcements. - :param block_hash: Block hash at which to perform query. - :param reuse_block: Whether to reuse the last-used block hash. + Args: + block_hash: Block hash at which to perform query. + reuse_block: Whether to reuse the last-used block hash. - :return: A list of SS58 addresses of the coldkeys that are scheduled for a coldkey swap. + Returns: + A list of ColdkeySwapAnnouncementInfo for all pending announcements. """ result = await self.substrate.query_map( module="SubtensorModule", - storage_function="ColdkeySwapScheduled", + storage_function="ColdkeySwapAnnouncements", + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + announcements = [] + async for ss58, data in result: + coldkey = decode_account_id(ss58) + announcements.append( + ColdkeySwapAnnouncementInfo._fix_decoded(coldkey, data) + ) + return announcements + + async def get_coldkey_swap_announcement( + self, + coldkey_ss58: str, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Optional[ColdkeySwapAnnouncementInfo]: + """Fetches a pending coldkey swap announcement for a specific coldkey. + + Args: + coldkey_ss58: The SS58 address of the coldkey to query. + block_hash: Block hash at which to perform query. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + ColdkeySwapAnnouncementInfo if an announcement exists, None otherwise. + """ + result = await self.query( + module="SubtensorModule", + storage_function="ColdkeySwapAnnouncements", + params=[coldkey_ss58], block_hash=block_hash, reuse_block_hash=reuse_block, ) - keys_pending_swap = [] - async for ss58, _ in result: - keys_pending_swap.append(decode_account_id(ss58)) - return keys_pending_swap + if result is None: + return None + + return ColdkeySwapAnnouncementInfo._fix_decoded(coldkey_ss58, result) async def get_crowdloans( self, block_hash: Optional[str] = None From 476778440eea8bf594fd892ec87b539085397b08 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 22 Dec 2025 19:37:15 -0800 Subject: [PATCH 06/19] update wallet_swap_coldkey cmd --- bittensor_cli/cli.py | 99 ++++++++++++++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 32 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index e06c9c9b5..925806f51 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4105,6 +4105,10 @@ def wallet_verify( def wallet_swap_coldkey( self, + action: str = typer.Argument( + None, + help="Action to perform: 'announce' to announce intent, 'execute' to complete swap after delay.", + ), wallet_name: Optional[str] = Options.wallet_name, wallet_path: Optional[str] = Options.wallet_path, wallet_hotkey: Optional[str] = Options.wallet_hotkey, @@ -4114,38 +4118,58 @@ def wallet_swap_coldkey( "--new-coldkey-ss58", "--new-wallet", "--new", - help="SS58 address of the new coldkey that will replace the current one.", + help="SS58 address or wallet name of the new coldkey.", ), network: Optional[list[str]] = Options.network, - proxy: Optional[str] = Options.proxy, - announce_only: bool = Options.announce_only, quiet: bool = Options.quiet, verbose: bool = Options.verbose, - force_swap: bool = typer.Option( - False, - "--force", - "-f", - "--force-swap", - help="Force the swap even if the new coldkey is already scheduled for a swap.", - ), + decline: bool = Options.decline, ): """ - Schedule a coldkey swap for a wallet. + Swap your coldkey to a new address using a two-step announcement process. + + Coldkey swaps require two steps for security: + + 1. [bold]Announce[/bold]: Declare your intent to swap. This pays the swap fee and starts a delay period. - This command allows you to schedule a coldkey swap for a wallet. You can either provide a new wallet name, or SS58 address. + 2. [bold]Execute[/bold]: After the delay (typically 5 days), complete the swap. EXAMPLES - [green]$[/green] btcli wallet schedule-coldkey-swap --new-wallet my_new_wallet + Step 1 - Announce your intent to swap: + + [green]$[/green] btcli wallet swap-coldkey announce --new-coldkey 5Dk...X3q + + Step 2 - After the delay period, execute the swap: - [green]$[/green] btcli wallet schedule-coldkey-swap --new-coldkey-ss58 5Dk...X3q + [green]$[/green] btcli wallet swap-coldkey execute --new-coldkey 5Dk...X3q + + Check status of pending swaps: + + [green]$[/green] btcli wallet swap-check """ self.verbosity_handler(quiet, verbose, prompt=False, json_output=False) - proxy = self.is_valid_proxy_name_or_ss58(proxy, announce_only) + + if not action: + console.print( + "\n[bold][blue]Coldkey Swap Actions:[/blue][/bold]\n" + " [dark_sea_green3]announce[/dark_sea_green3] - Start the swap process (pays fee, starts delay timer)\n" + " [dark_sea_green3]execute[/dark_sea_green3] - Complete the swap (after delay period)\n\n" + " [dim]You can check the current status of your swap with 'btcli wallet swap-check'.[/dim]\n" + ) + action = Prompt.ask( + "Select action", + choices=["announce", "execute"], + default="announce", + ) + + if action.lower() not in ("announce", "execute"): + print_error(f"Invalid action: {action}. Must be 'announce' or 'execute'.") + raise typer.Exit(1) if not wallet_name: wallet_name = Prompt.ask( - "Enter the [blue]wallet name[/blue] which you want to swap the coldkey for", + "Enter the [blue]wallet name[/blue] of the coldkey to swap", default=self.config.get("wallet_name") or defaults.wallet.name, ) wallet = self.wallet_ask( @@ -4156,8 +4180,7 @@ def wallet_swap_coldkey( validate=WV.WALLET, ) console.print( - f"\nWallet selected to swap the [blue]coldkey[/blue] from: \n" - f"[dark_sea_green3]{wallet}[/dark_sea_green3]\n" + f"\nWallet selected: [dark_sea_green3]{wallet}[/dark_sea_green3]\n" ) if not new_wallet_or_ss58: @@ -4177,25 +4200,37 @@ def wallet_swap_coldkey( validate=WV.WALLET, ) console.print( - f"\nNew wallet to swap the [blue]coldkey[/blue] to: \n" - f"[dark_sea_green3]{new_wallet}[/dark_sea_green3]\n" + f"\nNew coldkey wallet: [dark_sea_green3]{new_wallet}[/dark_sea_green3]\n" ) new_wallet_coldkey_ss58 = new_wallet.coldkeypub.ss58_address + logger.debug( - "args:\n" - f"network {network}\n" - f"new_coldkey_ss58 {new_wallet_coldkey_ss58}\n" - f"force_swap {force_swap}" + f"args:\n" + f"action: {action}\n" + f"network: {network}\n" + f"new_coldkey_ss58: {new_wallet_coldkey_ss58}" ) - return self._run_command( - wallets.schedule_coldkey_swap( - wallet=wallet, - subtensor=self.initialize_chain(network), - new_coldkey_ss58=new_wallet_coldkey_ss58, - force_swap=force_swap, - proxy=proxy, + + if action == "announce": + return self._run_command( + wallets.announce_coldkey_swap( + wallet=wallet, + subtensor=self.initialize_chain(network), + new_coldkey_ss58=new_wallet_coldkey_ss58, + decline=decline, + quiet=quiet, + ) + ) + else: + return self._run_command( + wallets.execute_coldkey_swap( + wallet=wallet, + subtensor=self.initialize_chain(network), + new_coldkey_ss58=new_wallet_coldkey_ss58, + decline=decline, + quiet=quiet, + ) ) - ) def axon_reset( self, From 7474832ca9fb8d4862b6e65812017416a37418d8 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 22 Dec 2025 19:44:45 -0800 Subject: [PATCH 07/19] add get_coldkey_swap_cost --- .../src/bittensor/subtensor_interface.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index bd5220018..4a435ff7b 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1984,6 +1984,30 @@ async def get_coldkey_swap_schedule_duration( return result + async def get_coldkey_swap_cost( + self, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> Balance: + """Retrieves the fee required to announce a coldkey swap. + + Args: + block_hash: Block hash at which to query the constant. + reuse_block: Whether to reuse the last-used block hash. + + Returns: + The swap cost as a Balance object. Returns 0 TAO if constant not found. + """ + swap_cost = await self.substrate.get_constant( + module_name="SubtensorModule", + constant_name="KeySwapCost", + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + if swap_cost is None: + return None + return Balance.from_rao(swap_cost.value) + async def get_coldkey_claim_type( self, coldkey_ss58: str, From 423ae03f10108865d4a8ff4a8bf520f2fe8c05ac Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 22 Dec 2025 19:45:22 -0800 Subject: [PATCH 08/19] add get_coldkey_swap_reannouncement_delay --- bittensor_cli/src/bittensor/subtensor_interface.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 4a435ff7b..7e39459ff 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1959,24 +1959,26 @@ async def get_crowdloan_contribution( return Balance.from_rao(contribution) return None - async def get_coldkey_swap_schedule_duration( + async def get_coldkey_swap_reannouncement_delay( self, block_hash: Optional[str] = None, reuse_block: bool = False, ) -> int: - """ - Retrieves the duration (in blocks) required for a coldkey swap to be executed. + """Retrieves the delay (in blocks) before the user can reannounce a coldkey swap. + + If the user has already announced a swap, they must wait this many blocks + after the original execution block before they can announce a new swap. Args: block_hash: The hash of the blockchain block number for the query. reuse_block: Whether to reuse the last-used blockchain block hash. Returns: - int: The number of blocks required for the coldkey swap schedule duration. + The number of blocks to wait before reannouncing. """ result = await self.query( module="SubtensorModule", - storage_function="ColdkeySwapScheduleDuration", + storage_function="ColdkeySwapReannouncementDelay", params=[], block_hash=block_hash, reuse_block_hash=reuse_block, From c7563b0a4525795f145ba77821dad526fd6f9356 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Mon, 22 Dec 2025 19:45:49 -0800 Subject: [PATCH 09/19] add get_coldkey_swap_announcement_delay --- .../src/bittensor/subtensor_interface.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 7e39459ff..5daec09b9 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1959,6 +1959,33 @@ async def get_crowdloan_contribution( return Balance.from_rao(contribution) return None + async def get_coldkey_swap_announcement_delay( + self, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> int: + """Retrieves the delay (in blocks) before a coldkey swap can be executed. + + This is the time the user must wait after announcing a coldkey swap + before they can execute the swap. + + Args: + block_hash: The hash of the blockchain block number for the query. + reuse_block: Whether to reuse the last-used blockchain block hash. + + Returns: + The number of blocks to wait after announcement. + """ + result = await self.query( + module="SubtensorModule", + storage_function="ColdkeySwapAnnouncementDelay", + params=[], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + return result + async def get_coldkey_swap_reannouncement_delay( self, block_hash: Optional[str] = None, From c45dd04c74e557729ca1df4bcd8d7c35c3cc56c7 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 23 Dec 2025 12:34:12 -0800 Subject: [PATCH 10/19] add reusable table --- bittensor_cli/src/commands/wallets.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 6d76650a8..208df6aba 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1,4 +1,5 @@ import asyncio +import hashlib import itertools import json import os @@ -1767,11 +1768,16 @@ async def swap_hotkey( return result -def create_identity_table(title: str = None): - if not title: - title = "On-Chain Identity" +def create_key_value_table(title: str = "Details") -> Table: + """Creates a key-value table for displaying information for various cmds. - table = Table( + Args: + title: The title shown above the table. + + Returns: + A Rich Table for key-value display. + """ + return Table( Column( "Item", justify="right", @@ -1789,7 +1795,6 @@ def create_identity_table(title: str = None): show_lines=False, pad_edge=True, ) - return table async def set_id( @@ -1846,7 +1851,7 @@ async def set_id( output_dict["success"] = True identity = await subtensor.query_identity(wallet.coldkeypub.ss58_address) - table = create_identity_table(title="New on-chain Identity") + table = create_key_value_table(title="New on-chain Identity\n") table.add_row("Address", wallet.coldkeypub.ss58_address) for key, value in identity.items(): table.add_row(key, str(value) if value else "~") @@ -1880,7 +1885,7 @@ async def get_id( json_console.print("{}") return {} - table = create_identity_table(title) + table = create_key_value_table(title) table.add_row("Address", ss58_address) for key, value in identity.items(): table.add_row(key, str(value) if value else "~") From 4b41956bf44660447eafc3105241707758e05816 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 23 Dec 2025 12:35:02 -0800 Subject: [PATCH 11/19] add compute_coldkey_hash --- bittensor_cli/src/commands/wallets.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 208df6aba..7f87280d5 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2046,6 +2046,26 @@ async def verify( return is_valid +def compute_coldkey_hash(ss58_address: str) -> str: + """ + Compute Blake2b-256 hash of a coldkey AccountId. + + Args: + ss58_address: SS58 address of the coldkey. + + Returns: + str: 0x-prefixed hex hash. + + Notes: + Hashes AccountId bytes (not the SS58). Used by coldkey-swap announcements. + """ + keypair = Keypair(ss58_address=ss58_address) + public_key_bytes = keypair.public_key + + hash_result = hashlib.blake2b(public_key_bytes, digest_size=32) + return "0x" + hash_result.hexdigest() + + async def schedule_coldkey_swap( wallet: Wallet, subtensor: SubtensorInterface, From a2d56e0c4198ae97f75781db8d8f2effe45319b9 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 23 Dec 2025 12:36:17 -0800 Subject: [PATCH 12/19] add announce_coldkey_swap --- bittensor_cli/src/commands/wallets.py | 202 ++++++++++++++++++-------- 1 file changed, 138 insertions(+), 64 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 7f87280d5..1b0f8fdce 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2066,66 +2066,139 @@ def compute_coldkey_hash(ss58_address: str) -> str: return "0x" + hash_result.hexdigest() -async def schedule_coldkey_swap( +async def announce_coldkey_swap( wallet: Wallet, subtensor: SubtensorInterface, new_coldkey_ss58: str, - force_swap: bool = False, decline: bool = False, quiet: bool = False, proxy: Optional[str] = None, ) -> bool: - """Schedules a coldkey swap operation to be executed at a future block. + """Announces intent to swap a coldkey to a new address. + + This is the first step of a two-step coldkey swap process. After announcing, + the user must wait for the announcement delay period to pass before executing + the swap with execute_coldkey_swap. Args: - wallet (Wallet): The wallet initiating the coldkey swap - subtensor (SubtensorInterface): Connection to the Bittensor network - new_coldkey_ss58 (str): SS58 address of the new coldkey - force_swap (bool, optional): Whether to force the swap even if the new coldkey is already scheduled for a swap. Defaults to False. + wallet: The wallet initiating the coldkey swap. + subtensor: Connection to the Bittensor network. + new_coldkey_ss58: SS58 address of the new coldkey. + decline: If True, skip confirmation prompt and decline. + quiet: If True, skip confirmation prompt and proceed. + proxy: Optional proxy SS58 address for the transaction. + Returns: - bool: True if the swap was scheduled successfully, False otherwise + True if the announcement was successful, False otherwise. """ if not is_valid_ss58_address(new_coldkey_ss58): print_error(f"Invalid SS58 address format: {new_coldkey_ss58}") return False - scheduled_coldkey_swap = await subtensor.get_scheduled_coldkey_swap() - if wallet.coldkeypub.ss58_address in scheduled_coldkey_swap: - print_error( - f"Coldkey {wallet.coldkeypub.ss58_address} is already scheduled for a swap." - ) - console.print("[dim]Use the force_swap (--force) flag to override this.[/dim]") - if not force_swap: + # Check for existing announcement + block_hash = await subtensor.substrate.get_chain_head() + new_coldkey_hash = compute_coldkey_hash(new_coldkey_ss58) + + existing = await subtensor.get_coldkey_swap_announcement( + wallet.coldkeypub.ss58_address, + block_hash=block_hash, + ) + if existing: + current_block, reannounce_delay, announce_delay = await asyncio.gather( + subtensor.substrate.get_block_number(block_hash=block_hash), + subtensor.get_coldkey_swap_reannouncement_delay(block_hash=block_hash), + subtensor.get_coldkey_swap_announcement_delay(block_hash=block_hash), + ) + remaining = existing.execution_block - current_block + reannounce_block = existing.execution_block + reannounce_delay + same_hash = new_coldkey_hash.lower() == str(existing.new_coldkey_hash).lower() + + # Show existing announcement info + table = create_key_value_table("Existing Coldkey Swap Announcement") + table.add_row("Execution Block", str(existing.execution_block)) + if remaining > 0: + table.add_row( + "Time Remaining", f"[yellow]{blocks_to_duration(remaining)}[/yellow]" + ) + table.add_row("Status", "[yellow]Pending[/yellow]") + else: + table.add_row("Status", "[green]Ready to Execute[/green]") + table.add_row("Announced Hash", f"[dim]{existing.new_coldkey_hash}[/dim]") + table.add_row("Requested Hash", f"[dim]{new_coldkey_hash}[/dim]") + table.add_row("Match", "[green]Yes[/green]" if same_hash else "[red]No[/red]") + console.print(table) + + # Check if reannouncement allowed + if current_block < reannounce_block: + time_until_reannounce = blocks_to_duration(reannounce_block - current_block) + console.print( + f"\n[dim]You can reannounce after block {reannounce_block} ({time_until_reannounce} from now).[/dim]", + f"Current block: {current_block}", + ) return False + + # Reannouncement allowed + if same_hash: + console.print( + "\n[yellow]You already have an announcement for this coldkey.[/yellow] " + "You can execute the existing swap without reannouncing." + ) + if not confirm_action( + "Do you still want to reannounce the same hash (the period to wait before executing the swap will be reset)?", + decline=decline, + quiet=quiet, + ): + return False else: console.print( - "[yellow]Continuing with the swap due to force_swap flag.[/yellow]\n" + f"\n[dim]Reannouncing with a different coldkey will reset the waiting period " + f"to {blocks_to_duration(announce_delay)} from now.[/dim]" ) + if not confirm_action( + "Proceed with reannouncement and reset the waiting period?", + decline=decline, + quiet=quiet, + ): + return False - prompt_msg = ( - "You are [red]swapping[/red] your [blue]coldkey[/blue] to a new address.\n" - f"Current ss58: [{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]\n" - f"New ss58: [{COLORS.G.CK}]{new_coldkey_ss58}[/{COLORS.G.CK}]\n" - "Are you sure you want to continue?" + # Proceed with the announcement + swap_cost, delay = await asyncio.gather( + subtensor.get_coldkey_swap_cost(block_hash=block_hash), + subtensor.get_coldkey_swap_announcement_delay(block_hash=block_hash), + ) + + table = create_key_value_table("Announcing Coldkey Swap\n") + table.add_row( + "Current Coldkey", + f"[{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]", + ) + table.add_row("New Coldkey", f"[{COLORS.G.CK}]{new_coldkey_ss58}[/{COLORS.G.CK}]") + table.add_row( + "New Coldkey Hash", + f"[dim]{new_coldkey_hash}[/dim]", + ) + table.add_row("Swap Cost", f"[green]{swap_cost}[/green]") + table.add_row( + "Delay Before Execution", f"[yellow]{blocks_to_duration(delay)}[/yellow]" ) - if not confirm_action(prompt_msg, decline=decline, quiet=quiet): + console.print(table) + + if not confirm_action( + "Are you sure you want to continue?", decline=decline, quiet=quiet + ): return False if not unlock_key(wallet).success: return False - block_pre_call, call = await asyncio.gather( - subtensor.substrate.get_block_number(), - subtensor.substrate.compose_call( + with console.status(":satellite: Announcing coldkey swap on-chain..."): + call = await subtensor.substrate.compose_call( call_module="SubtensorModule", - call_function="schedule_swap_coldkey", + call_function="announce_coldkey_swap", call_params={ - "new_coldkey": new_coldkey_ss58, + "new_coldkey_hash": new_coldkey_hash, }, - ), - ) - swap_info = None - with console.status(":satellite: Scheduling coldkey swap on-chain..."): + ) success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( call, wallet, @@ -2133,53 +2206,54 @@ async def schedule_coldkey_swap( wait_for_finalization=True, proxy=proxy, ) - block_post_call = await subtensor.substrate.get_block_number() if not success: - print_error(f"Failed to schedule coldkey swap: {err_msg}") + print_error(f"Failed to announce coldkey swap: {err_msg}") return False console.print( - ":white_heavy_check_mark: [green]Successfully scheduled coldkey swap" + ":white_heavy_check_mark: [green]Successfully announced coldkey swap[/green]" ) 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]) - 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"], - } + # Post-success information + new_block_hash = await subtensor.substrate.get_chain_head() + announcement = await subtensor.get_coldkey_swap_announcement( + wallet.coldkeypub.ss58_address, + block_hash=new_block_hash, + ) + if announcement: + current_block = await subtensor.substrate.get_block_number( + block_hash=new_block_hash + ) + remaining = announcement.execution_block - current_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, + details_table = create_key_value_table("Coldkey Swap Announced\n") + details_table.add_row( + "Original Coldkey", + f"[{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]", + ) + details_table.add_row( + "New Coldkey", f"[{COLORS.G.CK}]{new_coldkey_ss58}[/{COLORS.G.CK}]" + ) + details_table.add_row( + "New Coldkey Hash", + f"[dim]{new_coldkey_hash}[/dim]", + ) + details_table.add_row( + "Execution Block", f"[green]{announcement.execution_block}[/green]" + ) + details_table.add_row( + "Time Until Executable", f"[yellow]{blocks_to_duration(remaining)}[/yellow]" ) - if not swap_info: + console.print(details_table) console.print( - "[yellow]Warning: Could not find the swap extrinsic in recent blocks" + f"\n[dim]After the delay, run:" + f"\n[green]btcli wallet swap-coldkey execute --new-coldkey {new_coldkey_ss58}[/green]" ) - return True - console.print( - "\n[green]Coldkey swap details:[/green]" - f"\nBlock number: {swap_info['block_num']}" - f"\nOriginal address: [{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]" - f"\nDestination address: [{COLORS.G.CK}]{swap_info['dest_coldkey']}[/{COLORS.G.CK}]" - f"\nThe swap will be completed at block: [green]{swap_info['execution_block']}[/green]" - f"\n[dim]You can provide the block number to `btcli wallet swap-check`[/dim]" - ) + return True async def find_coldkey_swap_extrinsic( From 78b7c9e9d1517a0706c6bb367dd70f989ddd85c3 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 23 Dec 2025 12:36:46 -0800 Subject: [PATCH 13/19] add execute_coldkey_swap --- bittensor_cli/src/commands/wallets.py | 165 ++++++++++++++++++-------- 1 file changed, 114 insertions(+), 51 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 1b0f8fdce..353c085b4 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2256,69 +2256,132 @@ async def announce_coldkey_swap( return True -async def find_coldkey_swap_extrinsic( +async def execute_coldkey_swap( + wallet: Wallet, subtensor: SubtensorInterface, - start_block: int, - end_block: int, - wallet_ss58: str, -) -> dict: - """Search for a coldkey swap event in a range of blocks. + new_coldkey_ss58: str, + decline: bool = False, + quiet: bool = False, + proxy: Optional[str] = None, +) -> bool: + """Executes a previously announced coldkey swap. + + This is the second step of a two-step coldkey swap process. You must have + previously called announce_coldkey_swap and waited for the delay period. Args: - subtensor: SubtensorInterface for chain queries - start_block: Starting block number to search - end_block: Ending block number to search (inclusive) - wallet_ss58: SS58 address of the signing wallet + wallet: The wallet executing the coldkey swap. + subtensor: Connection to the Bittensor network. + new_coldkey_ss58: SS58 address of the new coldkey (must match announcement). + decline: If True, skip confirmation prompt and decline. + quiet: If True, skip confirmation prompt and proceed. + proxy: Optional proxy SS58 address for the transaction. Returns: - dict: Contains the following keys if found: - - block_num: Block number where swap was scheduled - - dest_coldkey: SS58 address of destination coldkey - - execution_block: Block number when swap will execute - Empty dict if not found + True if the swap was executed successfully, False otherwise. """ + if not is_valid_ss58_address(new_coldkey_ss58): + print_error(f"Invalid SS58 address format: {new_coldkey_ss58}") + return False - current_block, genesis_block = await asyncio.gather( - subtensor.substrate.get_block_number(), subtensor.substrate.get_block_hash(0) + block_hash = await subtensor.substrate.get_chain_head() + announcement = await subtensor.get_coldkey_swap_announcement( + wallet.coldkeypub.ss58_address, + block_hash=block_hash, ) - if ( - current_block - start_block > 300 - and genesis_block == Constants.genesis_block_hash_map["finney"] - ): - console.print("Querying archive node for coldkey swap events...") - await subtensor.substrate.close() - subtensor = SubtensorInterface("archive") - - block_hashes = await asyncio.gather( - *[ - subtensor.substrate.get_block_hash(block_num) - for block_num in range(start_block, end_block + 1) - ] + if not announcement: + print_error( + f"No pending coldkey swap announcement found for {wallet.coldkeypub.ss58_address}.\n" + "You must first announce your swap with 'btcli wallet swap-coldkey announce'." + ) + return False + + expected_hash = compute_coldkey_hash(new_coldkey_ss58) + if announcement.new_coldkey_hash != expected_hash: + table = create_key_value_table("Coldkey Hash Mismatch\n") + table.add_row("Announced Hash", f"[dim]{announcement.new_coldkey_hash}[/dim]") + table.add_row("Provided Hash", f"[dim]{expected_hash}[/dim]") + console.print(table) + print_error( + "The provided coldkey does not match the announced hash.\n" + "Make sure you're using the same coldkey you announced." + ) + return False + + current_block = await subtensor.substrate.get_block_number(block_hash=block_hash) + if current_block < announcement.execution_block: + remaining = announcement.execution_block - current_block + table = create_key_value_table("Coldkey Swap Not Ready") + table.add_row("Current Block", str(current_block)) + table.add_row("Execution Block", str(announcement.execution_block)) + table.add_row( + "Time Remaining", f"[yellow]{blocks_to_duration(remaining)}[/yellow]" + ) + console.print(table) + print_error( + "Coldkey swap cannot be executed yet. Please wait for the delay period." + ) + return False + + # Display confirmation table + table = create_key_value_table("Executing Coldkey Swap\n") + table.add_row( + "Current Coldkey", + f"[{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]", ) - block_events = await asyncio.gather( - *[ - subtensor.substrate.get_events(block_hash=block_hash) - for block_hash in block_hashes - ] + table.add_row("New Coldkey", f"[{COLORS.G.CK}]{new_coldkey_ss58}[/{COLORS.G.CK}]") + + console.print(table) + console.print( + "\n[bold red]WARNING:[/bold red] This action is irreversible. All assets will be transferred.\n" ) - for block_num, events in zip(range(start_block, end_block + 1), block_events): - for event in 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]) - - if old_coldkey == wallet_ss58: - return { - "block_num": block_num, - "dest_coldkey": decode_account_id(attributes["new_coldkey"][0]), - "execution_block": attributes["execution_block"], - } + if not confirm_action( + "Are you sure you want to continue?", decline=decline, quiet=quiet + ): + return False + + if not unlock_key(wallet).success: + return False + + with console.status(":satellite: Executing coldkey swap on-chain..."): + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="swap_coldkey_announced", + call_params={ + "new_coldkey": new_coldkey_ss58, + }, + ) + success, err_msg, ext_receipt = await subtensor.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + proxy=proxy, + ) + + if not success: + print_error(f"Failed to execute coldkey swap: {err_msg}") + return False + + console.print( + ":white_heavy_check_mark: [green]Successfully executed coldkey swap![/green]" + ) + await print_extrinsic_id(ext_receipt) - return {} + # Success details table + success_table = create_key_value_table("Coldkey Swap Completed\n") + success_table.add_row( + "Old Coldkey", + f"[{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]", + ) + success_table.add_row( + "New Coldkey", f"[{COLORS.G.CK}]{new_coldkey_ss58}[/{COLORS.G.CK}]" + ) + console.print(success_table) + console.print("\n[dim]All assets have been transferred to the new coldkey.[/dim]") + + return True async def check_swap_status( From 8ce14a640f0553dbf7e7533cd8798eb2203551db Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 23 Dec 2025 12:44:40 -0800 Subject: [PATCH 14/19] remove proxy in ck swaps --- bittensor_cli/src/commands/wallets.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 353c085b4..c622a5647 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2072,7 +2072,6 @@ async def announce_coldkey_swap( new_coldkey_ss58: str, decline: bool = False, quiet: bool = False, - proxy: Optional[str] = None, ) -> bool: """Announces intent to swap a coldkey to a new address. @@ -2086,7 +2085,6 @@ async def announce_coldkey_swap( new_coldkey_ss58: SS58 address of the new coldkey. decline: If True, skip confirmation prompt and decline. quiet: If True, skip confirmation prompt and proceed. - proxy: Optional proxy SS58 address for the transaction. Returns: True if the announcement was successful, False otherwise. @@ -2204,7 +2202,6 @@ async def announce_coldkey_swap( wallet, wait_for_inclusion=True, wait_for_finalization=True, - proxy=proxy, ) if not success: @@ -2262,7 +2259,6 @@ async def execute_coldkey_swap( new_coldkey_ss58: str, decline: bool = False, quiet: bool = False, - proxy: Optional[str] = None, ) -> bool: """Executes a previously announced coldkey swap. @@ -2275,7 +2271,6 @@ async def execute_coldkey_swap( new_coldkey_ss58: SS58 address of the new coldkey (must match announcement). decline: If True, skip confirmation prompt and decline. quiet: If True, skip confirmation prompt and proceed. - proxy: Optional proxy SS58 address for the transaction. Returns: True if the swap was executed successfully, False otherwise. @@ -2357,7 +2352,6 @@ async def execute_coldkey_swap( wallet, wait_for_inclusion=True, wait_for_finalization=True, - proxy=proxy, ) if not success: From 266b1dff8796055eaecad3718ac66a4f49f789a3 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 23 Dec 2025 13:38:46 -0800 Subject: [PATCH 15/19] adds mev_protection and warning --- bittensor_cli/cli.py | 3 +++ bittensor_cli/src/commands/wallets.py | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 925806f51..bb9e10e13 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -4120,6 +4120,7 @@ def wallet_swap_coldkey( "--new", help="SS58 address or wallet name of the new coldkey.", ), + mev_protection: bool = Options.mev_protection, network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, @@ -4219,6 +4220,7 @@ def wallet_swap_coldkey( new_coldkey_ss58=new_wallet_coldkey_ss58, decline=decline, quiet=quiet, + mev_protection=mev_protection, ) ) else: @@ -4229,6 +4231,7 @@ def wallet_swap_coldkey( new_coldkey_ss58=new_wallet_coldkey_ss58, decline=decline, quiet=quiet, + mev_protection=mev_protection, ) ) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index c622a5647..b76c94deb 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -2072,6 +2072,7 @@ async def announce_coldkey_swap( new_coldkey_ss58: str, decline: bool = False, quiet: bool = False, + mev_protection: bool = False, ) -> bool: """Announces intent to swap a coldkey to a new address. @@ -2085,6 +2086,7 @@ async def announce_coldkey_swap( new_coldkey_ss58: SS58 address of the new coldkey. decline: If True, skip confirmation prompt and decline. quiet: If True, skip confirmation prompt and proceed. + mev_protection: If True, encrypt the extrinsic with MEV protection. Returns: True if the announcement was successful, False otherwise. @@ -2202,6 +2204,7 @@ async def announce_coldkey_swap( wallet, wait_for_inclusion=True, wait_for_finalization=True, + mev_protection=mev_protection, ) if not success: @@ -2259,6 +2262,7 @@ async def execute_coldkey_swap( new_coldkey_ss58: str, decline: bool = False, quiet: bool = False, + mev_protection: bool = True, ) -> bool: """Executes a previously announced coldkey swap. @@ -2271,6 +2275,7 @@ async def execute_coldkey_swap( new_coldkey_ss58: SS58 address of the new coldkey (must match announcement). decline: If True, skip confirmation prompt and decline. quiet: If True, skip confirmation prompt and proceed. + mev_protection: If True, encrypt the extrinsic with MEV protection. Returns: True if the swap was executed successfully, False otherwise. @@ -2279,6 +2284,12 @@ async def execute_coldkey_swap( print_error(f"Invalid SS58 address format: {new_coldkey_ss58}") return False + if not mev_protection: + console.print( + "[yellow]WARNING: MEV protection is disabled.\n" + "This transaction is not protected & will expose the new coldkey.[/yellow]" + ) + block_hash = await subtensor.substrate.get_chain_head() announcement = await subtensor.get_coldkey_swap_announcement( wallet.coldkeypub.ss58_address, @@ -2352,6 +2363,7 @@ async def execute_coldkey_swap( wallet, wait_for_inclusion=True, wait_for_finalization=True, + mev_protection=mev_protection, ) if not success: From e6c70507ef09644e5d2cdd3071cf1d5d6795bca4 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 23 Dec 2025 13:45:47 -0800 Subject: [PATCH 16/19] cleanup --- bittensor_cli/src/commands/wallets.py | 55 +++------------------------ 1 file changed, 5 insertions(+), 50 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index b76c94deb..acbd67ab4 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -16,7 +16,7 @@ from rich.table import Column, Table from rich.tree import Tree from rich.padding import Padding -from bittensor_cli.src import COLOR_PALETTE, COLORS, Constants +from bittensor_cli.src import COLOR_PALETTE, COLORS from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import ( @@ -29,18 +29,15 @@ ) from bittensor_cli.src.bittensor.extrinsics.transfer import transfer_extrinsic from bittensor_cli.src.bittensor.networking import int_to_ip -from bittensor_cli.src.bittensor.subtensor_interface import ( - SubtensorInterface, - GENESIS_ADDRESS, -) +from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface from bittensor_cli.src.bittensor.utils import ( RAO_PER_TAO, confirm_action, console, - convert_blocks_to_time, json_console, print_error, print_verbose, + print_success, get_all_wallets_for_path, get_hotkey_wallets_for_wallet, is_valid_ss58_address, @@ -50,7 +47,6 @@ unlock_key, WalletLike, blocks_to_duration, - decode_account_id, get_hotkey_pub_ss58, print_extrinsic_id, ) @@ -1896,43 +1892,6 @@ async def get_id( return identity -async def check_coldkey_swap(wallet: Wallet, subtensor: SubtensorInterface): - arbitration_check = len( # TODO verify this works - ( - await subtensor.query( - module="SubtensorModule", - storage_function="ColdkeySwapDestinations", - params=[wallet.coldkeypub.ss58_address], - ) - ) - ) - if arbitration_check == 0: - console.print( - "[green]There has been no previous key swap initiated for your coldkey.[/green]" - ) - elif arbitration_check == 1: - arbitration_block = await subtensor.query( - module="SubtensorModule", - storage_function="ColdkeyArbitrationBlock", - params=[wallet.coldkeypub.ss58_address], - ) - arbitration_remaining = ( - arbitration_block.value - await subtensor.substrate.get_block_number(None) - ) - - hours, minutes, seconds = convert_blocks_to_time(arbitration_remaining) - console.print( - "[yellow]There has been 1 swap request made for this coldkey already." - " By adding another swap request, the key will enter arbitration." - f" Your key swap is scheduled for {hours} hours, {minutes} minutes, {seconds} seconds" - " from now.[/yellow]" - ) - elif arbitration_check > 1: - console.print( - f"[red]This coldkey is currently in arbitration with a total swaps of {arbitration_check}.[/red]" - ) - - async def sign( wallet: Wallet, message: str, use_hotkey: bool, json_output: bool = False ): @@ -2211,9 +2170,7 @@ async def announce_coldkey_swap( print_error(f"Failed to announce coldkey swap: {err_msg}") return False - console.print( - ":white_heavy_check_mark: [green]Successfully announced coldkey swap[/green]" - ) + print_success("[dark_sea_green3]Successfully announced coldkey swap") await print_extrinsic_id(ext_receipt) # Post-success information @@ -2370,9 +2327,7 @@ async def execute_coldkey_swap( print_error(f"Failed to execute coldkey swap: {err_msg}") return False - console.print( - ":white_heavy_check_mark: [green]Successfully executed coldkey swap![/green]" - ) + print_success("[dark_sea_green3]Successfully executed coldkey swap!") await print_extrinsic_id(ext_receipt) # Success details table From 5fb8a746ca038d223eaf9743ae9dd39924873b4f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 23 Dec 2025 14:09:48 -0800 Subject: [PATCH 17/19] adds confirmation for mev_protection --- bittensor_cli/src/commands/wallets.py | 40 +++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index acbd67ab4..536b42438 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -23,6 +23,10 @@ DelegateInfo, NeuronInfoLite, ) +from bittensor_cli.src.bittensor.extrinsics.mev_shield import ( + extract_mev_shield_id, + wait_for_extrinsic_by_hash, +) from bittensor_cli.src.bittensor.extrinsics.registration import ( run_faucet_extrinsic, swap_hotkey_extrinsic, @@ -2150,7 +2154,7 @@ async def announce_coldkey_swap( if not unlock_key(wallet).success: return False - with console.status(":satellite: Announcing coldkey swap on-chain..."): + with console.status(":satellite: Announcing coldkey swap on-chain...") as status: call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="announce_coldkey_swap", @@ -2170,6 +2174,22 @@ async def announce_coldkey_swap( print_error(f"Failed to announce coldkey swap: {err_msg}") 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: + print_error( + f"Failed to announce coldkey swap: {mev_error}", status=status + ) + return False + print_success("[dark_sea_green3]Successfully announced coldkey swap") await print_extrinsic_id(ext_receipt) @@ -2307,7 +2327,7 @@ async def execute_coldkey_swap( if not unlock_key(wallet).success: return False - with console.status(":satellite: Executing coldkey swap on-chain..."): + with console.status(":satellite: Executing coldkey swap on-chain...") as status: call = await subtensor.substrate.compose_call( call_module="SubtensorModule", call_function="swap_coldkey_announced", @@ -2327,6 +2347,22 @@ async def execute_coldkey_swap( print_error(f"Failed to execute coldkey swap: {err_msg}") 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: + print_error( + f"Failed to execute coldkey swap: {mev_error}", status=status + ) + return False + print_success("[dark_sea_green3]Successfully executed coldkey swap!") await print_extrinsic_id(ext_receipt) From bac7f09eca87d10174f6693167c2955ad739085f Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 23 Dec 2025 14:23:07 -0800 Subject: [PATCH 18/19] remove proxy unit test for swap-ck - not applicable anymore --- tests/unit_tests/test_cli.py | 40 ------------------------------------ 1 file changed, 40 deletions(-) diff --git a/tests/unit_tests/test_cli.py b/tests/unit_tests/test_cli.py index 60cc10708..a793ac0c0 100644 --- a/tests/unit_tests/test_cli.py +++ b/tests/unit_tests/test_cli.py @@ -551,46 +551,6 @@ def test_wallet_set_id_calls_proxy_validation(): mock_proxy_validation.assert_called_once_with(valid_proxy, False) -def test_wallet_swap_coldkey_calls_proxy_validation(): - """Test that wallet_swap_coldkey calls is_valid_proxy_name_or_ss58""" - cli_manager = CLIManager() - valid_proxy = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" - new_coldkey = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" - - with ( - patch.object(cli_manager, "verbosity_handler"), - patch.object(cli_manager, "wallet_ask") as mock_wallet_ask, - patch.object(cli_manager, "initialize_chain"), - patch.object(cli_manager, "_run_command"), - patch("bittensor_cli.cli.is_valid_ss58_address", return_value=True), - patch.object( - cli_manager, "is_valid_proxy_name_or_ss58", return_value=valid_proxy - ) as mock_proxy_validation, - ): - mock_wallet = Mock() - mock_wallet.coldkeypub = Mock() - mock_wallet.coldkeypub.ss58_address = ( - "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" - ) - mock_wallet_ask.return_value = mock_wallet - - cli_manager.wallet_swap_coldkey( - wallet_name="test_wallet", - wallet_path="/tmp/test", - wallet_hotkey="test_hotkey", - new_wallet_or_ss58=new_coldkey, - network=None, - proxy=valid_proxy, - announce_only=False, - quiet=True, - verbose=False, - force_swap=False, - ) - - # Assert that proxy validation was called - mock_proxy_validation.assert_called_once_with(valid_proxy, False) - - def test_stake_move_calls_proxy_validation(): """Test that stake_move calls is_valid_proxy_name_or_ss58""" cli_manager = CLIManager() From dfdc6620824f463a05dd8cc92482b6e31fb79046 Mon Sep 17 00:00:00 2001 From: ibraheem-latent Date: Tue, 23 Dec 2025 14:39:50 -0800 Subject: [PATCH 19/19] update tests --- tests/e2e_tests/test_wallet_interactions.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index 7ed705b65..02797b978 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -447,17 +447,17 @@ def test_wallet_identities(local_chain, wallet_setup): 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] + assert alice_identity["name"] in set_id_output[8] + assert alice_identity["url"] in set_id_output[9] + assert alice_identity["github_repo"] in set_id_output[10] + assert alice_identity["image"] in set_id_output[11] + assert alice_identity["discord"] in set_id_output[12] + assert alice_identity["description"] in set_id_output[13] + assert alice_identity["additional"] in set_id_output[14] # 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[6] + assert wallet_alice.coldkeypub.ss58_address in set_id_output[7] # Execute btcli get-identity using hotkey get_identity = exec_command_alice(