Skip to content

Commit aad7521

Browse files
authored
Merge pull request #401 from opentensor/feat/associate-hotkey
Feat/associate hotkey
2 parents 58972b9 + 274de24 commit aad7521

File tree

3 files changed

+257
-2
lines changed

3 files changed

+257
-2
lines changed

bittensor_cli/cli.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,9 @@ def __init__(self):
716716
self.wallet_app.command(
717717
"new-coldkey", rich_help_panel=HELP_PANELS["WALLET"]["MANAGEMENT"]
718718
)(self.wallet_new_coldkey)
719+
self.wallet_app.command(
720+
"associate-hotkey", rich_help_panel=HELP_PANELS["WALLET"]["MANAGEMENT"]
721+
)(self.wallet_associate_hotkey)
719722
self.wallet_app.command(
720723
"create", rich_help_panel=HELP_PANELS["WALLET"]["MANAGEMENT"]
721724
)(self.wallet_create_wallet)
@@ -2265,6 +2268,74 @@ def wallet_new_hotkey(
22652268
wallets.new_hotkey(wallet, n_words, use_password, uri, overwrite)
22662269
)
22672270

2271+
def wallet_associate_hotkey(
2272+
self,
2273+
wallet_name: Optional[str] = Options.wallet_name,
2274+
wallet_path: Optional[str] = Options.wallet_path,
2275+
wallet_hotkey: Optional[str] = Options.wallet_hotkey_ss58,
2276+
network: Optional[list[str]] = Options.network,
2277+
prompt: bool = Options.prompt,
2278+
quiet: bool = Options.quiet,
2279+
verbose: bool = Options.verbose,
2280+
):
2281+
"""
2282+
Associate a hotkey with a wallet(coldkey).
2283+
2284+
USAGE
2285+
2286+
This command is used to associate a hotkey with a wallet(coldkey).
2287+
2288+
EXAMPLE
2289+
2290+
[green]$[/green] btcli wallet associate-hotkey --hotkey-name hotkey_name
2291+
[green]$[/green] btcli wallet associate-hotkey --hotkey-ss58 5DkQ4...
2292+
"""
2293+
self.verbosity_handler(quiet, verbose)
2294+
if not wallet_name:
2295+
wallet_name = Prompt.ask(
2296+
"Enter the [blue]wallet name[/blue] [dim](which you want to associate with the hotkey)[/dim]",
2297+
default=self.config.get("wallet_name") or defaults.wallet.name,
2298+
)
2299+
if not wallet_hotkey:
2300+
wallet_hotkey = Prompt.ask(
2301+
"Enter the [blue]hotkey[/blue] name or "
2302+
"[blue]hotkey ss58 address[/blue] [dim](to associate with your coldkey)[/dim]"
2303+
)
2304+
2305+
hotkey_display = None
2306+
if is_valid_ss58_address(wallet_hotkey):
2307+
hotkey_ss58 = wallet_hotkey
2308+
wallet = self.wallet_ask(
2309+
wallet_name,
2310+
wallet_path,
2311+
None,
2312+
ask_for=[WO.NAME, WO.PATH],
2313+
validate=WV.WALLET,
2314+
)
2315+
hotkey_display = (
2316+
f"hotkey [{COLORS.GENERAL.HK}]{hotkey_ss58}[/{COLORS.GENERAL.HK}]"
2317+
)
2318+
else:
2319+
wallet = self.wallet_ask(
2320+
wallet_name,
2321+
wallet_path,
2322+
wallet_hotkey,
2323+
ask_for=[WO.NAME, WO.PATH, WO.HOTKEY],
2324+
validate=WV.WALLET_AND_HOTKEY,
2325+
)
2326+
hotkey_ss58 = wallet.hotkey.ss58_address
2327+
hotkey_display = f"hotkey [blue]{wallet_hotkey}[/blue] [{COLORS.GENERAL.HK}]({hotkey_ss58})[/{COLORS.GENERAL.HK}]"
2328+
2329+
return self._run_command(
2330+
wallets.associate_hotkey(
2331+
wallet,
2332+
self.initialize_chain(network),
2333+
hotkey_ss58,
2334+
hotkey_display,
2335+
prompt,
2336+
)
2337+
)
2338+
22682339
def wallet_new_coldkey(
22692340
self,
22702341
wallet_name: Optional[str] = Options.wallet_name,

bittensor_cli/src/commands/wallets.py

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,73 @@
5050
)
5151

5252

53+
async def associate_hotkey(
54+
wallet: Wallet,
55+
subtensor: SubtensorInterface,
56+
hotkey_ss58: str,
57+
hotkey_display: str,
58+
prompt: bool = False,
59+
):
60+
"""Associates a hotkey with a wallet"""
61+
62+
owner_ss58 = await subtensor.get_hotkey_owner(hotkey_ss58)
63+
if owner_ss58:
64+
if owner_ss58 == wallet.coldkeypub.ss58_address:
65+
console.print(
66+
f":white_heavy_check_mark: {hotkey_display.capitalize()} is already "
67+
f"associated with \nwallet [blue]{wallet.name}[/blue], "
68+
f"SS58: [{COLORS.GENERAL.CK}]{owner_ss58}[/{COLORS.GENERAL.CK}]"
69+
)
70+
return True
71+
else:
72+
owner_wallet = _get_wallet_by_ss58(wallet.path, owner_ss58)
73+
wallet_name = owner_wallet.name if owner_wallet else "unknown wallet"
74+
console.print(
75+
f"[yellow]Warning[/yellow]: {hotkey_display.capitalize()} is already associated with \n"
76+
f"wallet: [blue]{wallet_name}[/blue], SS58: [{COLORS.GENERAL.CK}]{owner_ss58}[/{COLORS.GENERAL.CK}]"
77+
)
78+
return False
79+
else:
80+
console.print(
81+
f"{hotkey_display.capitalize()} is not associated with any wallet"
82+
)
83+
84+
if prompt and not Confirm.ask("Do you want to continue with the association?"):
85+
return False
86+
87+
if not unlock_key(wallet).success:
88+
return False
89+
90+
call = await subtensor.substrate.compose_call(
91+
call_module="SubtensorModule",
92+
call_function="try_associate_hotkey",
93+
call_params={
94+
"hotkey": hotkey_ss58,
95+
},
96+
)
97+
98+
with console.status(":satellite: Associating hotkey on-chain..."):
99+
success, err_msg = await subtensor.sign_and_send_extrinsic(
100+
call,
101+
wallet,
102+
wait_for_inclusion=True,
103+
wait_for_finalization=False,
104+
)
105+
106+
if not success:
107+
console.print(
108+
f"[red]:cross_mark: Failed to associate hotkey: {err_msg}[/red]"
109+
)
110+
return False
111+
112+
console.print(
113+
f":white_heavy_check_mark: Successfully associated {hotkey_display} with \n"
114+
f"wallet [blue]{wallet.name}[/blue], "
115+
f"SS58: [{COLORS.GENERAL.CK}]{wallet.coldkeypub.ss58_address}[/{COLORS.GENERAL.CK}]"
116+
)
117+
return True
118+
119+
53120
async def regen_coldkey(
54121
wallet: Wallet,
55122
mnemonic: Optional[str],
@@ -257,6 +324,15 @@ def get_coldkey_wallets_for_path(path: str) -> list[Wallet]:
257324
return wallets
258325

259326

327+
def _get_wallet_by_ss58(path: str, ss58_address: str) -> Optional[Wallet]:
328+
"""Find a wallet by its SS58 address in the given path."""
329+
ss58_addresses, wallet_names = _get_coldkey_ss58_addresses_for_path(path)
330+
for wallet_name, addr in zip(wallet_names, ss58_addresses):
331+
if addr == ss58_address:
332+
return Wallet(path=path, name=wallet_name)
333+
return None
334+
335+
260336
def _get_coldkey_ss58_addresses_for_path(path: str) -> tuple[list[str], list[str]]:
261337
"""Get all coldkey ss58 addresses from path."""
262338

@@ -1596,8 +1672,7 @@ async def find_coldkey_swap_extrinsic(
15961672
"""
15971673

15981674
current_block, genesis_block = await asyncio.gather(
1599-
subtensor.substrate.get_block_number(),
1600-
subtensor.substrate.get_block_hash(0)
1675+
subtensor.substrate.get_block_number(), subtensor.substrate.get_block_hash(0)
16011676
)
16021677
if (
16031678
current_block - start_block > 300

tests/e2e_tests/test_wallet_interactions.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,3 +510,112 @@ def test_wallet_identities(local_chain, wallet_setup):
510510
assert "Message signed successfully" in sign_using_coldkey.stdout
511511

512512
print("✅ Passed wallet set-id, get-id, sign command")
513+
514+
515+
def test_wallet_associate_hotkey(local_chain, wallet_setup):
516+
"""
517+
Test the associating hotkeys and their different cases.
518+
519+
Steps:
520+
1. Create wallets for Alice, Bob, and Charlie
521+
2. Associate a hotkey with Alice's wallet using hotkey name
522+
3. Verify the association is successful
523+
4. Try to associate Alice's hotkey with Bob's wallet (should fail)
524+
5. Try to associate Alice's hotkey again (should show already associated)
525+
6. Associate Charlie's hotkey with Bob's wallet using SS58 address
526+
527+
Raises:
528+
AssertionError: If any of the checks or verifications fail
529+
"""
530+
print("Testing wallet associate-hotkey command 🧪")
531+
532+
_, wallet_alice, wallet_path_alice, exec_command_alice = wallet_setup("//Alice")
533+
_, wallet_bob, wallet_path_bob, exec_command_bob = wallet_setup("//Bob")
534+
_, wallet_charlie, _, _ = wallet_setup("//Charlie")
535+
536+
# Associate Alice's default hotkey with her wallet
537+
result = exec_command_alice(
538+
command="wallet",
539+
sub_command="associate-hotkey",
540+
extra_args=[
541+
"--wallet-path",
542+
wallet_path_alice,
543+
"--chain",
544+
"ws://127.0.0.1:9945",
545+
"--wallet-name",
546+
wallet_alice.name,
547+
"--wallet-hotkey",
548+
wallet_alice.hotkey_str,
549+
"--no-prompt",
550+
],
551+
)
552+
553+
# Assert successful association
554+
assert "Successfully associated hotkey" in result.stdout
555+
assert wallet_alice.hotkey.ss58_address in result.stdout
556+
assert wallet_alice.coldkeypub.ss58_address in result.stdout
557+
assert wallet_alice.hotkey_str in result.stdout
558+
559+
# Try to associate Alice's hotkey with Bob's wallet (should fail)
560+
result = exec_command_bob(
561+
command="wallet",
562+
sub_command="associate-hotkey",
563+
extra_args=[
564+
"--wallet-path",
565+
wallet_path_bob,
566+
"--chain",
567+
"ws://127.0.0.1:9945",
568+
"--wallet-name",
569+
wallet_bob.name,
570+
"--hotkey-ss58",
571+
wallet_alice.hotkey.ss58_address,
572+
"--no-prompt",
573+
],
574+
)
575+
576+
assert "Warning" in result.stdout
577+
assert "is already associated with" in result.stdout
578+
579+
# Try to associate Alice's hotkey again with Alice's wallet
580+
result = exec_command_alice(
581+
command="wallet",
582+
sub_command="associate-hotkey",
583+
extra_args=[
584+
"--wallet-path",
585+
wallet_path_alice,
586+
"--chain",
587+
"ws://127.0.0.1:9945",
588+
"--wallet-name",
589+
wallet_alice.name,
590+
"--wallet-hotkey",
591+
wallet_alice.hotkey_str,
592+
"--no-prompt",
593+
],
594+
)
595+
596+
assert "is already associated with" in result.stdout
597+
assert "wallet" in result.stdout
598+
assert wallet_alice.name in result.stdout
599+
600+
# Associate Charlie's hotkey with Bob's wallet using SS58 address
601+
result = exec_command_bob(
602+
command="wallet",
603+
sub_command="associate-hotkey",
604+
extra_args=[
605+
"--wallet-path",
606+
wallet_path_bob,
607+
"--chain",
608+
"ws://127.0.0.1:9945",
609+
"--wallet-name",
610+
wallet_bob.name,
611+
"--hotkey-ss58",
612+
wallet_charlie.hotkey.ss58_address,
613+
"--no-prompt",
614+
],
615+
)
616+
617+
assert "Successfully associated hotkey" in result.stdout
618+
assert wallet_charlie.hotkey.ss58_address in result.stdout
619+
assert wallet_bob.coldkeypub.ss58_address in result.stdout
620+
621+
print("✅ Passed wallet associate-hotkey command")

0 commit comments

Comments
 (0)