Skip to content
58 changes: 40 additions & 18 deletions bittensor_cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()

Expand Down
34 changes: 23 additions & 11 deletions bittensor_cli/src/bittensor/async_substrate_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
8 changes: 6 additions & 2 deletions bittensor_cli/src/bittensor/extrinsics/transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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

Expand Down
20 changes: 10 additions & 10 deletions bittensor_cli/src/bittensor/subtensor_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions bittensor_cli/src/bittensor/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]:
Expand Down
39 changes: 38 additions & 1 deletion tests/e2e_tests/test_wallet_interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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",
Expand All @@ -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",
Expand Down