From 04601d244f08b4d9a04231236dce5ce7c5cf3922 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 18 Mar 2025 15:57:03 -0700 Subject: [PATCH 01/12] add get_scheduled_coldkey_swap --- .../src/bittensor/subtensor_interface.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index dba786369..e47cfd563 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1501,3 +1501,28 @@ async def get_stake_fee( ) return Balance.from_rao(result) + + async def get_scheduled_coldkey_swap( + 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. + + :param block_hash: Block hash at which to perform query. + :param 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. + """ + result = await self.substrate.query_map( + module="SubtensorModule", + storage_function="ColdkeySwapScheduled", + 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 From 30508d46a0d6bea566aeca3f15c043953482ffb2 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 18 Mar 2025 15:58:02 -0700 Subject: [PATCH 02/12] Add schedule_coldkey_swap --- bittensor_cli/src/commands/wallets.py | 72 +++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 82b1253ac..6285402e4 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -14,6 +14,7 @@ from rich.table import Column, Table from rich.tree import Tree from rich.padding import Padding +from rich.prompt import Confirm from bittensor_cli.src import COLOR_PALETTE from bittensor_cli.src.bittensor import utils @@ -1472,3 +1473,74 @@ async def sign(wallet: Wallet, message: str, use_hotkey: str): signed_message = keypair.sign(message.encode("utf-8")).hex() console.print("[dark_sea_green3]Message signed successfully:") console.print(signed_message) + + +async def schedule_coldkey_swap( + wallet: Wallet, + subtensor: SubtensorInterface, + new_coldkey_ss58: str, + force_swap: bool = False, +) -> bool: + """Schedules a coldkey swap operation to be executed at a future block. + + 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. + Returns: + bool: True if the swap was scheduled successfully, 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: + return False + else: + console.print( + "[yellow]Continuing with the swap due to force_swap flag.[/yellow]\n" + ) + + prompt = ( + "You are [red]swapping[/red] your [blue]coldkey[/blue] to a new address.\n" + f"Current ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" + f"New ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{new_coldkey_ss58}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" + "Are you sure you want to continue?" + ) + if not Confirm.ask(prompt): + return False + + if not unlock_key(wallet).success: + return False + + call = await subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="schedule_swap_coldkey", + call_params={ + "new_coldkey": new_coldkey_ss58, + }, + ) + + with console.status(":satellite: Scheduling coldkey swap on-chain..."): + success, err_msg = await subtensor.sign_and_send_extrinsic( + call, + wallet, + wait_for_inclusion=True, + wait_for_finalization=True, + ) + + if not success: + print_error(f"Failed to schedule coldkey swap: {err_msg}") + return False + + console.print( + ":white_heavy_check_mark: [green]Successfully scheduled coldkey swap" + ) + return True From dc9e692421716bf3cacc487696445f84a8966f14 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Tue, 18 Mar 2025 15:59:34 -0700 Subject: [PATCH 03/12] Adds cli entry --- bittensor_cli/cli.py | 88 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 68d95db4c..8bb7691dc 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -684,6 +684,9 @@ def __init__(self): self.wallet_app.command( "swap-hotkey", rich_help_panel=HELP_PANELS["WALLET"]["SECURITY"] )(self.wallet_swap_hotkey) + self.wallet_app.command( + "swap-coldkey", rich_help_panel=HELP_PANELS["WALLET"]["SECURITY"] + )(self.wallet_swap_coldkey) self.wallet_app.command( "regen-coldkey", rich_help_panel=HELP_PANELS["WALLET"]["SECURITY"] )(self.wallet_regen_coldkey) @@ -2762,6 +2765,91 @@ def wallet_sign( return self._run_command(wallets.sign(wallet, message, use_hotkey)) + def wallet_swap_coldkey( + self, + wallet_name: Optional[str] = Options.wallet_name, + wallet_path: Optional[str] = Options.wallet_path, + wallet_hotkey: Optional[str] = Options.wallet_hotkey, + new_wallet_or_ss58: Optional[str] = typer.Option( + None, + "--new-coldkey", + "--new-coldkey-ss58", + "--new-wallet", + "--new", + help="SS58 address of the new coldkey that will replace the current one.", + ), + network: Optional[list[str]] = Options.network, + 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.", + ), + ): + """ + Schedule a coldkey swap for a wallet. + + This command allows you to schedule a coldkey swap for a wallet. You can either provide a new wallet name, or SS58 address. + + EXAMPLES + + [green]$[/green] btcli wallet schedule-coldkey-swap --new-wallet my_new_wallet + + [green]$[/green] btcli wallet schedule-coldkey-swap --new-coldkey-ss58 5Dk...X3q + """ + self.verbosity_handler(quiet, verbose) + + if not wallet_name: + wallet_name = Prompt.ask( + "Enter the [blue]wallet name[/blue] which you want to swap the coldkey for", + default=self.config.get("wallet_name") or defaults.wallet.name, + ) + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME], + 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" + ) + + if not new_wallet_or_ss58: + new_wallet_or_ss58 = Prompt.ask( + "Enter the [blue]new wallet name[/blue] or [blue]SS58 address[/blue] of the new coldkey", + ) + + if is_valid_ss58_address(new_wallet_or_ss58): + new_wallet_coldkey_ss58 = new_wallet_or_ss58 + else: + new_wallet_name = new_wallet_or_ss58 + new_wallet = self.wallet_ask( + new_wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME], + 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" + ) + new_wallet_coldkey_ss58 = new_wallet.coldkeypub.ss58_address + + 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, + ) + ) + def stake_list( self, network: Optional[list[str]] = Options.network, From 89b83d21d027c317411c337558c2f8e2ff3864bc Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 19 Mar 2025 15:45:39 -0700 Subject: [PATCH 04/12] Adds block calculation --- bittensor_cli/src/commands/wallets.py | 77 +++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 6285402e4..4883fb950 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -16,7 +16,7 @@ from rich.padding import Padding from rich.prompt import Confirm -from bittensor_cli.src import COLOR_PALETTE +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 ( @@ -1510,8 +1510,8 @@ async def schedule_coldkey_swap( prompt = ( "You are [red]swapping[/red] your [blue]coldkey[/blue] to a new address.\n" - f"Current ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n" - f"New ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{new_coldkey_ss58}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\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?" ) if not Confirm.ask(prompt): @@ -1520,12 +1520,15 @@ async def schedule_coldkey_swap( if not unlock_key(wallet).success: return False - call = await subtensor.substrate.compose_call( - call_module="SubtensorModule", - call_function="schedule_swap_coldkey", - call_params={ - "new_coldkey": new_coldkey_ss58, - }, + block_pre_call, call = await asyncio.gather( + subtensor.substrate.get_block_number(), + subtensor.substrate.compose_call( + call_module="SubtensorModule", + call_function="schedule_swap_coldkey", + call_params={ + "new_coldkey": new_coldkey_ss58, + }, + ), ) with console.status(":satellite: Scheduling coldkey swap on-chain..."): @@ -1535,6 +1538,7 @@ async def schedule_coldkey_swap( wait_for_inclusion=True, wait_for_finalization=True, ) + block_post_call = await subtensor.substrate.get_block_number() if not success: print_error(f"Failed to schedule coldkey swap: {err_msg}") @@ -1543,4 +1547,59 @@ async def schedule_coldkey_swap( console.print( ":white_heavy_check_mark: [green]Successfully scheduled coldkey swap" ) + + block_num, dest_coldkey = await find_coldkey_swap_extrinsic( + subtensor=subtensor, + start_block=block_pre_call, + end_block=block_post_call, + wallet_ss58=wallet.coldkeypub.ss58_address, + ) + + if block_num is not None: + console.print( + f"\n[green]Coldkey swap details:[/green]" + f"\nBlock number: {block_num}" + f"\nOriginal address: [{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]" + f"\nDestination address: [{COLORS.G.CK}]{dest_coldkey}[/{COLORS.G.CK}]" + f"\n\nYou can provide this block number to `btcli wallet swap check`" + ) + else: + console.print( + "[yellow]Warning: Could not find the swap extrinsic in recent blocks" + ) return True + + +async def find_coldkey_swap_extrinsic( + subtensor: SubtensorInterface, + start_block: int, + end_block: int, + wallet_ss58: str, +) -> tuple[Optional[int], Optional[str]]: + """Search for a coldkey swap extrinsic in a range of blocks. + + 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 + + Returns: + tuple[Optional[int], Optional[str]]: + (block number, destination coldkey ss58) if found, + (None, None) if not found + """ + for block_num in range(start_block, end_block + 1): + block_data = await subtensor.substrate.get_block(block_number=block_num) + for extrinsic in block_data["extrinsics"]: + extrinsic_data = extrinsic.value + if ( + "call" in extrinsic_data + and extrinsic_data["call"].get("call_function") + == "schedule_swap_coldkey" + and extrinsic_data.get("address") == wallet_ss58 + ): + new_coldkey_ss58 = extrinsic_data["call"]["call_args"][0]["value"] + return block_num, new_coldkey_ss58 + + return None, None From f3826b113e0ad432304cf3385b6bdbb5a4bd54ec Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 19 Mar 2025 15:59:53 -0700 Subject: [PATCH 05/12] Adds duration --- bittensor_cli/src/commands/wallets.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 4883fb950..314dc0b44 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -45,6 +45,7 @@ millify_tao, unlock_key, WalletLike, + blocks_to_duration, ) @@ -1548,11 +1549,14 @@ async def schedule_coldkey_swap( ":white_heavy_check_mark: [green]Successfully scheduled coldkey swap" ) - block_num, dest_coldkey = await find_coldkey_swap_extrinsic( - subtensor=subtensor, - start_block=block_pre_call, - end_block=block_post_call, - wallet_ss58=wallet.coldkeypub.ss58_address, + (block_num, dest_coldkey), schedule_duration = await asyncio.gather( + find_coldkey_swap_extrinsic( + subtensor=subtensor, + start_block=block_pre_call, + end_block=block_post_call, + wallet_ss58=wallet.coldkeypub.ss58_address, + ), + subtensor.get_coldkey_swap_schedule_duration() ) if block_num is not None: @@ -1561,7 +1565,8 @@ async def schedule_coldkey_swap( f"\nBlock number: {block_num}" f"\nOriginal address: [{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]" f"\nDestination address: [{COLORS.G.CK}]{dest_coldkey}[/{COLORS.G.CK}]" - f"\n\nYou can provide this block number to `btcli wallet swap check`" + f"\nThe swap will be completed in [green]{blocks_to_duration(schedule_duration)} (Block: {block_num+schedule_duration})[/green] from now." + f"\n[dim]You can provide the block number to `btcli wallet swap-check`[/dim]" ) else: console.print( From 7ab8b5ea99dac61846faa8626ef2d8788e5a7ae2 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 19 Mar 2025 16:00:33 -0700 Subject: [PATCH 06/12] Adds get_coldkey_swap_schedule_duration call --- .../src/bittensor/subtensor_interface.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index e47cfd563..a19dc8b85 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -1526,3 +1526,28 @@ async def get_scheduled_coldkey_swap( async for ss58, _ in result: keys_pending_swap.append(decode_account_id(ss58)) return keys_pending_swap + + async def get_coldkey_swap_schedule_duration( + self, + block_hash: Optional[str] = None, + reuse_block: bool = False, + ) -> int: + """ + Retrieves the duration (in blocks) required for a coldkey swap to be executed. + + 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. + """ + result = await self.query( + module="SubtensorModule", + storage_function="ColdkeySwapScheduleDuration", + params=[], + block_hash=block_hash, + reuse_block_hash=reuse_block, + ) + + return result From 6b794ca9f74f7e05c38eaa9bf960cf7769479383 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 19 Mar 2025 17:28:32 -0700 Subject: [PATCH 07/12] Updates wallet_check_ck_swap --- bittensor_cli/cli.py | 98 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 91 insertions(+), 7 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 8bb7691dc..38aeb1abd 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -109,6 +109,17 @@ class Options: "--wallet.hotkey", help="Hotkey of the wallet", ) + wallet_ss58_address = typer.Option( + None, + "--wallet-name", + "--name", + "--wallet_name", + "--wallet.name", + "--address", + "--ss58", + "--ss58-address", + help="SS58 address or wallet name to check. Leave empty to be prompted.", + ) wallet_hotkey_ss58 = typer.Option( None, "--hotkey", @@ -687,6 +698,9 @@ def __init__(self): self.wallet_app.command( "swap-coldkey", rich_help_panel=HELP_PANELS["WALLET"]["SECURITY"] )(self.wallet_swap_coldkey) + self.wallet_app.command( + "swap-check", rich_help_panel=HELP_PANELS["WALLET"]["SECURITY"] + )(self.wallet_check_ck_swap) self.wallet_app.command( "regen-coldkey", rich_help_panel=HELP_PANELS["WALLET"]["SECURITY"] )(self.wallet_regen_coldkey) @@ -2309,28 +2323,98 @@ def wallet_new_coldkey( def wallet_check_ck_swap( self, - wallet_name: Optional[str] = Options.wallet_name, + 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", + ), network: Optional[list[str]] = Options.network, quiet: bool = Options.quiet, verbose: bool = Options.verbose, ): """ - Check the status of your scheduled coldkey swap. + Check the status of scheduled coldkey swaps. USAGE - Users should provide the old coldkey wallet to check the swap status. + 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) - EXAMPLE + EXAMPLES + + Show all pending swaps: + [green]$[/green] btcli wallet swap-check --all - [green]$[/green] btcli wallet check_coldkey_swap + Check specific wallet's swap: + [green]$[/green] btcli wallet swap-check --wallet.name my_wallet + + Check swap using SS58 address: + [green]$[/green] btcli wallet swap-check --ss58-address 5DkQ4... + + Check swap details with block number: + [green]$[/green] btcli wallet swap-check --wallet.name my_wallet --block 12345 """ self.verbosity_handler(quiet, verbose) - wallet = self.wallet_ask(wallet_name, wallet_path, wallet_hotkey) self.initialize_chain(network) - return self._run_command(wallets.check_coldkey_swap(wallet, self.subtensor)) + + if not wallet_ss58_address: + wallet_or_ss58_address = Prompt.ask( + "Enter the [blue]SS58 address[/blue] or the [blue]wallet name[/blue]. [dim]Leave blank to check all pending swaps[/dim]" + ) + if not wallet_or_ss58_address: + show_all = True + else: + if is_valid_ss58_address(wallet_or_ss58_address): + return self._run_command( + wallets.check_swap_status( + self.subtensor, wallet_or_ss58_address, scheduled_block + ) + ) + else: + wallet_name = wallet_or_ss58_address + + if show_all: + return self._run_command( + wallets.check_swap_status(self.subtensor, None, None) + ) + + wallet = self.wallet_ask( + wallet_name, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + + 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: + console.print( + "[red]Invalid block number. Skipping block check.[/red]" + ) + + return self._run_command( + wallets.check_swap_status( + self.subtensor, wallet.coldkeypub.ss58_address, scheduled_block + ) + ) def wallet_create_wallet( self, From 3b1a9c450a04699403931dc57174e327c80396bc Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Wed, 19 Mar 2025 17:28:56 -0700 Subject: [PATCH 08/12] Updates wallet check swap --- bittensor_cli/src/commands/wallets.py | 107 +++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 314dc0b44..ca8bb301f 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1556,7 +1556,7 @@ async def schedule_coldkey_swap( end_block=block_post_call, wallet_ss58=wallet.coldkeypub.ss58_address, ), - subtensor.get_coldkey_swap_schedule_duration() + subtensor.get_coldkey_swap_schedule_duration(), ) if block_num is not None: @@ -1608,3 +1608,108 @@ async def find_coldkey_swap_extrinsic( return block_num, new_coldkey_ss58 return None, None + + +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. + + Args: + subtensor: Connection to the network + origin_ss58: The SS58 address of the original coldkey + block_number: Optional block number where the swap was scheduled + """ + scheduled_swaps = await subtensor.get_scheduled_coldkey_swap() + + if not origin_ss58: + 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, + ) + + 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[/dim]" + ) + return + + is_pending = origin_ss58 in scheduled_swaps + + if not is_pending: + console.print( + f"[red]No pending swap found for coldkey:[/red] [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]" + ) + return + + console.print( + f"[green]Found pending swap for coldkey:[/green] [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]" + ) + + if expected_block_number is None: + return + + # Find the swap extrinsic details + block_num, dest_coldkey = await find_coldkey_swap_extrinsic( + subtensor=subtensor, + start_block=expected_block_number, + end_block=expected_block_number, + wallet_ss58=origin_ss58, + ) + + if block_num is None: + console.print( + f"[yellow]Warning: Could not find swap extrinsic at block {expected_block_number}[/yellow]" + ) + return + + current_block, schedule_duration = await asyncio.gather( + subtensor.substrate.get_block_number(), + subtensor.get_coldkey_swap_schedule_duration(), + ) + + completion_block = block_num + schedule_duration + remaining_blocks = completion_block - current_block + + if remaining_blocks <= 0: + console.print("[green]Swap period has completed![/green]") + return + + console.print( + "\n[green]Coldkey swap details:[/green]" + f"\nScheduled at block: {block_num}" + f"\nOriginal address: [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]" + f"\nDestination address: [{COLORS.G.CK}]{dest_coldkey}[/{COLORS.G.CK}]" + f"\nCompletion block: {completion_block}" + f"\nTime remaining: {blocks_to_duration(remaining_blocks)}" + ) From f03f2afcc5e8bf93529dc01a204e0f4e96c73ceb Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 20 Mar 2025 13:36:27 -0700 Subject: [PATCH 09/12] Improves how we fetch events --- bittensor_cli/src/commands/wallets.py | 113 ++++++++++++++------------ 1 file changed, 59 insertions(+), 54 deletions(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index ca8bb301f..2206356a0 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -46,6 +46,7 @@ unlock_key, WalletLike, blocks_to_duration, + decode_account_id, ) @@ -1549,39 +1550,36 @@ async def schedule_coldkey_swap( ":white_heavy_check_mark: [green]Successfully scheduled coldkey swap" ) - (block_num, dest_coldkey), schedule_duration = await asyncio.gather( - find_coldkey_swap_extrinsic( - subtensor=subtensor, - start_block=block_pre_call, - end_block=block_post_call, - wallet_ss58=wallet.coldkeypub.ss58_address, - ), - subtensor.get_coldkey_swap_schedule_duration(), + swap_info = await find_coldkey_swap_extrinsic( + subtensor=subtensor, + start_block=block_pre_call, + end_block=block_post_call, + wallet_ss58=wallet.coldkeypub.ss58_address, ) - if block_num is not None: - console.print( - f"\n[green]Coldkey swap details:[/green]" - f"\nBlock number: {block_num}" - f"\nOriginal address: [{COLORS.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.G.CK}]" - f"\nDestination address: [{COLORS.G.CK}]{dest_coldkey}[/{COLORS.G.CK}]" - f"\nThe swap will be completed in [green]{blocks_to_duration(schedule_duration)} (Block: {block_num+schedule_duration})[/green] from now." - f"\n[dim]You can provide the block number to `btcli wallet swap-check`[/dim]" - ) - else: + if not swap_info: console.print( "[yellow]Warning: Could not find the swap extrinsic in recent blocks" ) 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]" + ) + async def find_coldkey_swap_extrinsic( subtensor: SubtensorInterface, start_block: int, end_block: int, wallet_ss58: str, -) -> tuple[Optional[int], Optional[str]]: - """Search for a coldkey swap extrinsic in a range of blocks. +) -> dict: + """Search for a coldkey swap event in a range of blocks. Args: subtensor: SubtensorInterface for chain queries @@ -1590,24 +1588,42 @@ async def find_coldkey_swap_extrinsic( wallet_ss58: SS58 address of the signing wallet Returns: - tuple[Optional[int], Optional[str]]: - (block number, destination coldkey ss58) if found, - (None, None) if not found + 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 """ - for block_num in range(start_block, end_block + 1): - block_data = await subtensor.substrate.get_block(block_number=block_num) - for extrinsic in block_data["extrinsics"]: - extrinsic_data = extrinsic.value + block_hashes = await asyncio.gather( + *[ + subtensor.substrate.get_block_hash(block_num) + for block_num in range(start_block, end_block + 1) + ] + ) + block_events = await asyncio.gather( + *[ + subtensor.substrate.get_events(block_hash=block_hash) + for block_hash in block_hashes + ] + ) + + for block_num, events in zip(range(start_block, end_block + 1), block_events): + for event in events: if ( - "call" in extrinsic_data - and extrinsic_data["call"].get("call_function") - == "schedule_swap_coldkey" - and extrinsic_data.get("address") == wallet_ss58 + event.get("event", {}).get("module_id") == "SubtensorModule" + and event.get("event", {}).get("event_id") == "ColdkeySwapScheduled" ): - new_coldkey_ss58 = extrinsic_data["call"]["call_args"][0]["value"] - return block_num, new_coldkey_ss58 + attributes = event["event"].get("attributes", {}) + old_coldkey = decode_account_id(attributes["old_coldkey"][0]) - return None, None + if old_coldkey == wallet_ss58: + return { + "block_num": block_num, + "dest_coldkey": decode_account_id(attributes["new_coldkey"][0]), + "execution_block": attributes["execution_block"], + } + + return {} async def check_swap_status( @@ -1637,10 +1653,7 @@ async def check_swap_status( style=COLOR_PALETTE["GENERAL"]["SUBHEADING_MAIN"], no_wrap=True, ), - Column( - "Status", - style="dark_sea_green3" - ), + Column("Status", style="dark_sea_green3"), title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Pending Coldkey Swaps\n", show_header=True, show_edge=False, @@ -1653,10 +1666,7 @@ async def check_swap_status( ) for coldkey in scheduled_swaps: - table.add_row( - coldkey, - "Pending" - ) + table.add_row(coldkey, "Pending") console.print(table) console.print( @@ -1680,26 +1690,21 @@ async def check_swap_status( return # Find the swap extrinsic details - block_num, dest_coldkey = await find_coldkey_swap_extrinsic( + swap_info = await find_coldkey_swap_extrinsic( subtensor=subtensor, start_block=expected_block_number, end_block=expected_block_number, wallet_ss58=origin_ss58, ) - if block_num is None: + if not swap_info: console.print( f"[yellow]Warning: Could not find swap extrinsic at block {expected_block_number}[/yellow]" ) return - current_block, schedule_duration = await asyncio.gather( - subtensor.substrate.get_block_number(), - subtensor.get_coldkey_swap_schedule_duration(), - ) - - completion_block = block_num + schedule_duration - remaining_blocks = completion_block - current_block + current_block = await subtensor.substrate.get_block_number() + remaining_blocks = swap_info["execution_block"] - current_block if remaining_blocks <= 0: console.print("[green]Swap period has completed![/green]") @@ -1707,9 +1712,9 @@ async def check_swap_status( console.print( "\n[green]Coldkey swap details:[/green]" - f"\nScheduled at block: {block_num}" + f"\nScheduled at block: {swap_info['block_num']}" f"\nOriginal address: [{COLORS.G.CK}]{origin_ss58}[/{COLORS.G.CK}]" - f"\nDestination address: [{COLORS.G.CK}]{dest_coldkey}[/{COLORS.G.CK}]" - f"\nCompletion block: {completion_block}" + f"\nDestination address: [{COLORS.G.CK}]{swap_info['dest_coldkey']}[/{COLORS.G.CK}]" + f"\nCompletion block: {swap_info['execution_block']}" f"\nTime remaining: {blocks_to_duration(remaining_blocks)}" ) From 35c32da4c7dd40afe37f4fb6666a82b9b4311a10 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 20 Mar 2025 17:13:12 -0700 Subject: [PATCH 10/12] Adds archive node logic --- bittensor_cli/src/__init__.py | 4 ++++ bittensor_cli/src/commands/wallets.py | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/bittensor_cli/src/__init__.py b/bittensor_cli/src/__init__.py index 1ecd29a33..7011dfd47 100644 --- a/bittensor_cli/src/__init__.py +++ b/bittensor_cli/src/__init__.py @@ -33,6 +33,10 @@ class Constants: "latent-lite": latent_lite_entrypoint, "subvortex": subvortex_entrypoint, } + genesis_block_hash_map = { + "finney": "0x2f0555cc76fc2840a25a6ea3b9637146806f1f44b090c175ffde2a7e5ab36c03", + "test": "0x8f9cf856bf558a14440e75569c9e58594757048d7b3a84b5d25f6bd978263105", + } delegates_detail_url = "https://raw.githubusercontent.com/opentensor/bittensor-delegates/main/public/delegates.json" diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 2206356a0..2841a1395 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -16,7 +16,7 @@ from rich.padding import Padding from rich.prompt import Confirm -from bittensor_cli.src import COLOR_PALETTE, COLORS +from bittensor_cli.src import COLOR_PALETTE, COLORS, Constants from bittensor_cli.src.bittensor import utils from bittensor_cli.src.bittensor.balances import Balance from bittensor_cli.src.bittensor.chain_data import ( @@ -1594,6 +1594,19 @@ async def find_coldkey_swap_extrinsic( - execution_block: Block number when swap will execute Empty dict if not found """ + + current_block, genesis_block = await asyncio.gather( + subtensor.substrate.get_block_number(), + subtensor.substrate.get_block_hash(0) + ) + 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) From ba05ad22041f5d0dae729e7127fd2523e49fb653 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 20 Mar 2025 17:26:39 -0700 Subject: [PATCH 11/12] Finalise feat --- bittensor_cli/cli.py | 58 ++++++++++++--------------- bittensor_cli/src/commands/wallets.py | 2 +- 2 files changed, 27 insertions(+), 33 deletions(-) diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index 38aeb1abd..4b3998344 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -2357,45 +2357,42 @@ def wallet_check_ck_swap( [green]$[/green] btcli wallet swap-check --all Check specific wallet's swap: - [green]$[/green] btcli wallet swap-check --wallet.name my_wallet + [green]$[/green] btcli wallet swap-check --wallet-name my_wallet Check swap using SS58 address: - [green]$[/green] btcli wallet swap-check --ss58-address 5DkQ4... + [green]$[/green] btcli wallet swap-check --ss58 5DkQ4... Check swap details with block number: - [green]$[/green] btcli wallet swap-check --wallet.name my_wallet --block 12345 + [green]$[/green] btcli wallet swap-check --wallet-name my_wallet --block 12345 """ self.verbosity_handler(quiet, verbose) self.initialize_chain(network) - if not wallet_ss58_address: - wallet_or_ss58_address = Prompt.ask( - "Enter the [blue]SS58 address[/blue] or the [blue]wallet name[/blue]. [dim]Leave blank to check all pending swaps[/dim]" - ) - if not wallet_or_ss58_address: - show_all = True - else: - if is_valid_ss58_address(wallet_or_ss58_address): - return self._run_command( - wallets.check_swap_status( - self.subtensor, wallet_or_ss58_address, scheduled_block - ) - ) - else: - wallet_name = wallet_or_ss58_address - if show_all: return self._run_command( wallets.check_swap_status(self.subtensor, None, None) ) - wallet = self.wallet_ask( - wallet_name, - wallet_path, - wallet_hotkey, - ask_for=[WO.NAME, WO.PATH], - validate=WV.WALLET, - ) + 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]" + ) + if not wallet_ss58_address: + return self._run_command( + wallets.check_swap_status(self.subtensor, None, None) + ) + + if is_valid_ss58_address(wallet_ss58_address): + ss58_address = wallet_ss58_address + else: + wallet = self.wallet_ask( + wallet_ss58_address, + wallet_path, + wallet_hotkey, + ask_for=[WO.NAME, WO.PATH], + validate=WV.WALLET, + ) + ss58_address = wallet.coldkeypub.ss58_address if not scheduled_block: block_input = Prompt.ask( @@ -2406,14 +2403,11 @@ def wallet_check_ck_swap( try: scheduled_block = int(block_input) except ValueError: - console.print( - "[red]Invalid block number. Skipping block check.[/red]" - ) + print_error("Invalid block number") + raise typer.Exit() return self._run_command( - wallets.check_swap_status( - self.subtensor, wallet.coldkeypub.ss58_address, scheduled_block - ) + wallets.check_swap_status(self.subtensor, ss58_address, scheduled_block) ) def wallet_create_wallet( diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index 2841a1395..dbf9f5f1a 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1683,7 +1683,7 @@ async def check_swap_status( console.print(table) console.print( - "\n[dim]Tip: Check specific swap details by providing the original coldkey SS58 address[/dim]" + "\n[dim]Tip: Check specific swap details by providing the original coldkey SS58 address and the block number.[/dim]" ) return From f295a9642c493c791f8644735969dbac63f4fac4 Mon Sep 17 00:00:00 2001 From: ibraheem-opentensor Date: Thu, 20 Mar 2025 17:26:59 -0700 Subject: [PATCH 12/12] cleanup --- bittensor_cli/src/commands/wallets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py index dbf9f5f1a..61d0bcb6f 100644 --- a/bittensor_cli/src/commands/wallets.py +++ b/bittensor_cli/src/commands/wallets.py @@ -1702,7 +1702,6 @@ async def check_swap_status( if expected_block_number is None: return - # Find the swap extrinsic details swap_info = await find_coldkey_swap_extrinsic( subtensor=subtensor, start_block=expected_block_number,