Skip to content

Commit 4b24ca8

Browse files
authored
Merge pull request #481 from opentensor/feat/thewhaleking/multiple-netuid-staking
Allows for staking to multiple netuids in one btcli command
2 parents 64134b5 + 7efb09b commit 4b24ca8

File tree

3 files changed

+159
-39
lines changed

3 files changed

+159
-39
lines changed

bittensor_cli/cli.py

Lines changed: 69 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python3
22
import asyncio
33
import curses
4+
import copy
45
import importlib
56
import json
67
import os.path
@@ -10,7 +11,7 @@
1011
import traceback
1112
import warnings
1213
from pathlib import Path
13-
from typing import Coroutine, Optional
14+
from typing import Coroutine, Optional, Union
1415
from dataclasses import fields
1516

1617
import rich
@@ -89,6 +90,23 @@ class Options:
8990
Re-usable typer args
9091
"""
9192

93+
@classmethod
94+
def edit_help(cls, option_name: str, help_text: str):
95+
"""
96+
Edits the `help` attribute of a copied given Typer option in this class, returning
97+
the modified Typer option.
98+
99+
Args:
100+
option_name: the name of the option (e.g. "wallet_name")
101+
help_text: New help text to be used (e.g. "Wallet's name")
102+
103+
Returns:
104+
Modified Typer Option with new help text.
105+
"""
106+
copied_attr = copy.copy(getattr(cls, option_name))
107+
setattr(copied_attr, "help", help_text)
108+
return copied_attr
109+
92110
wallet_name = typer.Option(
93111
None,
94112
"--wallet-name",
@@ -3202,7 +3220,11 @@ def stake_add(
32023220
help="When set, this command stakes to all hotkeys associated with the wallet. Do not use if specifying "
32033221
"hotkeys in `--include-hotkeys`.",
32043222
),
3205-
netuid: Optional[int] = Options.netuid_not_req,
3223+
netuids: Optional[str] = Options.edit_help(
3224+
"netuids",
3225+
"Netuid(s) to for which to add stake. Specify multiple netuids by separating with a comma, e.g."
3226+
"`btcli st add -n 1,2,3",
3227+
),
32063228
all_netuids: bool = Options.all_netuids,
32073229
wallet_name: str = Options.wallet_name,
32083230
wallet_path: str = Options.wallet_path,
@@ -3242,29 +3264,52 @@ def stake_add(
32423264
6. Stake all balance to a subnet:
32433265
[green]$[/green] btcli stake add --all --netuid 3
32443266
3267+
7. Stake the same amount to multiple subnets:
3268+
[green]$[/green] btcli stake add --amount 100 --netuids 4,5,6
3269+
32453270
[bold]Safe Staking Parameters:[/bold]
32463271
• [blue]--safe[/blue]: Enables rate tolerance checks
32473272
• [blue]--tolerance[/blue]: Maximum % rate change allowed (0.05 = 5%)
32483273
• [blue]--partial[/blue]: Complete partial stake if rates exceed tolerance
32493274
32503275
"""
3276+
netuids = netuids or []
32513277
self.verbosity_handler(quiet, verbose, json_output)
32523278
safe_staking = self.ask_safe_staking(safe_staking)
32533279
if safe_staking:
32543280
rate_tolerance = self.ask_rate_tolerance(rate_tolerance)
32553281
allow_partial_stake = self.ask_partial_stake(allow_partial_stake)
32563282
console.print("\n")
3257-
netuid = get_optional_netuid(netuid, all_netuids)
3283+
3284+
if netuids:
3285+
netuids = parse_to_list(
3286+
netuids, int, "Netuids must be ints separated by commas", False
3287+
)
3288+
else:
3289+
netuid_ = get_optional_netuid(None, all_netuids)
3290+
netuids = [netuid_] if netuid_ else None
3291+
if netuids:
3292+
for netuid_ in netuids:
3293+
# ensure no negative netuids make it into our list
3294+
validate_netuid(netuid_)
32583295

32593296
if stake_all and amount:
32603297
print_error(
32613298
"Cannot specify an amount and 'stake-all'. Choose one or the other."
32623299
)
3263-
raise typer.Exit()
3300+
return
32643301

32653302
if stake_all and not amount:
32663303
if not Confirm.ask("Stake all the available TAO tokens?", default=False):
3267-
raise typer.Exit()
3304+
return
3305+
3306+
if (
3307+
stake_all
3308+
and (isinstance(netuids, list) and len(netuids) > 1)
3309+
or (netuids is None)
3310+
):
3311+
print_error("Cannot stake all to multiple subnets.")
3312+
return
32683313

32693314
if all_hotkeys and include_hotkeys:
32703315
print_error(
@@ -3285,9 +3330,10 @@ def stake_add(
32853330
"Enter the [blue]wallet name[/blue]",
32863331
default=self.config.get("wallet_name") or defaults.wallet.name,
32873332
)
3288-
if netuid is not None:
3333+
if netuids is not None:
32893334
hotkey_or_ss58 = Prompt.ask(
3290-
"Enter the [blue]wallet hotkey[/blue] name or [blue]ss58 address[/blue] to stake to [dim](or Press Enter to view delegates)[/dim]",
3335+
"Enter the [blue]wallet hotkey[/blue] name or [blue]ss58 address[/blue] to stake to [dim]"
3336+
"(or Press Enter to view delegates)[/dim]",
32913337
)
32923338
else:
32933339
hotkey_or_ss58 = Prompt.ask(
@@ -3299,10 +3345,18 @@ def stake_add(
32993345
wallet = self.wallet_ask(
33003346
wallet_name, wallet_path, wallet_hotkey, ask_for=[WO.NAME, WO.PATH]
33013347
)
3348+
if len(netuids) > 1:
3349+
netuid_ = IntPrompt.ask(
3350+
"Enter the netuid for which to show delegates",
3351+
choices=[str(x) for x in netuids],
3352+
)
3353+
else:
3354+
netuid_ = netuids[0]
3355+
33023356
selected_hotkey = self._run_command(
33033357
subnets.show(
33043358
subtensor=self.initialize_chain(network),
3305-
netuid=netuid,
3359+
netuid=netuid_,
33063360
sort=False,
33073361
max_rows=12,
33083362
prompt=False,
@@ -3312,7 +3366,7 @@ def stake_add(
33123366
)
33133367
if not selected_hotkey:
33143368
print_error("No delegate selected. Exiting.")
3315-
raise typer.Exit()
3369+
return
33163370
include_hotkeys = selected_hotkey
33173371
elif is_valid_ss58_address(hotkey_or_ss58):
33183372
wallet = self.wallet_ask(
@@ -3373,8 +3427,8 @@ def stake_add(
33733427
)
33743428
if free_balance == Balance.from_tao(0):
33753429
print_error("You dont have any balance to stake.")
3376-
raise typer.Exit()
3377-
if netuid is not None:
3430+
return
3431+
if netuids:
33783432
amount = FloatPrompt.ask(
33793433
f"Amount to [{COLORS.G.SUBHEAD_MAIN}]stake (TAO τ)"
33803434
)
@@ -3396,7 +3450,7 @@ def stake_add(
33963450
add_stake.stake_add(
33973451
wallet,
33983452
self.initialize_chain(network),
3399-
netuid,
3453+
netuids,
34003454
stake_all,
34013455
amount,
34023456
prompt,
@@ -4796,12 +4850,9 @@ def subnets_list(
47964850
def subnets_price(
47974851
self,
47984852
network: Optional[list[str]] = Options.network,
4799-
netuids: str = typer.Option(
4800-
None,
4801-
"--netuids",
4802-
"--netuid",
4803-
"-n",
4804-
help="Netuid(s) to show the price for.",
4853+
netuids: str = Options.edit_help(
4854+
"netuids",
4855+
"Netuids to show the price for. Separate multiple netuids with a comma, for example: `-n 0,1,2`.",
48054856
),
48064857
interval_hours: int = typer.Option(
48074858
24,

bittensor_cli/src/commands/stake/add.py

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
async def stake_add(
3232
wallet: Wallet,
3333
subtensor: "SubtensorInterface",
34-
netuid: Optional[int],
34+
netuids: Optional[list[int]],
3535
stake_all: bool,
3636
amount: float,
3737
prompt: bool,
@@ -48,7 +48,7 @@ async def stake_add(
4848
Args:
4949
wallet: wallet object
5050
subtensor: SubtensorInterface object
51-
netuid: the netuid to stake to (None indicates all subnets)
51+
netuids: the netuids to stake to (None indicates all subnets)
5252
stake_all: whether to stake all available balance
5353
amount: specified amount of balance to stake
5454
prompt: whether to prompt the user
@@ -233,9 +233,7 @@ async def stake_extrinsic(
233233
return True
234234

235235
netuids = (
236-
[int(netuid)]
237-
if netuid is not None
238-
else await subtensor.get_all_subnet_netuids()
236+
netuids if netuids is not None else await subtensor.get_all_subnet_netuids()
239237
)
240238

241239
hotkeys_to_stake_to = _get_hotkeys_to_stake_to(
@@ -445,10 +443,10 @@ def _prompt_stake_amount(
445443
while True:
446444
amount_input = Prompt.ask(
447445
f"\nEnter the amount to {action_name}"
448-
f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] "
449-
f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}](max: {current_balance})[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] "
446+
f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]{Balance.get_unit(netuid)}[/{COLOR_PALETTE.S.STAKE_AMOUNT}] "
447+
f"[{COLOR_PALETTE.S.STAKE_AMOUNT}](max: {current_balance})[/{COLOR_PALETTE.S.STAKE_AMOUNT}] "
450448
f"or "
451-
f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]'all'[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}] "
449+
f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]'all'[/{COLOR_PALETTE.S.STAKE_AMOUNT}] "
452450
f"for entire balance"
453451
)
454452

@@ -463,7 +461,7 @@ def _prompt_stake_amount(
463461
if amount > current_balance.tao:
464462
console.print(
465463
f"[red]Amount exceeds available balance of "
466-
f"[{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]{current_balance}[/{COLOR_PALETTE['STAKE']['STAKE_AMOUNT']}]"
464+
f"[{COLOR_PALETTE.S.STAKE_AMOUNT}]{current_balance}[/{COLOR_PALETTE.S.STAKE_AMOUNT}]"
467465
f"[/red]"
468466
)
469467
continue
@@ -542,10 +540,10 @@ def _define_stake_table(
542540
Table: An initialized rich Table object with appropriate columns
543541
"""
544542
table = Table(
545-
title=f"\n[{COLOR_PALETTE['GENERAL']['HEADER']}]Staking to:\n"
546-
f"Wallet: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.name}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}], "
547-
f"Coldkey ss58: [{COLOR_PALETTE['GENERAL']['COLDKEY']}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE['GENERAL']['COLDKEY']}]\n"
548-
f"Network: {subtensor.network}[/{COLOR_PALETTE['GENERAL']['HEADER']}]\n",
543+
title=f"\n[{COLOR_PALETTE.G.HEADER}]Staking to:\n"
544+
f"Wallet: [{COLOR_PALETTE.G.CK}]{wallet.name}[/{COLOR_PALETTE.G.CK}], "
545+
f"Coldkey ss58: [{COLOR_PALETTE.G.CK}]{wallet.coldkeypub.ss58_address}[/{COLOR_PALETTE.G.CK}]\n"
546+
f"Network: {subtensor.network}[/{COLOR_PALETTE.G.HEADER}]\n",
549547
show_footer=True,
550548
show_edge=False,
551549
header_style="bold white",
@@ -609,9 +607,13 @@ def _print_table_and_slippage(table: Table, max_slippage: float, safe_staking: b
609607

610608
# Greater than 5%
611609
if max_slippage > 5:
612-
message = f"[{COLOR_PALETTE['STAKE']['SLIPPAGE_TEXT']}]-------------------------------------------------------------------------------------------------------------------\n"
613-
message += f"[bold]WARNING:[/bold] The slippage on one of your operations is high: [{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}]{max_slippage} %[/{COLOR_PALETTE['STAKE']['SLIPPAGE_PERCENT']}], this may result in a loss of funds.\n"
614-
message += "-------------------------------------------------------------------------------------------------------------------\n"
610+
message = (
611+
f"[{COLOR_PALETTE.S.SLIPPAGE_TEXT}]" + ("-" * 115) + "\n"
612+
f"[bold]WARNING:[/bold] The slippage on one of your operations is high: "
613+
f"[{COLOR_PALETTE.S.SLIPPAGE_PERCENT}]{max_slippage} %[/{COLOR_PALETTE.S.SLIPPAGE_PERCENT}], "
614+
f"this may result in a loss of funds.\n" + ("-" * 115) + "\n"
615+
)
616+
615617
console.print(message)
616618

617619
# Table description

tests/e2e_tests/test_staking_sudo.py

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def test_staking(local_chain, wallet_setup):
3636
"""
3737
print("Testing staking and sudo commands🧪")
3838
netuid = 2
39+
multiple_netuids = [2, 3]
3940
wallet_path_alice = "//Alice"
4041

4142
# Create wallet for Alice
@@ -91,7 +92,42 @@ def test_staking(local_chain, wallet_setup):
9192
)
9293
result_output = json.loads(result.stdout)
9394
assert result_output["success"] is True
94-
assert result_output["netuid"] == 2
95+
assert result_output["netuid"] == netuid
96+
97+
# Register another subnet with sudo as Alice
98+
result_for_second_repo = exec_command_alice(
99+
command="subnets",
100+
sub_command="create",
101+
extra_args=[
102+
"--wallet-path",
103+
wallet_path_alice,
104+
"--chain",
105+
"ws://127.0.0.1:9945",
106+
"--wallet-name",
107+
wallet_alice.name,
108+
"--wallet-hotkey",
109+
wallet_alice.hotkey_str,
110+
"--subnet-name",
111+
"Test Subnet",
112+
"--repo",
113+
"https://github.com/username/repo",
114+
"--contact",
115+
116+
"--url",
117+
"https://testsubnet.com",
118+
"--discord",
119+
"alice#1234",
120+
"--description",
121+
"A test subnet for e2e testing",
122+
"--additional-info",
123+
"Created by Alice",
124+
"--no-prompt",
125+
"--json-output",
126+
],
127+
)
128+
result_output_second = json.loads(result_for_second_repo.stdout)
129+
assert result_output_second["success"] is True
130+
assert result_output_second["netuid"] == multiple_netuids[1]
95131

96132
# Register Alice in netuid = 1 using her hotkey
97133
register_subnet = exec_command_alice(
@@ -192,7 +228,7 @@ def test_staking(local_chain, wallet_setup):
192228
assert get_identity_output["additional"] == sn_add_info
193229

194230
# Add stake to Alice's hotkey
195-
add_stake = exec_command_alice(
231+
add_stake_single = exec_command_alice(
196232
command="stake",
197233
sub_command="add",
198234
extra_args=[
@@ -216,10 +252,10 @@ def test_staking(local_chain, wallet_setup):
216252
"144",
217253
],
218254
)
219-
assert "✅ Finalized" in add_stake.stdout, add_stake.stderr
255+
assert "✅ Finalized" in add_stake_single.stdout, add_stake_single.stderr
220256

221257
# Execute stake show for Alice's wallet
222-
show_stake = exec_command_alice(
258+
show_stake_adding_single = exec_command_alice(
223259
command="stake",
224260
sub_command="list",
225261
extra_args=[
@@ -235,7 +271,8 @@ def test_staking(local_chain, wallet_setup):
235271

236272
# Assert correct stake is added
237273
cleaned_stake = [
238-
re.sub(r"\s+", " ", line) for line in show_stake.stdout.splitlines()
274+
re.sub(r"\s+", " ", line)
275+
for line in show_stake_adding_single.stdout.splitlines()
239276
]
240277
stake_added = cleaned_stake[8].split("│")[3].strip().split()[0]
241278
assert Balance.from_tao(float(stake_added)) >= Balance.from_tao(90)
@@ -284,6 +321,36 @@ def test_staking(local_chain, wallet_setup):
284321
)
285322
assert "✅ Finalized" in remove_stake.stdout
286323

324+
add_stake_multiple = exec_command_alice(
325+
command="stake",
326+
sub_command="add",
327+
extra_args=[
328+
"--netuids",
329+
",".join(str(x) for x in multiple_netuids),
330+
"--wallet-path",
331+
wallet_path_alice,
332+
"--wallet-name",
333+
wallet_alice.name,
334+
"--hotkey",
335+
wallet_alice.hotkey_str,
336+
"--chain",
337+
"ws://127.0.0.1:9945",
338+
"--amount",
339+
"100",
340+
"--tolerance",
341+
"0.1",
342+
"--partial",
343+
"--no-prompt",
344+
"--era",
345+
"144",
346+
],
347+
)
348+
assert "✅ Finalized" in add_stake_multiple.stdout, add_stake_multiple.stderr
349+
for netuid_ in multiple_netuids:
350+
assert f"Stake added to netuid: {netuid_}" in add_stake_multiple.stdout, (
351+
add_stake_multiple.stderr
352+
)
353+
287354
# Fetch the hyperparameters of the subnet
288355
hyperparams = exec_command_alice(
289356
command="sudo",

0 commit comments

Comments
 (0)