diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py index b3cc16ff7..5008a03c5 100755 --- a/bittensor_cli/cli.py +++ b/bittensor_cli/cli.py @@ -816,30 +816,35 @@ def initialize_chain( self.subtensor = SubtensorInterface(defaults.subtensor.network) return self.subtensor - def _run_command(self, cmd: Coroutine) -> None: + def _run_command(self, cmd: Coroutine): """ Runs the supplied coroutine with `asyncio.run` """ async def _run(): + initiated = False try: if self.subtensor: async with self.subtensor: + initiated = True result = await cmd else: + initiated = True result = await cmd return result except (ConnectionRefusedError, ssl.SSLError): err_console.print(f"Unable to connect to the chain: {self.subtensor}") - asyncio.create_task(cmd).cancel() - raise typer.Exit() - except ConnectionClosed: - asyncio.create_task(cmd).cancel() - raise typer.Exit() - except SubstrateRequestException as e: - err_console.print(str(e)) - asyncio.create_task(cmd).cancel() - raise typer.Exit() + except ( + ConnectionClosed, + SubstrateRequestException, + KeyboardInterrupt, + ) as e: + if isinstance(e, SubstrateRequestException): + err_console.print(str(e)) + finally: + if initiated is False: + asyncio.create_task(cmd).cancel() + raise typer.Exit() if sys.version_info < (3, 10): # For Python 3.9 or lower @@ -857,15 +862,32 @@ def main_callback( """ Command line interface (CLI) for Bittensor. Uses the values in the configuration file. These values can be overriden by passing them explicitly in the command line. """ - # create config file if it does not exist - if not os.path.exists(self.config_path): + # Load or create the config file + if os.path.exists(self.config_path): + with open(self.config_path, "r") as f: + config = safe_load(f) + else: directory_path = Path(self.config_base_path) directory_path.mkdir(exist_ok=True, parents=True) - with open(self.config_path, "w+") as f: - safe_dump(defaults.config.dictionary, f) - # check config - with open(self.config_path, "r") as f: - config = safe_load(f) + config = defaults.config.dictionary.copy() + with open(self.config_path, "w") as f: + safe_dump(config, f) + + # Update missing values + updated = False + for key, value in defaults.config.dictionary.items(): + if key not in config: + config[key] = value + updated = True + elif isinstance(value, dict): + for sub_key, sub_value in value.items(): + if sub_key not in config[key]: + config[key][sub_key] = sub_value + updated = True + if updated: + with open(self.config_path, "w") as f: + safe_dump(config, f) + for k, v in config.items(): if k in self.config.keys(): self.config[k] = v @@ -3459,7 +3481,7 @@ def stake_remove( if not unstake_all and not amount and not keep_stake: amount = FloatPrompt.ask("[blue bold]Amount to unstake (TAO ฯ„)[/blue bold]") - if unstake_all and not amount: + if unstake_all and not amount and prompt: if not Confirm.ask("Unstake all staked TAO tokens?", default=False): raise typer.Exit() diff --git a/bittensor_cli/src/bittensor/async_substrate_interface.py b/bittensor_cli/src/bittensor/async_substrate_interface.py index 60ec9dce9..bd6bbb987 100644 --- a/bittensor_cli/src/bittensor/async_substrate_interface.py +++ b/bittensor_cli/src/bittensor/async_substrate_interface.py @@ -460,6 +460,9 @@ def __init__(self, chain, runtime_config, metadata, type_registry): self.runtime_config = runtime_config self.metadata = metadata + def __str__(self): + return f"Runtime: {self.chain} | {self.config}" + @property def implements_scaleinfo(self) -> bool: """ @@ -897,9 +900,10 @@ async def init_runtime( async def get_runtime(block_hash, block_id) -> Runtime: # Check if runtime state already set to current block - if (block_hash and block_hash == self.last_block_hash) or ( - block_id and block_id == self.block_id - ): + if ( + (block_hash and block_hash == self.last_block_hash) + or (block_id and block_id == self.block_id) + ) and self.metadata is not None: return Runtime( self.chain, self.runtime_config, @@ -945,9 +949,11 @@ async def get_runtime(block_hash, block_id) -> Runtime: raise SubstrateRequestException( f"No runtime information for block '{block_hash}'" ) - # Check if runtime state already set to current block - if runtime_info.get("specVersion") == self.runtime_version: + if ( + runtime_info.get("specVersion") == self.runtime_version + and self.metadata is not None + ): return Runtime( self.chain, self.runtime_config, @@ -962,16 +968,19 @@ async def get_runtime(block_hash, block_id) -> Runtime: if self.runtime_version in self.__metadata_cache: # Get metadata from cache # self.debug_message('Retrieved metadata for {} from memory'.format(self.runtime_version)) - self.metadata = self.__metadata_cache[self.runtime_version] + metadata = self.metadata = self.__metadata_cache[ + self.runtime_version + ] else: - self.metadata = await self.get_block_metadata( + metadata = self.metadata = await self.get_block_metadata( block_hash=runtime_block_hash, decode=True ) # self.debug_message('Retrieved metadata for {} from Substrate node'.format(self.runtime_version)) # Update metadata cache self.__metadata_cache[self.runtime_version] = self.metadata - + else: + metadata = self.metadata # Update type registry self.reload_type_registry(use_remote_preset=False, auto_discover=True) @@ -1012,7 +1021,10 @@ async def get_runtime(block_hash, block_id) -> Runtime: if block_id and block_hash: raise ValueError("Cannot provide block_hash and block_id at the same time") - if not (runtime := self.runtime_cache.retrieve(block_id, block_hash)): + if ( + not (runtime := self.runtime_cache.retrieve(block_id, block_hash)) + or runtime.metadata is None + ): runtime = await get_runtime(block_hash, block_id) self.runtime_cache.add_item(block_id, block_hash, runtime) return runtime @@ -1123,7 +1135,7 @@ async def create_storage_key( ------- StorageKey """ - await self.init_runtime(block_hash=block_hash) + runtime = await self.init_runtime(block_hash=block_hash) return StorageKey.create_from_storage_function( pallet, @@ -2272,7 +2284,7 @@ async def get_metadata_constant(self, module_name, constant_name, block_hash=Non MetadataModuleConstants """ - # await self.init_runtime(block_hash=block_hash) + await self.init_runtime(block_hash=block_hash) for module in self.metadata.pallets: if module_name == module.name and module.constants: diff --git a/bittensor_cli/src/bittensor/extrinsics/transfer.py b/bittensor_cli/src/bittensor/extrinsics/transfer.py index d5311c068..eada5ec70 100644 --- a/bittensor_cli/src/bittensor/extrinsics/transfer.py +++ b/bittensor_cli/src/bittensor/extrinsics/transfer.py @@ -101,7 +101,11 @@ async def do_transfer() -> tuple[bool, str, str]: block_hash_ = response.block_hash return True, block_hash_, "" else: - return False, "", format_error_message(await response.error_message) + return ( + False, + "", + format_error_message(await response.error_message, subtensor.substrate), + ) # Validate destination address. if not is_valid_bittensor_address_or_public_key(destination): @@ -193,7 +197,7 @@ async def do_transfer() -> tuple[bool, str, str]: ) console.print( f"Balance:\n" - f" [blue]{account_balance}[/blue] :arrow_right: [green]{new_balance[wallet.coldkey.ss58_address]}[/green]" + f" [blue]{account_balance}[/blue] :arrow_right: [green]{new_balance[wallet.coldkeypub.ss58_address]}[/green]" ) return True diff --git a/bittensor_cli/src/bittensor/subtensor_interface.py b/bittensor_cli/src/bittensor/subtensor_interface.py index 62063c206..b056937aa 100644 --- a/bittensor_cli/src/bittensor/subtensor_interface.py +++ b/bittensor_cli/src/bittensor/subtensor_interface.py @@ -111,18 +111,18 @@ def __str__(self): return f"Network: {self.network}, Chain: {self.chain_endpoint}" async def __aenter__(self): - with console.status( - f"[yellow]Connecting to Substrate:[/yellow][bold white] {self}..." - ): - try: + try: + with console.status( + f"[yellow]Connecting to Substrate:[/yellow][bold white] {self}..." + ): async with self.substrate: return self - except TimeoutException: - err_console.print( - "\n[red]Error[/red]: Timeout occurred connecting to substrate. " - f"Verify your chain and network settings: {self}" - ) - raise typer.Exit(code=1) + except TimeoutException: + err_console.print( + "\n[red]Error[/red]: Timeout occurred connecting to substrate. " + f"Verify your chain and network settings: {self}" + ) + raise typer.Exit(code=1) async def __aexit__(self, exc_type, exc_val, exc_tb): await self.substrate.close() diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py index e63807a83..b6b05baae 100644 --- a/bittensor_cli/src/bittensor/utils.py +++ b/bittensor_cli/src/bittensor/utils.py @@ -443,7 +443,7 @@ def get_explorer_url_for_network( explorer_opentensor_url = "{root_url}/query/{block_hash}".format( root_url=explorer_root_urls.get("opentensor"), block_hash=block_hash ) - explorer_taostats_url = "{root_url}/extrinsic/{block_hash}".format( + explorer_taostats_url = "{root_url}/hash/{block_hash}".format( root_url=explorer_root_urls.get("taostats"), block_hash=block_hash ) explorer_urls["opentensor"] = explorer_opentensor_url @@ -532,7 +532,7 @@ def format_error_message( err_docs = error_message.get("docs", [err_description]) err_description = err_docs[0] if err_docs else err_description - return f"Subtensor returned `{err_name}({err_type})` error. This means: `{err_description}`." + return f"Subtensor returned `{err_name}({err_type})` error. This means: '{err_description}'." def convert_blocks_to_time(blocks: int, block_time: int = 12) -> tuple[int, int, int]: diff --git a/tests/e2e_tests/test_wallet_interactions.py b/tests/e2e_tests/test_wallet_interactions.py index 8641f3698..c43efb90b 100644 --- a/tests/e2e_tests/test_wallet_interactions.py +++ b/tests/e2e_tests/test_wallet_interactions.py @@ -353,6 +353,7 @@ def test_wallet_identities(local_chain, wallet_setup): """ print("Testing wallet set-id, get-id, sign command ๐Ÿงช") + netuid = 1 wallet_path_alice = "//Alice" # Create wallet for Alice @@ -361,7 +362,7 @@ def test_wallet_identities(local_chain, wallet_setup): ) # Register Alice to the root network (0) - # Either root list neurons can set-id or subnet owners + # Either root list neurons + subnet registered can set-id or subnet owners root_register = exec_command_alice( command="root", sub_command="register", @@ -379,6 +380,42 @@ def test_wallet_identities(local_chain, wallet_setup): ) assert "โœ… Registered" in root_register.stdout + # Register a subnet with sudo as Alice + result = exec_command_alice( + command="subnets", + sub_command="create", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--chain", + "ws://127.0.0.1:9945", + "--wallet-name", + wallet_alice.name, + "--no-prompt", + ], + ) + assert f"โœ… Registered subnetwork with netuid: {netuid}" in result.stdout + + # Register Alice in netuid = 1 using her hotkey + register_subnet = exec_command_alice( + command="subnets", + sub_command="register", + extra_args=[ + "--wallet-path", + wallet_path_alice, + "--wallet-name", + wallet_alice.name, + "--hotkey", + wallet_alice.hotkey_str, + "--netuid", + netuid, + "--chain", + "ws://127.0.0.1:9945", + "--no-prompt", + ], + ) + assert "โœ… Registered" in register_subnet.stdout + # Define values for Alice's identity alice_identity = { "display_name": "Alice",