bittensor-cli 9.8.7__py3-none-any.whl → 9.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bittensor_cli/cli.py CHANGED
@@ -817,6 +817,9 @@ class CLIManager:
817
817
  self.wallet_app.command(
818
818
  "sign", rich_help_panel=HELP_PANELS["WALLET"]["OPERATIONS"]
819
819
  )(self.wallet_sign)
820
+ self.wallet_app.command(
821
+ "verify", rich_help_panel=HELP_PANELS["WALLET"]["OPERATIONS"]
822
+ )(self.wallet_verify)
820
823
 
821
824
  # stake commands
822
825
  self.stake_app.command(
@@ -1889,6 +1892,12 @@ class CLIManager:
1889
1892
  transfer_all: bool = typer.Option(
1890
1893
  False, "--all", prompt=False, help="Transfer all available balance."
1891
1894
  ),
1895
+ allow_death: bool = typer.Option(
1896
+ False,
1897
+ "--allow-death",
1898
+ prompt=False,
1899
+ help="Transfer balance even if the resulting balance falls below the existential deposit.",
1900
+ ),
1892
1901
  period: int = Options.period,
1893
1902
  wallet_name: str = Options.wallet_name,
1894
1903
  wallet_path: str = Options.wallet_path,
@@ -1932,7 +1941,7 @@ class CLIManager:
1932
1941
  subtensor = self.initialize_chain(network)
1933
1942
  if transfer_all and amount:
1934
1943
  print_error("Cannot specify an amount and '--all' flag.")
1935
- raise typer.Exit()
1944
+ return False
1936
1945
  elif transfer_all:
1937
1946
  amount = 0
1938
1947
  elif not amount:
@@ -1944,6 +1953,7 @@ class CLIManager:
1944
1953
  destination=destination_ss58_address,
1945
1954
  amount=amount,
1946
1955
  transfer_all=transfer_all,
1956
+ allow_death=allow_death,
1947
1957
  era=period,
1948
1958
  prompt=prompt,
1949
1959
  json_output=json_output,
@@ -3091,6 +3101,59 @@ class CLIManager:
3091
3101
 
3092
3102
  return self._run_command(wallets.sign(wallet, message, use_hotkey, json_output))
3093
3103
 
3104
+ def wallet_verify(
3105
+ self,
3106
+ message: Optional[str] = typer.Option(
3107
+ None, "--message", "-m", help="The message that was signed"
3108
+ ),
3109
+ signature: Optional[str] = typer.Option(
3110
+ None, "--signature", "-s", help="The signature to verify (hex format)"
3111
+ ),
3112
+ public_key_or_ss58: Optional[str] = typer.Option(
3113
+ None,
3114
+ "--address",
3115
+ "-a",
3116
+ "--public-key",
3117
+ "-p",
3118
+ help="SS58 address or public key (hex) of the signer",
3119
+ ),
3120
+ quiet: bool = Options.quiet,
3121
+ verbose: bool = Options.verbose,
3122
+ json_output: bool = Options.json_output,
3123
+ ):
3124
+ """
3125
+ Verify a message signature using the signer's public key or SS58 address.
3126
+
3127
+ This command allows you to verify that a message was signed by the owner of a specific address.
3128
+
3129
+ USAGE
3130
+
3131
+ Provide the original message, the signature (in hex format), and either the SS58 address
3132
+ or public key of the signer to verify the signature.
3133
+
3134
+ EXAMPLES
3135
+
3136
+ [green]$[/green] btcli wallet verify --message "Hello world" --signature "0xabc123..." --address "5GrwvaEF..."
3137
+
3138
+ [green]$[/green] btcli wallet verify -m "Test message" -s "0xdef456..." -p "0x1234abcd..."
3139
+ """
3140
+ self.verbosity_handler(quiet, verbose, json_output)
3141
+
3142
+ if not public_key_or_ss58:
3143
+ public_key_or_ss58 = Prompt.ask(
3144
+ "Enter the [blue]address[/blue] (SS58 or hex format)"
3145
+ )
3146
+
3147
+ if not message:
3148
+ message = Prompt.ask("Enter the [blue]message[/blue]")
3149
+
3150
+ if not signature:
3151
+ signature = Prompt.ask("Enter the [blue]signature[/blue]")
3152
+
3153
+ return self._run_command(
3154
+ wallets.verify(message, signature, public_key_or_ss58, json_output)
3155
+ )
3156
+
3094
3157
  def wallet_swap_coldkey(
3095
3158
  self,
3096
3159
  wallet_name: Optional[str] = Options.wallet_name,
@@ -26,9 +26,9 @@ async def transfer_extrinsic(
26
26
  amount: Balance,
27
27
  era: int = 3,
28
28
  transfer_all: bool = False,
29
+ allow_death: bool = False,
29
30
  wait_for_inclusion: bool = True,
30
31
  wait_for_finalization: bool = False,
31
- keep_alive: bool = True,
32
32
  prompt: bool = False,
33
33
  ) -> bool:
34
34
  """Transfers funds from this wallet to the destination public key address.
@@ -39,11 +39,11 @@ async def transfer_extrinsic(
39
39
  :param amount: Amount to stake as Bittensor balance.
40
40
  :param era: Length (in blocks) for which the transaction should be valid.
41
41
  :param transfer_all: Whether to transfer all funds from this wallet to the destination address.
42
+ :param allow_death: Whether to allow for falling below the existential deposit when performing this transfer.
42
43
  :param wait_for_inclusion: If set, waits for the extrinsic to enter a block before returning `True`,
43
44
  or returns `False` if the extrinsic fails to enter the block within the timeout.
44
45
  :param wait_for_finalization: If set, waits for the extrinsic to be finalized on the chain before returning
45
46
  `True`, or returns `False` if the extrinsic fails to be finalized within the timeout.
46
- :param keep_alive: If set, keeps the account alive by keeping the balance above the existential deposit.
47
47
  :param prompt: If `True`, the call waits for confirmation from the user before proceeding.
48
48
  :return: success: Flag is `True` if extrinsic was finalized or included in the block. If we did not wait for
49
49
  finalization / inclusion, the response is `True`, regardless of its inclusion.
@@ -57,8 +57,8 @@ async def transfer_extrinsic(
57
57
  """
58
58
  call = await subtensor.substrate.compose_call(
59
59
  call_module="Balances",
60
- call_function="transfer_keep_alive",
61
- call_params={"dest": destination, "value": amount.rao},
60
+ call_function=call_function,
61
+ call_params=call_params,
62
62
  )
63
63
 
64
64
  try:
@@ -82,8 +82,8 @@ async def transfer_extrinsic(
82
82
  """
83
83
  call = await subtensor.substrate.compose_call(
84
84
  call_module="Balances",
85
- call_function="transfer_keep_alive",
86
- call_params={"dest": destination, "value": amount.rao},
85
+ call_function=call_function,
86
+ call_params=call_params,
87
87
  )
88
88
  extrinsic = await subtensor.substrate.create_signed_extrinsic(
89
89
  call=call, keypair=wallet.coldkey, era={"period": era}
@@ -115,6 +115,20 @@ async def transfer_extrinsic(
115
115
  if not unlock_key(wallet).success:
116
116
  return False
117
117
 
118
+ call_params = {"dest": destination}
119
+ if transfer_all:
120
+ call_function = "transfer_all"
121
+ if allow_death:
122
+ call_params["keep_alive"] = False
123
+ else:
124
+ call_params["keep_alive"] = True
125
+ else:
126
+ call_params["value"] = amount.rao
127
+ if allow_death:
128
+ call_function = "transfer_allow_death"
129
+ else:
130
+ call_function = "transfer_keep_alive"
131
+
118
132
  # Check balance.
119
133
  with console.status(
120
134
  f":satellite: Checking balance and fees on chain [white]{subtensor.network}[/white]",
@@ -131,23 +145,26 @@ async def transfer_extrinsic(
131
145
  )
132
146
  fee = await get_transfer_fee()
133
147
 
134
- if not keep_alive:
135
- # Check if the transfer should keep_alive the account
148
+ if allow_death:
149
+ # Check if the transfer should keep alive the account
136
150
  existential_deposit = Balance(0)
137
151
 
138
- # Check if we have enough balance.
139
- if transfer_all is True:
140
- amount = account_balance - fee - existential_deposit
141
- if amount < Balance(0):
142
- print_error("Not enough balance to transfer")
143
- return False
144
-
145
- if account_balance < (amount + fee + existential_deposit):
152
+ if account_balance < (amount + fee + existential_deposit) and not allow_death:
146
153
  err_console.print(
147
154
  ":cross_mark: [bold red]Not enough balance[/bold red]:\n\n"
148
155
  f" balance: [bright_cyan]{account_balance}[/bright_cyan]\n"
149
156
  f" amount: [bright_cyan]{amount}[/bright_cyan]\n"
150
- f" for fee: [bright_cyan]{fee}[/bright_cyan]"
157
+ f" for fee: [bright_cyan]{fee}[/bright_cyan]\n"
158
+ f" would bring you under the existential deposit: [bright_cyan]{existential_deposit}[/bright_cyan].\n"
159
+ f"You can try again with `--allow-death`."
160
+ )
161
+ return False
162
+ elif account_balance < (amount + fee) and allow_death:
163
+ print_error(
164
+ ":cross_mark: [bold red]Not enough balance[/bold red]:\n\n"
165
+ f" balance: [bright_red]{account_balance}[/bright_red]\n"
166
+ f" amount: [bright_red]{amount}[/bright_red]\n"
167
+ f" for fee: [bright_red]{fee}[/bright_red]"
151
168
  )
152
169
  return False
153
170
 
@@ -3,7 +3,9 @@ import os
3
3
  from typing import Optional, Any, Union, TypedDict, Iterable
4
4
 
5
5
  import aiohttp
6
+ from async_substrate_interface.utils.storage import StorageKey
6
7
  from bittensor_wallet import Wallet
8
+ from bittensor_wallet.bittensor_wallet import Keypair
7
9
  from bittensor_wallet.utils import SS58_FORMAT
8
10
  from scalecodec import GenericCall
9
11
  from async_substrate_interface.errors import SubstrateRequestException
@@ -881,9 +883,10 @@ class SubtensorInterface:
881
883
  storage_function="IdentitiesV2",
882
884
  block_hash=block_hash,
883
885
  reuse_block_hash=reuse_block,
886
+ fully_exhaust=True,
884
887
  )
885
888
  all_identities = {}
886
- async for ss58_address, identity in identities:
889
+ for ss58_address, identity in identities.records:
887
890
  all_identities[decode_account_id(ss58_address[0])] = decode_hex_identity(
888
891
  identity.value
889
892
  )
@@ -939,22 +942,22 @@ class SubtensorInterface:
939
942
  :param reuse_block: Whether to reuse the last-used blockchain block hash.
940
943
  :return: Dict with 'coldkeys' and 'hotkeys' as keys.
941
944
  """
942
-
943
- coldkey_identities = await self.query_all_identities()
945
+ if block_hash is None:
946
+ block_hash = await self.substrate.get_chain_head()
947
+ coldkey_identities = await self.query_all_identities(block_hash=block_hash)
944
948
  identities = {"coldkeys": {}, "hotkeys": {}}
945
- if not coldkey_identities:
946
- return identities
947
- query = await self.substrate.query_multiple( # TODO probably more efficient to do this with query_multi
948
- params=list(coldkey_identities.keys()),
949
- module="SubtensorModule",
950
- storage_function="OwnedHotkeys",
951
- block_hash=block_hash,
952
- reuse_block_hash=reuse_block,
953
- )
949
+ sks = [
950
+ await self.substrate.create_storage_key(
951
+ "SubtensorModule", "OwnedHotkeys", [ck], block_hash=block_hash
952
+ )
953
+ for ck in coldkey_identities.keys()
954
+ ]
955
+ query = await self.substrate.query_multi(sks, block_hash=block_hash)
954
956
 
955
- for coldkey_ss58, hotkeys in query.items():
957
+ storage_key: StorageKey
958
+ for storage_key, hotkeys in query:
959
+ coldkey_ss58 = storage_key.params[0]
956
960
  coldkey_identity = coldkey_identities.get(coldkey_ss58)
957
- hotkeys = [decode_account_id(hotkey[0]) for hotkey in hotkeys or []]
958
961
 
959
962
  identities["coldkeys"][coldkey_ss58] = {
960
963
  "identity": coldkey_identity,
@@ -1455,6 +1458,8 @@ class SubtensorInterface:
1455
1458
  ),
1456
1459
  self.get_subnet_price(netuid=netuid, block_hash=block_hash),
1457
1460
  )
1461
+ if not result:
1462
+ raise ValueError(f"Subnet {netuid} not found")
1458
1463
  subnet_ = DynamicInfo.from_any(result)
1459
1464
  subnet_.price = price
1460
1465
  return subnet_
@@ -1484,6 +1489,19 @@ class SubtensorInterface:
1484
1489
 
1485
1490
  return [decode_account_id(hotkey[0]) for hotkey in owned_hotkeys or []]
1486
1491
 
1492
+ async def get_extrinsic_fee(self, call: GenericCall, keypair: Keypair) -> Balance:
1493
+ """
1494
+ Determines the fee for the extrinsic call.
1495
+ Args:
1496
+ call: Created extrinsic call
1497
+ keypair: The keypair that would sign the extrinsic (usually you would just want to use the *pub for this)
1498
+
1499
+ Returns:
1500
+ Balance object representing the fee for this extrinsic.
1501
+ """
1502
+ fee_dict = await self.substrate.get_payment_info(call, keypair)
1503
+ return Balance.from_rao(fee_dict["partial_fee"])
1504
+
1487
1505
  async def get_stake_fee(
1488
1506
  self,
1489
1507
  origin_hotkey_ss58: Optional[str],
@@ -65,6 +65,45 @@ async def stake_add(
65
65
  bool: True if stake operation is successful, False otherwise
66
66
  """
67
67
 
68
+ async def get_stake_extrinsic_fee(
69
+ netuid_: int,
70
+ amount_: Balance,
71
+ staking_address_: str,
72
+ safe_staking_: bool,
73
+ price_limit: Optional[Balance] = None,
74
+ ):
75
+ """
76
+ Quick method to get the extrinsic fee for adding stake depending on the args supplied.
77
+ Args:
78
+ netuid_: The netuid where the stake will be added
79
+ amount_: the amount of stake to add
80
+ staking_address_: the hotkey ss58 to stake to
81
+ safe_staking_: whether to use safe staking
82
+ price_limit: rate with tolerance
83
+
84
+ Returns:
85
+ Balance object representing the extrinsic fee for adding stake.
86
+ """
87
+ call_fn = "add_stake" if not safe_staking_ else "add_stake_limit"
88
+ call_params = {
89
+ "hotkey": staking_address_,
90
+ "netuid": netuid_,
91
+ "amount_staked": amount_.rao,
92
+ }
93
+ if safe_staking_:
94
+ call_params.update(
95
+ {
96
+ "limit_price": price_limit,
97
+ "allow_partial": allow_partial_stake,
98
+ }
99
+ )
100
+ call = await subtensor.substrate.compose_call(
101
+ call_module="SubtensorModule",
102
+ call_function=call_fn,
103
+ call_params=call_params,
104
+ )
105
+ return await subtensor.get_extrinsic_fee(call, wallet.coldkeypub)
106
+
68
107
  async def safe_stake_extrinsic(
69
108
  netuid_: int,
70
109
  amount_: Balance,
@@ -87,7 +126,7 @@ async def stake_add(
87
126
  "hotkey": hotkey_ss58_,
88
127
  "netuid": netuid_,
89
128
  "amount_staked": amount_.rao,
90
- "limit_price": price_limit,
129
+ "limit_price": price_limit.rao,
91
130
  "allow_partial": allow_partial_stake,
92
131
  },
93
132
  ),
@@ -332,19 +371,6 @@ async def stake_add(
332
371
  # Temporary workaround - calculations without slippage
333
372
  current_price_float = float(subnet_info.price.tao)
334
373
  rate = 1.0 / current_price_float
335
- received_amount = rate * amount_to_stake
336
-
337
- # Add rows for the table
338
- base_row = [
339
- str(netuid), # netuid
340
- f"{hotkey[1]}", # hotkey
341
- str(amount_to_stake), # amount
342
- str(rate)
343
- + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", # rate
344
- str(received_amount.set_unit(netuid)), # received
345
- str(stake_fee), # fee
346
- # str(slippage_pct), # slippage
347
- ]
348
374
 
349
375
  # If we are staking safe, add price tolerance
350
376
  if safe_staking:
@@ -356,21 +382,45 @@ async def stake_add(
356
382
  rate_with_tolerance = f"{_rate_with_tolerance:.4f}"
357
383
  price_with_tolerance = Balance.from_tao(
358
384
  price_with_tolerance
359
- ).rao # Actual price to pass to extrinsic
385
+ ) # Actual price to pass to extrinsic
360
386
  else:
361
387
  rate_with_tolerance = "1"
362
388
  price_with_tolerance = Balance.from_rao(1)
389
+ extrinsic_fee = await get_stake_extrinsic_fee(
390
+ netuid_=netuid,
391
+ amount_=amount_to_stake,
392
+ staking_address_=hotkey[1],
393
+ safe_staking_=safe_staking,
394
+ price_limit=price_with_tolerance,
395
+ )
363
396
  prices_with_tolerance.append(price_with_tolerance)
364
-
365
- base_row.extend(
366
- [
367
- f"{rate_with_tolerance} {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ",
368
- f"[{'dark_sea_green3' if allow_partial_stake else 'red'}]"
369
- # safe staking
370
- f"{allow_partial_stake}[/{'dark_sea_green3' if allow_partial_stake else 'red'}]",
371
- ]
397
+ row_extension = [
398
+ f"{rate_with_tolerance} {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ",
399
+ f"[{'dark_sea_green3' if allow_partial_stake else 'red'}]"
400
+ # safe staking
401
+ f"{allow_partial_stake}[/{'dark_sea_green3' if allow_partial_stake else 'red'}]",
402
+ ]
403
+ else:
404
+ extrinsic_fee = await get_stake_extrinsic_fee(
405
+ netuid_=netuid,
406
+ amount_=amount_to_stake,
407
+ staking_address_=hotkey[1],
408
+ safe_staking_=safe_staking,
372
409
  )
373
-
410
+ row_extension = []
411
+ received_amount = rate * (amount_to_stake - stake_fee - extrinsic_fee)
412
+ # Add rows for the table
413
+ base_row = [
414
+ str(netuid), # netuid
415
+ f"{hotkey[1]}", # hotkey
416
+ str(amount_to_stake), # amount
417
+ str(rate)
418
+ + f" {Balance.get_unit(netuid)}/{Balance.get_unit(0)} ", # rate
419
+ str(received_amount.set_unit(netuid)), # received
420
+ str(stake_fee), # fee
421
+ str(extrinsic_fee),
422
+ # str(slippage_pct), # slippage
423
+ ] + row_extension
374
424
  rows.append(tuple(base_row))
375
425
 
376
426
  # Define and print stake table + slippage warning
@@ -569,17 +619,17 @@ def _define_stake_table(
569
619
  "Hotkey", justify="center", style=COLOR_PALETTE["GENERAL"]["HOTKEY"]
570
620
  )
571
621
  table.add_column(
572
- f"Amount ({Balance.get_unit(0)})",
622
+ "Amount (τ)",
573
623
  justify="center",
574
624
  style=COLOR_PALETTE["POOLS"]["TAO"],
575
625
  )
576
626
  table.add_column(
577
- f"Rate (per {Balance.get_unit(0)})",
627
+ "Rate (per τ)",
578
628
  justify="center",
579
629
  style=COLOR_PALETTE["POOLS"]["RATE"],
580
630
  )
581
631
  table.add_column(
582
- "Received",
632
+ "Est. Received",
583
633
  justify="center",
584
634
  style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"],
585
635
  )
@@ -588,6 +638,11 @@ def _define_stake_table(
588
638
  justify="center",
589
639
  style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"],
590
640
  )
641
+ table.add_column(
642
+ "Extrinsic Fee (τ)",
643
+ justify="center",
644
+ style=COLOR_PALETTE.STAKE.TAO,
645
+ )
591
646
  # TODO: Uncomment when slippage is reimplemented for v3
592
647
  # table.add_column(
593
648
  # "Slippage", justify="center", style=COLOR_PALETTE["STAKE"]["SLIPPAGE_PERCENT"]
@@ -33,12 +33,15 @@ async def display_stake_movement_cross_subnets(
33
33
  destination_hotkey: str,
34
34
  amount_to_move: Balance,
35
35
  stake_fee: Balance,
36
+ extrinsic_fee: Balance,
36
37
  ) -> tuple[Balance, str]:
37
38
  """Calculate and display stake movement information"""
38
39
 
39
40
  if origin_netuid == destination_netuid:
40
41
  subnet = await subtensor.subnet(origin_netuid)
41
- received_amount_tao = subnet.alpha_to_tao(amount_to_move - stake_fee)
42
+ received_amount_tao = (
43
+ subnet.alpha_to_tao(amount_to_move - stake_fee) - extrinsic_fee
44
+ )
42
45
  received_amount = subnet.tao_to_alpha(received_amount_tao)
43
46
 
44
47
  if received_amount < Balance.from_tao(0).set_unit(destination_netuid):
@@ -62,7 +65,9 @@ async def display_stake_movement_cross_subnets(
62
65
  price_destination = dynamic_destination.price.tao
63
66
  rate = price_origin / (price_destination or 1)
64
67
 
65
- received_amount_tao = dynamic_origin.alpha_to_tao(amount_to_move - stake_fee)
68
+ received_amount_tao = (
69
+ dynamic_origin.alpha_to_tao(amount_to_move - stake_fee) - extrinsic_fee
70
+ )
66
71
  received_amount = dynamic_destination.tao_to_alpha(received_amount_tao)
67
72
  received_amount.set_unit(destination_netuid)
68
73
 
@@ -81,14 +86,14 @@ async def display_stake_movement_cross_subnets(
81
86
  # Create and display table
82
87
  table = Table(
83
88
  title=(
84
- f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]"
89
+ f"\n[{COLOR_PALETTE.G.HEADER}]"
85
90
  f"Moving stake from: "
86
- f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{Balance.get_unit(origin_netuid)}(Netuid: {origin_netuid})"
87
- f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}] "
91
+ f"[{COLOR_PALETTE.G.SUBHEAD}]{Balance.get_unit(origin_netuid)}(Netuid: {origin_netuid})"
92
+ f"[/{COLOR_PALETTE.G.SUBHEAD}] "
88
93
  f"to: "
89
- f"[{COLOR_PALETTE['GENERAL']['SUBHEADING']}]{Balance.get_unit(destination_netuid)}(Netuid: {destination_netuid})"
90
- f"[/{COLOR_PALETTE['GENERAL']['SUBHEADING']}]\nNetwork: {subtensor.network}\n"
91
- f"[/{COLOR_PALETTE['GENERAL']['HEADER']}]"
94
+ f"[{COLOR_PALETTE.G.SUBHEAD}]{Balance.get_unit(destination_netuid)}(Netuid: {destination_netuid})"
95
+ f"[/{COLOR_PALETTE.G.SUBHEAD}]\nNetwork: {subtensor.network}\n"
96
+ f"[/{COLOR_PALETTE.G.HEADER}]"
92
97
  ),
93
98
  show_footer=True,
94
99
  show_edge=False,
@@ -132,6 +137,9 @@ async def display_stake_movement_cross_subnets(
132
137
  justify="center",
133
138
  style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"],
134
139
  )
140
+ table.add_column(
141
+ "Extrinsic Fee (τ)", justify="center", style=COLOR_PALETTE.STAKE.TAO
142
+ )
135
143
 
136
144
  table.add_row(
137
145
  f"{Balance.get_unit(origin_netuid)}({origin_netuid})",
@@ -142,6 +150,7 @@ async def display_stake_movement_cross_subnets(
142
150
  price_str,
143
151
  str(received_amount),
144
152
  str(stake_fee.set_unit(origin_netuid)),
153
+ str(extrinsic_fee),
145
154
  )
146
155
 
147
156
  console.print(table)
@@ -165,10 +174,10 @@ def prompt_stake_amount(
165
174
  while True:
166
175
  amount_input = Prompt.ask(
167
176
  f"\nEnter the amount to {action_name} "
168
- f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] "
169
- f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}](max: {current_balance})[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] "
177
+ f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE.S.STAKE_AMOUNT}] "
178
+ f"[{COLOR_PALETTE.S.STAKE_AMOUNT}](max: {current_balance})[/{COLOR_PALETTE.S.STAKE_AMOUNT}] "
170
179
  f"or "
171
- f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]'all'[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] "
180
+ f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]'all'[/{COLOR_PALETTE.S.STAKE_AMOUNT}] "
172
181
  f"for entire balance"
173
182
  )
174
183
 
@@ -183,7 +192,7 @@ def prompt_stake_amount(
183
192
  if amount > current_balance.tao:
184
193
  console.print(
185
194
  f"[red]Amount exceeds available balance of "
186
- f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]"
195
+ f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]{current_balance}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]"
187
196
  f"[/red]"
188
197
  )
189
198
  continue
@@ -270,8 +279,8 @@ async def stake_move_transfer_selection(
270
279
 
271
280
  # Display available netuids for selected hotkey
272
281
  table = Table(
273
- title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Available Stakes for Hotkey\n[/{COLOR_PALETTE['GENERAL']['HEADER']}]"
274
- f"[{COLOR_PALETTE['GENERAL']['HOTKEY']}]{origin_hotkey_ss58}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]\n",
282
+ title=f"\n[{COLOR_PALETTE.G.HEADER}]Available Stakes for Hotkey\n[/{COLOR_PALETTE.G.HEADER}]"
283
+ f"[{COLOR_PALETTE.G.HK}]{origin_hotkey_ss58}[/{COLOR_PALETTE.G.HK}]\n",
275
284
  show_edge=False,
276
285
  header_style="bold white",
277
286
  border_style="bright_black",
@@ -279,7 +288,7 @@ async def stake_move_transfer_selection(
279
288
  width=len(origin_hotkey_ss58) + 20,
280
289
  )
281
290
  table.add_column("Netuid", style="cyan")
282
- table.add_column("Stake Amount", style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"])
291
+ table.add_column("Stake Amount", style=COLOR_PALETTE.STAKE.STAKE_AMOUNT)
283
292
 
284
293
  available_netuids = []
285
294
  for netuid in origin_hotkey_info["netuids"]:
@@ -347,8 +356,8 @@ async def stake_swap_selection(
347
356
 
348
357
  # Display available stakes
349
358
  table = Table(
350
- title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Available Stakes for Hotkey\n[/{COLOR_PALETTE['GENERAL']['HEADER']}]"
351
- f"[{COLOR_PALETTE['GENERAL']['HOTKEY']}]{wallet.hotkey_str}: {wallet.hotkey.ss58_address}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}]\n",
359
+ title=f"\n[{COLOR_PALETTE.G.HEADER}]Available Stakes for Hotkey\n[/{COLOR_PALETTE.G.HEADER}]"
360
+ f"[{COLOR_PALETTE.G.HK}]{wallet.hotkey_str}: {wallet.hotkey.ss58_address}[/{COLOR_PALETTE.G.HK}]\n",
352
361
  show_edge=False,
353
362
  header_style="bold white",
354
363
  border_style="bright_black",
@@ -366,7 +375,7 @@ async def stake_swap_selection(
366
375
  for idx, (netuid, stake_info) in enumerate(sorted(hotkey_stakes.items())):
367
376
  subnet_info = subnet_dict[netuid]
368
377
  subnet_name_cell = (
369
- f"[{COLOR_PALETTE['GENERAL']['SYMBOL']}]{subnet_info.symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE['GENERAL']['SYMBOL']}]"
378
+ f"[{COLOR_PALETTE.G.SYM}]{subnet_info.symbol if netuid != 0 else 'τ'}[/{COLOR_PALETTE.G.SYM}]"
370
379
  f" {get_subnet_name(subnet_info)}"
371
380
  )
372
381
 
@@ -498,14 +507,28 @@ async def move_stake(
498
507
  )
499
508
  return False
500
509
 
501
- stake_fee = await subtensor.get_stake_fee(
502
- origin_hotkey_ss58=origin_hotkey,
503
- origin_netuid=origin_netuid,
504
- origin_coldkey_ss58=wallet.coldkeypub.ss58_address,
505
- destination_hotkey_ss58=destination_hotkey,
506
- destination_netuid=destination_netuid,
507
- destination_coldkey_ss58=wallet.coldkeypub.ss58_address,
508
- amount=amount_to_move_as_balance.rao,
510
+ call = await subtensor.substrate.compose_call(
511
+ call_module="SubtensorModule",
512
+ call_function="move_stake",
513
+ call_params={
514
+ "origin_hotkey": origin_hotkey,
515
+ "origin_netuid": origin_netuid,
516
+ "destination_hotkey": destination_hotkey,
517
+ "destination_netuid": destination_netuid,
518
+ "alpha_amount": amount_to_move_as_balance.rao,
519
+ },
520
+ )
521
+ stake_fee, extrinsic_fee = await asyncio.gather(
522
+ subtensor.get_stake_fee(
523
+ origin_hotkey_ss58=origin_hotkey,
524
+ origin_netuid=origin_netuid,
525
+ origin_coldkey_ss58=wallet.coldkeypub.ss58_address,
526
+ destination_hotkey_ss58=destination_hotkey,
527
+ destination_netuid=destination_netuid,
528
+ destination_coldkey_ss58=wallet.coldkeypub.ss58_address,
529
+ amount=amount_to_move_as_balance.rao,
530
+ ),
531
+ subtensor.get_extrinsic_fee(call, wallet.coldkeypub),
509
532
  )
510
533
 
511
534
  # Display stake movement details
@@ -519,6 +542,7 @@ async def move_stake(
519
542
  destination_hotkey=destination_hotkey,
520
543
  amount_to_move=amount_to_move_as_balance,
521
544
  stake_fee=stake_fee,
545
+ extrinsic_fee=extrinsic_fee,
522
546
  )
523
547
  except ValueError:
524
548
  return False
@@ -529,20 +553,10 @@ async def move_stake(
529
553
  if not unlock_key(wallet).success:
530
554
  return False
531
555
  with console.status(
532
- f"\n:satellite: Moving [blue]{amount_to_move_as_balance}[/blue] from [blue]{origin_hotkey}[/blue] on netuid: [blue]{origin_netuid}[/blue] \nto "
556
+ f"\n:satellite: Moving [blue]{amount_to_move_as_balance}[/blue] from [blue]{origin_hotkey}[/blue] on netuid: "
557
+ f"[blue]{origin_netuid}[/blue] \nto "
533
558
  f"[blue]{destination_hotkey}[/blue] on netuid: [blue]{destination_netuid}[/blue] ..."
534
559
  ):
535
- call = await subtensor.substrate.compose_call(
536
- call_module="SubtensorModule",
537
- call_function="move_stake",
538
- call_params={
539
- "origin_hotkey": origin_hotkey,
540
- "origin_netuid": origin_netuid,
541
- "destination_hotkey": destination_hotkey,
542
- "destination_netuid": destination_netuid,
543
- "alpha_amount": amount_to_move_as_balance.rao,
544
- },
545
- )
546
560
  extrinsic = await subtensor.substrate.create_signed_extrinsic(
547
561
  call=call, keypair=wallet.coldkey, era={"period": era}
548
562
  )
@@ -677,19 +691,33 @@ async def transfer_stake(
677
691
  if amount_to_transfer > current_stake:
678
692
  err_console.print(
679
693
  f"[red]Not enough stake to transfer[/red]:\n"
680
- f"Stake balance: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_stake}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] < "
681
- f"Transfer amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_to_transfer}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]"
694
+ f"Stake balance: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{current_stake}[/{COLOR_PALETTE.S.STAKE_AMOUNT}] < "
695
+ f"Transfer amount: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{amount_to_transfer}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]"
682
696
  )
683
697
  return False
684
698
 
685
- stake_fee = await subtensor.get_stake_fee(
686
- origin_hotkey_ss58=origin_hotkey,
687
- origin_netuid=origin_netuid,
688
- origin_coldkey_ss58=wallet.coldkeypub.ss58_address,
689
- destination_hotkey_ss58=origin_hotkey,
690
- destination_netuid=dest_netuid,
691
- destination_coldkey_ss58=dest_coldkey_ss58,
692
- amount=amount_to_transfer.rao,
699
+ call = await subtensor.substrate.compose_call(
700
+ call_module="SubtensorModule",
701
+ call_function="transfer_stake",
702
+ call_params={
703
+ "destination_coldkey": dest_coldkey_ss58,
704
+ "hotkey": origin_hotkey,
705
+ "origin_netuid": origin_netuid,
706
+ "destination_netuid": dest_netuid,
707
+ "alpha_amount": amount_to_transfer.rao,
708
+ },
709
+ )
710
+ stake_fee, extrinsic_fee = await asyncio.gather(
711
+ subtensor.get_stake_fee(
712
+ origin_hotkey_ss58=origin_hotkey,
713
+ origin_netuid=origin_netuid,
714
+ origin_coldkey_ss58=wallet.coldkeypub.ss58_address,
715
+ destination_hotkey_ss58=origin_hotkey,
716
+ destination_netuid=dest_netuid,
717
+ destination_coldkey_ss58=dest_coldkey_ss58,
718
+ amount=amount_to_transfer.rao,
719
+ ),
720
+ subtensor.get_extrinsic_fee(call, wallet.coldkeypub),
693
721
  )
694
722
 
695
723
  # Display stake movement details
@@ -703,6 +731,7 @@ async def transfer_stake(
703
731
  destination_hotkey=origin_hotkey,
704
732
  amount_to_move=amount_to_transfer,
705
733
  stake_fee=stake_fee,
734
+ extrinsic_fee=extrinsic_fee,
706
735
  )
707
736
  except ValueError:
708
737
  return False
@@ -715,18 +744,6 @@ async def transfer_stake(
715
744
  return False
716
745
 
717
746
  with console.status("\n:satellite: Transferring stake ..."):
718
- call = await subtensor.substrate.compose_call(
719
- call_module="SubtensorModule",
720
- call_function="transfer_stake",
721
- call_params={
722
- "destination_coldkey": dest_coldkey_ss58,
723
- "hotkey": origin_hotkey,
724
- "origin_netuid": origin_netuid,
725
- "destination_netuid": dest_netuid,
726
- "alpha_amount": amount_to_transfer.rao,
727
- },
728
- )
729
-
730
747
  extrinsic = await subtensor.substrate.create_signed_extrinsic(
731
748
  call=call, keypair=wallet.coldkey, era={"period": era}
732
749
  )
@@ -846,19 +863,32 @@ async def swap_stake(
846
863
  if amount_to_swap > current_stake:
847
864
  err_console.print(
848
865
  f"[red]Not enough stake to swap[/red]:\n"
849
- f"Stake balance: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_stake}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] < "
850
- f"Swap amount: [{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{amount_to_swap}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]"
866
+ f"Stake balance: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{current_stake}[/{COLOR_PALETTE.S.STAKE_AMOUNT}] < "
867
+ f"Swap amount: [{COLOR_PALETTE.S.STAKE_AMOUNT}]{amount_to_swap}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]"
851
868
  )
852
869
  return False
853
870
 
854
- stake_fee = await subtensor.get_stake_fee(
855
- origin_hotkey_ss58=hotkey_ss58,
856
- origin_netuid=origin_netuid,
857
- origin_coldkey_ss58=wallet.coldkeypub.ss58_address,
858
- destination_hotkey_ss58=hotkey_ss58,
859
- destination_netuid=destination_netuid,
860
- destination_coldkey_ss58=wallet.coldkeypub.ss58_address,
861
- amount=amount_to_swap.rao,
871
+ call = await subtensor.substrate.compose_call(
872
+ call_module="SubtensorModule",
873
+ call_function="swap_stake",
874
+ call_params={
875
+ "hotkey": hotkey_ss58,
876
+ "origin_netuid": origin_netuid,
877
+ "destination_netuid": destination_netuid,
878
+ "alpha_amount": amount_to_swap.rao,
879
+ },
880
+ )
881
+ stake_fee, extrinsic_fee = await asyncio.gather(
882
+ subtensor.get_stake_fee(
883
+ origin_hotkey_ss58=hotkey_ss58,
884
+ origin_netuid=origin_netuid,
885
+ origin_coldkey_ss58=wallet.coldkeypub.ss58_address,
886
+ destination_hotkey_ss58=hotkey_ss58,
887
+ destination_netuid=destination_netuid,
888
+ destination_coldkey_ss58=wallet.coldkeypub.ss58_address,
889
+ amount=amount_to_swap.rao,
890
+ ),
891
+ subtensor.get_extrinsic_fee(call, wallet.coldkeypub),
862
892
  )
863
893
 
864
894
  # Display stake movement details
@@ -872,6 +902,7 @@ async def swap_stake(
872
902
  destination_hotkey=hotkey_ss58,
873
903
  amount_to_move=amount_to_swap,
874
904
  stake_fee=stake_fee,
905
+ extrinsic_fee=extrinsic_fee,
875
906
  )
876
907
  except ValueError:
877
908
  return False
@@ -887,17 +918,6 @@ async def swap_stake(
887
918
  f"\n:satellite: Swapping stake from netuid [blue]{origin_netuid}[/blue] "
888
919
  f"to netuid [blue]{destination_netuid}[/blue]..."
889
920
  ):
890
- call = await subtensor.substrate.compose_call(
891
- call_module="SubtensorModule",
892
- call_function="swap_stake",
893
- call_params={
894
- "hotkey": hotkey_ss58,
895
- "origin_netuid": origin_netuid,
896
- "destination_netuid": destination_netuid,
897
- "alpha_amount": amount_to_swap.rao,
898
- },
899
- )
900
-
901
921
  extrinsic = await subtensor.substrate.create_signed_extrinsic(
902
922
  call=call, keypair=wallet.coldkey, era={"period": era}
903
923
  )
@@ -47,6 +47,7 @@ async def unstake(
47
47
  era: int,
48
48
  ):
49
49
  """Unstake from hotkey(s)."""
50
+
50
51
  with console.status(
51
52
  f"Retrieving subnet data & identities from {subtensor.network}...",
52
53
  spinner="earth",
@@ -82,7 +83,7 @@ async def unstake(
82
83
  hotkey_to_unstake_all = hotkeys_to_unstake_from[0]
83
84
  unstake_all_alpha = Confirm.ask(
84
85
  "\nDo you want to:\n"
85
- "[blue]Yes[/blue]: Unstake from all subnets and automatically restake to subnet 0 (root)\n"
86
+ "[blue]Yes[/blue]: Unstake from all subnets and automatically re-stake to subnet 0 (root)\n"
86
87
  "[blue]No[/blue]: Unstake everything (including subnet 0)",
87
88
  default=True,
88
89
  )
@@ -210,8 +211,38 @@ async def unstake(
210
211
 
211
212
  try:
212
213
  current_price = subnet_info.price.tao
214
+ if safe_staking:
215
+ if subnet_info.is_dynamic:
216
+ price_with_tolerance = current_price * (1 - rate_tolerance)
217
+ rate_with_tolerance = price_with_tolerance
218
+ price_limit = Balance.from_tao(
219
+ rate_with_tolerance
220
+ ) # Actual price to pass to extrinsic
221
+ else:
222
+ price_limit = Balance.from_rao(1)
223
+ extrinsic_fee = await _get_extrinsic_fee(
224
+ "unstake_safe",
225
+ wallet,
226
+ subtensor,
227
+ hotkey_ss58=staking_address_ss58,
228
+ amount=amount_to_unstake_as_balance,
229
+ netuid=netuid,
230
+ price_limit=price_limit,
231
+ allow_partial_stake=allow_partial_stake,
232
+ )
233
+ else:
234
+ extrinsic_fee = await _get_extrinsic_fee(
235
+ "unstake",
236
+ wallet,
237
+ subtensor,
238
+ hotkey_ss58=staking_address_ss58,
239
+ netuid=netuid,
240
+ amount=amount_to_unstake_as_balance,
241
+ )
213
242
  rate = current_price
214
- received_amount = amount_to_unstake_as_balance * rate
243
+ received_amount = (
244
+ (amount_to_unstake_as_balance - stake_fee) * rate
245
+ ) - extrinsic_fee
215
246
  except ValueError:
216
247
  continue
217
248
  total_received_amount += received_amount
@@ -233,8 +264,9 @@ async def unstake(
233
264
  staking_address_name, # Hotkey Name
234
265
  str(amount_to_unstake_as_balance), # Amount to Unstake
235
266
  f"{subnet_info.price.tao:.6f}"
236
- + f"({Balance.get_unit(0)}/{Balance.get_unit(netuid)})", # Rate
267
+ + f"(τ/{Balance.get_unit(netuid)})", # Rate
237
268
  str(stake_fee.set_unit(netuid)), # Fee
269
+ str(extrinsic_fee), # Extrinsic fee
238
270
  str(received_amount), # Received Amount
239
271
  # slippage_pct, # Slippage Percent
240
272
  ]
@@ -431,10 +463,15 @@ async def unstake_all(
431
463
  style=COLOR_PALETTE["POOLS"]["RATE"],
432
464
  )
433
465
  table.add_column(
434
- f"Fee ({Balance.unit})",
466
+ f"Fee ({Balance.get_unit(1)})",
435
467
  justify="center",
436
468
  style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"],
437
469
  )
470
+ table.add_column(
471
+ "Extrinsic Fee (τ)",
472
+ justify="center",
473
+ style=COLOR_PALETTE.STAKE.TAO,
474
+ )
438
475
  table.add_column(
439
476
  f"Received ({Balance.unit})",
440
477
  justify="center",
@@ -467,8 +504,17 @@ async def unstake_all(
467
504
 
468
505
  try:
469
506
  current_price = subnet_info.price.tao
507
+ extrinsic_type = (
508
+ "unstake_all" if not unstake_all_alpha else "unstake_all_alpha"
509
+ )
510
+ extrinsic_fee = await _get_extrinsic_fee(
511
+ extrinsic_type,
512
+ wallet,
513
+ subtensor,
514
+ hotkey_ss58=stake.hotkey_ss58,
515
+ )
470
516
  rate = current_price
471
- received_amount = stake_amount * rate - stake_fee
517
+ received_amount = ((stake_amount - stake_fee) * rate) - extrinsic_fee
472
518
 
473
519
  if received_amount < Balance.from_tao(0):
474
520
  print_error("Not enough Alpha to pay the transaction fee.")
@@ -485,6 +531,7 @@ async def unstake_all(
485
531
  f"{float(subnet_info.price):.6f}"
486
532
  + f"({Balance.get_unit(0)}/{Balance.get_unit(stake.netuid)})",
487
533
  str(stake_fee),
534
+ str(extrinsic_fee),
488
535
  str(received_amount),
489
536
  )
490
537
  console.print(table)
@@ -830,6 +877,63 @@ async def _unstake_all_extrinsic(
830
877
  err_out(f"{failure_prelude} with error: {str(e)}")
831
878
 
832
879
 
880
+ async def _get_extrinsic_fee(
881
+ _type: str,
882
+ wallet: Wallet,
883
+ subtensor: "SubtensorInterface",
884
+ hotkey_ss58: str,
885
+ netuid: Optional[int] = None,
886
+ amount: Optional[Balance] = None,
887
+ price_limit: Optional[Balance] = None,
888
+ allow_partial_stake: bool = False,
889
+ ) -> Balance:
890
+ """
891
+ Retrieves the extrinsic fee for a given unstaking call.
892
+ Args:
893
+ _type: 'unstake', 'unstake_safe', 'unstake_all', 'unstake_all_alpha' depending on the specific
894
+ extrinsic to be called
895
+ wallet: Wallet object
896
+ subtensor: SubtensorInterface object
897
+ hotkey_ss58: the hotkey ss58 to unstake from
898
+ netuid: the netuid from which to remove the stake
899
+ amount: the amount of stake to remove
900
+ price_limit: the price limit
901
+ allow_partial_stake: whether to allow partial unstaking
902
+
903
+ Returns:
904
+ Balance object representing the extrinsic fee.
905
+ """
906
+ lookup_table = {
907
+ "unstake": lambda: (
908
+ "remove_stake",
909
+ {
910
+ "hotkey": hotkey_ss58,
911
+ "netuid": netuid,
912
+ "amount_unstaked": amount.rao,
913
+ },
914
+ ),
915
+ "unstake_safe": lambda: (
916
+ "remove_stake_limit",
917
+ {
918
+ "hotkey": hotkey_ss58,
919
+ "netuid": netuid,
920
+ "amount_unstaked": amount.rao,
921
+ "limit_price": price_limit,
922
+ "allow_partial": allow_partial_stake,
923
+ },
924
+ ),
925
+ "unstake_all": lambda: ("unstake_all", {"hotkey": hotkey_ss58}),
926
+ "unstake_all_alpha": lambda: ("unstake_all_alpha", {"hotkey": hotkey_ss58}),
927
+ }
928
+ call_fn, call_params = lookup_table[_type]()
929
+ call = await subtensor.substrate.compose_call(
930
+ call_module="SubtensorModule",
931
+ call_function=call_fn,
932
+ call_params=call_params,
933
+ )
934
+ return await subtensor.get_extrinsic_fee(call, wallet.coldkeypub)
935
+
936
+
833
937
  # Helpers
834
938
  async def _unstake_selection(
835
939
  dynamic_info,
@@ -1184,7 +1288,7 @@ def _create_unstake_table(
1184
1288
  style=COLOR_PALETTE["POOLS"]["TAO"],
1185
1289
  )
1186
1290
  table.add_column(
1187
- f"Rate ({Balance.get_unit(0)}/{Balance.get_unit(1)})",
1291
+ f"Rate (τ/{Balance.get_unit(1)})",
1188
1292
  justify="center",
1189
1293
  style=COLOR_PALETTE["POOLS"]["RATE"],
1190
1294
  )
@@ -1194,7 +1298,10 @@ def _create_unstake_table(
1194
1298
  style=COLOR_PALETTE["STAKE"]["STAKE_AMOUNT"],
1195
1299
  )
1196
1300
  table.add_column(
1197
- f"Received ({Balance.get_unit(0)})",
1301
+ "Extrinsic Fee (τ)", justify="center", style=COLOR_PALETTE.STAKE.TAO
1302
+ )
1303
+ table.add_column(
1304
+ "Received (τ)",
1198
1305
  justify="center",
1199
1306
  style=COLOR_PALETTE["POOLS"]["TAO_EQUIV"],
1200
1307
  footer=str(total_received_amount),
@@ -1408,6 +1408,7 @@ async def transfer(
1408
1408
  destination: str,
1409
1409
  amount: float,
1410
1410
  transfer_all: bool,
1411
+ allow_death: bool,
1411
1412
  era: int,
1412
1413
  prompt: bool,
1413
1414
  json_output: bool,
@@ -1419,6 +1420,7 @@ async def transfer(
1419
1420
  destination=destination,
1420
1421
  amount=Balance.from_tao(amount),
1421
1422
  transfer_all=transfer_all,
1423
+ allow_death=allow_death,
1422
1424
  era=era,
1423
1425
  prompt=prompt,
1424
1426
  )
@@ -1800,10 +1802,96 @@ async def sign(
1800
1802
  )
1801
1803
 
1802
1804
  signed_message = keypair.sign(message.encode("utf-8")).hex()
1803
- console.print("[dark_sea_green3]Message signed successfully:")
1805
+ signer_address = keypair.ss58_address
1806
+ console.print("[dark_sea_green3]Message signed successfully!\n")
1807
+
1804
1808
  if json_output:
1805
- json_console.print(json.dumps({"signed_message": signed_message}))
1806
- console.print(signed_message)
1809
+ json_console.print(
1810
+ json.dumps(
1811
+ {"signed_message": signed_message, "signer_address": signer_address}
1812
+ )
1813
+ )
1814
+ else:
1815
+ console.print(f"[yellow]Signature:[/yellow]\n{signed_message}")
1816
+ console.print(f"[yellow]Signer address:[/yellow] {signer_address}")
1817
+
1818
+
1819
+ async def verify(
1820
+ message: str,
1821
+ signature: str,
1822
+ public_key_or_ss58: str,
1823
+ json_output: bool = False,
1824
+ ):
1825
+ """Verify a message signature using a public key or SS58 address."""
1826
+
1827
+ if is_valid_ss58_address(public_key_or_ss58):
1828
+ print_verbose(f"[blue]SS58 address detected:[/blue] {public_key_or_ss58}")
1829
+ keypair = Keypair(ss58_address=public_key_or_ss58)
1830
+ signer_address = public_key_or_ss58
1831
+ else:
1832
+ try:
1833
+ public_key_hex = public_key_or_ss58.strip().lower()
1834
+ if public_key_hex.startswith("0x"):
1835
+ public_key_hex = public_key_hex[2:]
1836
+ if len(public_key_hex) == 64:
1837
+ bytes.fromhex(public_key_hex)
1838
+ print_verbose("[blue]Hex public key detected[/blue] (64 characters)")
1839
+ keypair = Keypair(public_key=public_key_hex)
1840
+ signer_address = keypair.ss58_address
1841
+ print_verbose(
1842
+ f"[blue]Corresponding SS58 address:[/blue] {signer_address}"
1843
+ )
1844
+ else:
1845
+ raise ValueError("Public key must be 32 bytes (64 hex characters)")
1846
+
1847
+ except (ValueError, TypeError) as e:
1848
+ if json_output:
1849
+ json_console.print(
1850
+ json.dumps(
1851
+ {
1852
+ "verified": False,
1853
+ "error": f"Invalid public key or SS58 address: {str(e)}",
1854
+ }
1855
+ )
1856
+ )
1857
+ else:
1858
+ err_console.print(
1859
+ f":cross_mark: Invalid SS58 address or hex public key (64 chars, with or without 0x prefix)- {str(e)}"
1860
+ )
1861
+ return False
1862
+
1863
+ try:
1864
+ signature_bytes = bytes.fromhex(signature.strip().lower().replace("0x", ""))
1865
+ except ValueError as e:
1866
+ if json_output:
1867
+ json_console.print(
1868
+ json.dumps(
1869
+ {
1870
+ "verified": False,
1871
+ "error": f"Invalid signature format: {str(e)}",
1872
+ }
1873
+ )
1874
+ )
1875
+ else:
1876
+ err_console.print(f"[red]:cross_mark: Invalid signature format: {str(e)}")
1877
+ return False
1878
+
1879
+ is_valid = keypair.verify(message.encode("utf-8"), signature_bytes)
1880
+
1881
+ if json_output:
1882
+ json_console.print(
1883
+ json.dumps(
1884
+ {"verified": is_valid, "signer": signer_address, "message": message}
1885
+ )
1886
+ )
1887
+ else:
1888
+ if is_valid:
1889
+ console.print("[dark_sea_green3]Signature is valid!\n")
1890
+ console.print(f"[yellow]Signer:[/yellow] {signer_address}")
1891
+ else:
1892
+ err_console.print(":cross_mark: [red]Signature verification failed!")
1893
+
1894
+ return is_valid
1807
1895
 
1808
1896
 
1809
1897
  async def schedule_coldkey_swap(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bittensor-cli
3
- Version: 9.8.7
3
+ Version: 9.9.0
4
4
  Summary: Bittensor CLI
5
5
  Author: bittensor.com
6
6
  Project-URL: homepage, https://github.com/opentensor/btcli
@@ -8,7 +8,7 @@ Project-URL: Repository, https://github.com/opentensor/btcli
8
8
  Requires-Python: <3.14,>=3.9
9
9
  Description-Content-Type: text/markdown
10
10
  Requires-Dist: wheel
11
- Requires-Dist: async-substrate-interface>=1.1.0
11
+ Requires-Dist: async-substrate-interface>=1.4.2
12
12
  Requires-Dist: aiohttp~=3.10.2
13
13
  Requires-Dist: backoff~=2.2.1
14
14
  Requires-Dist: click<8.2.0
@@ -1,5 +1,5 @@
1
1
  bittensor_cli/__init__.py,sha256=Lpv4NkbAQgwrfqFOnTMuR_S-fqGdaWCSLhxnFnGTHM0,1232
2
- bittensor_cli/cli.py,sha256=Q2AzvNEUz080lc7Usz-Dg2kFjQYCNERXnE1cWTWQubw,232741
2
+ bittensor_cli/cli.py,sha256=lG3LyOXXiOOEIPgiW9qnqmKnH2wa598iJ6pf-eNCveg,234995
3
3
  bittensor_cli/doc_generation_helper.py,sha256=GexqjEIKulWg84hpNBEchJ840oOgOi7DWpt447nsdNI,91
4
4
  bittensor_cli/version.py,sha256=dU1xsa3mG5FPdhzvqlzDByNcCHmzcFQH3q1pQr4u76g,720
5
5
  bittensor_cli/src/__init__.py,sha256=QCR6kt9rbUiLC4gkvczl47eg725nqSFK4Xr7_mz3e_s,33171
@@ -8,12 +8,12 @@ bittensor_cli/src/bittensor/balances.py,sha256=q5KkxF8wmUguWAFddEKstfDKTxPe5ISHp
8
8
  bittensor_cli/src/bittensor/chain_data.py,sha256=_GATOCacWMN2jRurzn3hffJiq_l7AznGRkKePTAflwQ,44420
9
9
  bittensor_cli/src/bittensor/minigraph.py,sha256=BIzmSVLfBYiRAeGD_i1LAC8Cw7zxp38a91SIFEPMqYc,10479
10
10
  bittensor_cli/src/bittensor/networking.py,sha256=pZLMs8YXpZzDMLXWMBb_Bj6TVkm_q9khyY-lnbwVMuE,462
11
- bittensor_cli/src/bittensor/subtensor_interface.py,sha256=iPpM0mGsRwc7iejBI9oChpFd8DgEw13-Yb1fflQyCb8,61886
11
+ bittensor_cli/src/bittensor/subtensor_interface.py,sha256=37BB5dfcRE6QrAKwuYs9XlaLs36xHrhJ3wCsWd-rXek,62672
12
12
  bittensor_cli/src/bittensor/utils.py,sha256=WfxxQnekVy7nbB2jsPrRC4E7SFIvAjDJJjq3glhKMSc,48270
13
13
  bittensor_cli/src/bittensor/extrinsics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  bittensor_cli/src/bittensor/extrinsics/registration.py,sha256=7ybqpJ7-Z_PTgj4boCOFxgfjMBCHv0JE7MNpJzJmyDc,66360
15
15
  bittensor_cli/src/bittensor/extrinsics/root.py,sha256=C9WPssL2HpNK8u_IFPPr8TrdFgbLPTfkbAYzalfmbRM,19188
16
- bittensor_cli/src/bittensor/extrinsics/transfer.py,sha256=tKXC7DTvaR15Lh20FWtoRMkEWQdNFQcXstZCEw9sWHM,8567
16
+ bittensor_cli/src/bittensor/extrinsics/transfer.py,sha256=S4HFvgwhUk-NQ26FOkg8m1ggAJ50jgPsW_isdAdS_rc,9235
17
17
  bittensor_cli/src/bittensor/templates/main-filters.j2,sha256=ZM0l2exr6FKqw86GRyHHtw-h8Y5Ckgx-SPc-48gZoxI,914
18
18
  bittensor_cli/src/bittensor/templates/main-header.j2,sha256=aZwB9NeEjNr0pM5mPYTYeky_031zAu6yErIMiwTPcwE,1684
19
19
  bittensor_cli/src/bittensor/templates/neuron-details.j2,sha256=O_Mhg2E04BzW_cSGIldObhVTSRX29gDNqnimW4ty9rY,4848
@@ -30,22 +30,22 @@ bittensor_cli/src/bittensor/templates/view.js,sha256=QIPnPp9SuYS9wcl7cwL8nFzd8id
30
30
  bittensor_cli/src/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
31
  bittensor_cli/src/commands/sudo.py,sha256=zyv4sJM7sH73_c-mgskcnF2kQfJBJ8_xF74WrZFv7rY,33820
32
32
  bittensor_cli/src/commands/view.py,sha256=9lx6vfOkel8KIefUhDNaBS_j5lNR2djcRFRbK4mbnDE,12535
33
- bittensor_cli/src/commands/wallets.py,sha256=v6JDcWhP_CcBruboixKM1XMqhUKL6GbllGd39wHKbBs,71646
33
+ bittensor_cli/src/commands/wallets.py,sha256=SIcEf7AkxonUAM3zYEaQUJTczCIY6iFtR3sTunhB1C8,74745
34
34
  bittensor_cli/src/commands/weights.py,sha256=BCJm_mlw0pVK4YEZuEMqQBpvvOoB7B1rzdvMeN3uTfM,16503
35
35
  bittensor_cli/src/commands/liquidity/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
36
  bittensor_cli/src/commands/liquidity/liquidity.py,sha256=AXCjBvQb2gakP8n1z81npYkZ_QF5CQy7r82AMtQwXzk,21692
37
37
  bittensor_cli/src/commands/liquidity/utils.py,sha256=egfZHnvBMc8ydntAHnU6V5Zyi-wLkomjNuucUj73aZQ,6361
38
38
  bittensor_cli/src/commands/stake/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
- bittensor_cli/src/commands/stake/add.py,sha256=oEpWFnRWazaFsbKn06o4gwQ1JfFmn4AwxRNecimI3js,27614
39
+ bittensor_cli/src/commands/stake/add.py,sha256=jdNOilpEsq8f7vJUQH1qFdiQ-HbSg6siYQl_CQdtlh0,29731
40
40
  bittensor_cli/src/commands/stake/children_hotkeys.py,sha256=lMiV-Z3SGZUEapdy0LRthFLx0RlFK0KVxytE47ybdEc,31746
41
41
  bittensor_cli/src/commands/stake/list.py,sha256=tzjhiJucXgOGaw7TGt420nGosH85AEjvOimP1XXV3Xs,29038
42
- bittensor_cli/src/commands/stake/move.py,sha256=X8rphxkL5BBHJSNpNqY_o3fffCpSI0-yX03SlNXfX_A,34857
43
- bittensor_cli/src/commands/stake/remove.py,sha256=DX7yPXs5n8Gp3O_Fwz8cMYI8CAiRp16gbKLwSCAoYmM,47341
42
+ bittensor_cli/src/commands/stake/move.py,sha256=RJiY_oSQUV-ZO04Y_PosrDqfz0JfdQX4ioB74f_7lU8,35144
43
+ bittensor_cli/src/commands/stake/remove.py,sha256=Xf_BWihl--4WGKPl6Wz4VkWus3JKEikHQKA0tbQTetM,51318
44
44
  bittensor_cli/src/commands/subnets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
45
  bittensor_cli/src/commands/subnets/price.py,sha256=akXkbilWjQYqTYvtOhXngX_cVkU0Mv-Gl3kjce6dtl0,21490
46
46
  bittensor_cli/src/commands/subnets/subnets.py,sha256=ZeR7gxjtxi9eQdPLlIOmw3jDxDPnteppTPJ1-1y_TCs,94601
47
- bittensor_cli-9.8.7.dist-info/METADATA,sha256=PPTxAAdYJbLNkgqEtJHrr6nhlGBP-fVlbXiG5IRTEvA,6601
48
- bittensor_cli-9.8.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
49
- bittensor_cli-9.8.7.dist-info/entry_points.txt,sha256=hBTLGLbVxmAKy69XSKaUZvjTCmyEzDGZKq4S8UOto8I,49
50
- bittensor_cli-9.8.7.dist-info/top_level.txt,sha256=DvgvXpmTtI_Q1BbDZMlK90LFcGFCreN1daViEPV2iFw,14
51
- bittensor_cli-9.8.7.dist-info/RECORD,,
47
+ bittensor_cli-9.9.0.dist-info/METADATA,sha256=3KHTWo7iaqwIrHfos259k3CbxZI9MBy_OwGxAZSf2rI,6601
48
+ bittensor_cli-9.9.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
49
+ bittensor_cli-9.9.0.dist-info/entry_points.txt,sha256=hBTLGLbVxmAKy69XSKaUZvjTCmyEzDGZKq4S8UOto8I,49
50
+ bittensor_cli-9.9.0.dist-info/top_level.txt,sha256=DvgvXpmTtI_Q1BbDZMlK90LFcGFCreN1daViEPV2iFw,14
51
+ bittensor_cli-9.9.0.dist-info/RECORD,,