htcli 1.1.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.
- htcli-1.1.0.dist-info/METADATA +509 -0
- htcli-1.1.0.dist-info/RECORD +140 -0
- htcli-1.1.0.dist-info/WHEEL +4 -0
- htcli-1.1.0.dist-info/entry_points.txt +2 -0
- htcli-1.1.0.dist-info/licenses/LICENSE +21 -0
- src/__init__.py +0 -0
- src/htcli/__init__.py +5 -0
- src/htcli/client/__init__.py +338 -0
- src/htcli/client/extrinsics/__init__.py +26 -0
- src/htcli/client/extrinsics/base.py +487 -0
- src/htcli/client/extrinsics/consensus.py +79 -0
- src/htcli/client/extrinsics/governance.py +714 -0
- src/htcli/client/extrinsics/identity.py +490 -0
- src/htcli/client/extrinsics/node.py +1054 -0
- src/htcli/client/extrinsics/overwatch.py +401 -0
- src/htcli/client/extrinsics/staking.py +1504 -0
- src/htcli/client/extrinsics/subnet.py +2218 -0
- src/htcli/client/extrinsics/validator.py +203 -0
- src/htcli/client/extrinsics/wallet.py +323 -0
- src/htcli/client/offchain/__init__.py +10 -0
- src/htcli/client/offchain/backup.py +385 -0
- src/htcli/client/offchain/config.py +541 -0
- src/htcli/client/offchain/wallet.py +839 -0
- src/htcli/client/rpc/__init__.py +20 -0
- src/htcli/client/rpc/chain.py +568 -0
- src/htcli/client/rpc/node.py +783 -0
- src/htcli/client/rpc/overwatch.py +680 -0
- src/htcli/client/rpc/staking.py +216 -0
- src/htcli/client/rpc/subnet.py +2104 -0
- src/htcli/client/rpc/wallet.py +912 -0
- src/htcli/commands/__init__.py +31 -0
- src/htcli/commands/chain/__init__.py +66 -0
- src/htcli/commands/chain/display.py +204 -0
- src/htcli/commands/chain/handlers.py +260 -0
- src/htcli/commands/config/__init__.py +158 -0
- src/htcli/commands/config/display.py +353 -0
- src/htcli/commands/config/handlers.py +347 -0
- src/htcli/commands/config/prompts.py +357 -0
- src/htcli/commands/consensus/__init__.py +61 -0
- src/htcli/commands/consensus/handlers.py +100 -0
- src/htcli/commands/governance/__init__.py +49 -0
- src/htcli/commands/governance/handlers.py +81 -0
- src/htcli/commands/node/__init__.py +304 -0
- src/htcli/commands/node/display.py +749 -0
- src/htcli/commands/node/error_handling.py +470 -0
- src/htcli/commands/node/handlers.py +844 -0
- src/htcli/commands/node/prompts.py +346 -0
- src/htcli/commands/overwatch/__init__.py +219 -0
- src/htcli/commands/overwatch/display.py +396 -0
- src/htcli/commands/overwatch/error_handling.py +276 -0
- src/htcli/commands/overwatch/handlers.py +443 -0
- src/htcli/commands/overwatch/prompts.py +359 -0
- src/htcli/commands/stake/__init__.py +736 -0
- src/htcli/commands/stake/display.py +1103 -0
- src/htcli/commands/stake/error_handling.py +425 -0
- src/htcli/commands/stake/handlers.py +1902 -0
- src/htcli/commands/stake/prompts.py +1080 -0
- src/htcli/commands/subnet/__init__.py +639 -0
- src/htcli/commands/subnet/display.py +801 -0
- src/htcli/commands/subnet/error_handling.py +524 -0
- src/htcli/commands/subnet/handlers.py +2855 -0
- src/htcli/commands/subnet/prompts.py +1225 -0
- src/htcli/commands/validator/__init__.py +192 -0
- src/htcli/commands/validator/display.py +54 -0
- src/htcli/commands/validator/handlers.py +340 -0
- src/htcli/commands/wallet/__init__.py +546 -0
- src/htcli/commands/wallet/display.py +806 -0
- src/htcli/commands/wallet/error_handling.py +210 -0
- src/htcli/commands/wallet/handlers.py +3040 -0
- src/htcli/commands/wallet/prompts.py +1518 -0
- src/htcli/config.py +184 -0
- src/htcli/dependencies.py +186 -0
- src/htcli/errors/__init__.py +63 -0
- src/htcli/errors/base.py +141 -0
- src/htcli/errors/display.py +20 -0
- src/htcli/errors/handlers.py +710 -0
- src/htcli/main.py +343 -0
- src/htcli/models/__init__.py +21 -0
- src/htcli/models/enums/enum_types.py +35 -0
- src/htcli/models/errors.py +103 -0
- src/htcli/models/requests/__init__.py +197 -0
- src/htcli/models/requests/config.py +70 -0
- src/htcli/models/requests/consensus.py +19 -0
- src/htcli/models/requests/governance.py +38 -0
- src/htcli/models/requests/identity.py +51 -0
- src/htcli/models/requests/key.py +22 -0
- src/htcli/models/requests/node.py +91 -0
- src/htcli/models/requests/overwatch.py +64 -0
- src/htcli/models/requests/staking.py +580 -0
- src/htcli/models/requests/subnet.py +195 -0
- src/htcli/models/requests/validator.py +139 -0
- src/htcli/models/requests/wallet.py +118 -0
- src/htcli/models/responses/__init__.py +147 -0
- src/htcli/models/responses/base.py +18 -0
- src/htcli/models/responses/chain.py +39 -0
- src/htcli/models/responses/config.py +58 -0
- src/htcli/models/responses/identity.py +102 -0
- src/htcli/models/responses/overwatch.py +51 -0
- src/htcli/models/responses/staking.py +502 -0
- src/htcli/models/responses/subnet.py +856 -0
- src/htcli/models/responses/wallet.py +185 -0
- src/htcli/ui/__init__.py +87 -0
- src/htcli/ui/colors.py +309 -0
- src/htcli/ui/components/__init__.py +60 -0
- src/htcli/ui/components/panels.py +174 -0
- src/htcli/ui/components/progress.py +166 -0
- src/htcli/ui/components/spinners.py +92 -0
- src/htcli/ui/components/tables.py +809 -0
- src/htcli/ui/components/trees.py +721 -0
- src/htcli/ui/display.py +336 -0
- src/htcli/ui/prompts.py +870 -0
- src/htcli/utils/__init__.py +76 -0
- src/htcli/utils/blockchain/__init__.py +75 -0
- src/htcli/utils/blockchain/formatting.py +368 -0
- src/htcli/utils/blockchain/patches.py +286 -0
- src/htcli/utils/blockchain/peer_id.py +186 -0
- src/htcli/utils/blockchain/staking.py +448 -0
- src/htcli/utils/blockchain/type_registry.py +1373 -0
- src/htcli/utils/blockchain/validation.py +179 -0
- src/htcli/utils/cache.py +613 -0
- src/htcli/utils/constants.py +38 -0
- src/htcli/utils/legacy/__init__.py +12 -0
- src/htcli/utils/legacy/colors.py +311 -0
- src/htcli/utils/legacy/crypto.py +1176 -0
- src/htcli/utils/legacy/formatting.py +452 -0
- src/htcli/utils/legacy/interactive.py +306 -0
- src/htcli/utils/legacy/subnet_manifest.py +265 -0
- src/htcli/utils/legacy/validation.py +488 -0
- src/htcli/utils/logging.py +183 -0
- src/htcli/utils/network/__init__.py +20 -0
- src/htcli/utils/network/subnet.py +344 -0
- src/htcli/utils/prompts.py +27 -0
- src/htcli/utils/scale_codec.py +155 -0
- src/htcli/utils/validation/__init__.py +57 -0
- src/htcli/utils/validation/prompt_validators.py +267 -0
- src/htcli/utils/wallet/__init__.py +65 -0
- src/htcli/utils/wallet/auth.py +151 -0
- src/htcli/utils/wallet/core.py +1069 -0
- src/htcli/utils/wallet/crypto.py +1615 -0
- src/htcli/utils/wallet/migration.py +159 -0
|
@@ -0,0 +1,1518 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wallet command prompting logic.
|
|
3
|
+
|
|
4
|
+
Handles user interaction and input validation for wallet operations.
|
|
5
|
+
Uses HTCLI UI components and Pydantic models for proper validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from ...models.requests import (
|
|
12
|
+
PreseededWalletRequest,
|
|
13
|
+
WalletCreateRequest,
|
|
14
|
+
WalletDeleteRequest,
|
|
15
|
+
WalletRestoreRequest,
|
|
16
|
+
WalletTransferRequest,
|
|
17
|
+
WalletUpdateRequest,
|
|
18
|
+
)
|
|
19
|
+
from ...errors.base import AmbiguousWalletError
|
|
20
|
+
from ...ui.colors import error, info, warning
|
|
21
|
+
from ...ui.components import HTCLITable
|
|
22
|
+
from ...ui.display import HTCLIConsole
|
|
23
|
+
from ...ui.prompts import (
|
|
24
|
+
HTCLIPrompt,
|
|
25
|
+
amount_prompt,
|
|
26
|
+
confirm_prompt,
|
|
27
|
+
integer_prompt,
|
|
28
|
+
password_prompt,
|
|
29
|
+
prompt_for_optional,
|
|
30
|
+
prompt_for_required,
|
|
31
|
+
select_prompt,
|
|
32
|
+
text_prompt,
|
|
33
|
+
)
|
|
34
|
+
from ...utils.blockchain.validation import validate_wallet_name
|
|
35
|
+
from ...utils.validation import validate_wallet_name_prompt
|
|
36
|
+
from ...utils.wallet.crypto import (
|
|
37
|
+
_find_wallet_entries,
|
|
38
|
+
format_address_display,
|
|
39
|
+
get_wallet_info_by_name,
|
|
40
|
+
list_keys,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
console = HTCLIConsole()
|
|
44
|
+
prompt = HTCLIPrompt()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def validate_password_strength(password: str) -> bool:
|
|
48
|
+
"""Validate password strength."""
|
|
49
|
+
if len(password) < 8:
|
|
50
|
+
console.print(error("Password must be at least 8 characters long."))
|
|
51
|
+
return False
|
|
52
|
+
if not re.search(r"[A-Z]", password):
|
|
53
|
+
console.print(error("Password must contain at least one uppercase letter."))
|
|
54
|
+
return False
|
|
55
|
+
if not re.search(r"[a-z]", password):
|
|
56
|
+
console.print(error("Password must contain at least one lowercase letter."))
|
|
57
|
+
return False
|
|
58
|
+
if not re.search(r"\d", password):
|
|
59
|
+
console.print(error("Password must contain at least one digit."))
|
|
60
|
+
return False
|
|
61
|
+
if not re.search(r"[!@#$%^&*(),.?\":{}|<>]", password):
|
|
62
|
+
console.print(error("Password must contain at least one special character."))
|
|
63
|
+
return False
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def prompt_wallet_creation(
|
|
68
|
+
name: Optional[str] = None,
|
|
69
|
+
key_type: Optional[str] = None,
|
|
70
|
+
wallet_type: str = "coldkey",
|
|
71
|
+
coldkey_name: Optional[str] = None,
|
|
72
|
+
password: Optional[str] = None,
|
|
73
|
+
) -> WalletCreateRequest:
|
|
74
|
+
"""Collect wallet creation parameters from user."""
|
|
75
|
+
|
|
76
|
+
# Validate wallet name if provided
|
|
77
|
+
if name is not None:
|
|
78
|
+
is_valid, error_msg = validate_wallet_name_prompt(name)
|
|
79
|
+
if not is_valid:
|
|
80
|
+
console.print(error(f"Invalid wallet name: {error_msg}"))
|
|
81
|
+
name = None
|
|
82
|
+
|
|
83
|
+
# Get wallet name and check for existence
|
|
84
|
+
if not name:
|
|
85
|
+
if wallet_type == "hotkey":
|
|
86
|
+
# For hotkeys, we'll check uniqueness after we know the coldkey
|
|
87
|
+
# So we just validate the name format here
|
|
88
|
+
prompt_text = f"{wallet_type.capitalize()} name"
|
|
89
|
+
name = text_prompt(
|
|
90
|
+
prompt_text,
|
|
91
|
+
validator=validate_wallet_name,
|
|
92
|
+
error_message="Invalid wallet name. Use alphanumeric characters, hyphens, and underscores only.",
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
# For coldkeys, check against all existing wallets
|
|
96
|
+
existing_wallets = [w["name"] for w in list_keys()]
|
|
97
|
+
while True:
|
|
98
|
+
prompt_text = (
|
|
99
|
+
f"{wallet_type.capitalize()} name"
|
|
100
|
+
if wallet_type in ["hotkey", "coldkey"]
|
|
101
|
+
else "Wallet name"
|
|
102
|
+
)
|
|
103
|
+
name = text_prompt(
|
|
104
|
+
prompt_text,
|
|
105
|
+
validator=validate_wallet_name,
|
|
106
|
+
error_message="Invalid wallet name. Use alphanumeric characters, hyphens, and underscores only.",
|
|
107
|
+
)
|
|
108
|
+
if name in existing_wallets:
|
|
109
|
+
console.print(
|
|
110
|
+
error(
|
|
111
|
+
f"Wallet with name '{name}' already exists. Please choose another name."
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
# Set key type to ECDSA for HyperTensor EVM compatibility
|
|
118
|
+
if not key_type:
|
|
119
|
+
key_type = "ecdsa"
|
|
120
|
+
|
|
121
|
+
# Get coldkey address for hotkeys
|
|
122
|
+
if wallet_type == "hotkey" and not coldkey_name:
|
|
123
|
+
coldkeys = [w for w in list_keys() if not w.get("is_hotkey")]
|
|
124
|
+
if not coldkeys:
|
|
125
|
+
raise ValueError("No coldkeys found. Please create a coldkey first.")
|
|
126
|
+
|
|
127
|
+
console.print(info("Available coldkeys:"))
|
|
128
|
+
table = HTCLITable(title="Coldkeys")
|
|
129
|
+
table.add_column("Index", style="white")
|
|
130
|
+
table.add_column("Name", style="white")
|
|
131
|
+
table.add_column("Encrypted", style="dim")
|
|
132
|
+
for i, w in enumerate(coldkeys):
|
|
133
|
+
encrypted_status = "✅ Yes" if w.get("is_encrypted") else "❌ No"
|
|
134
|
+
table.add_row(str(i + 1), w["name"], encrypted_status)
|
|
135
|
+
console.print(table.table)
|
|
136
|
+
|
|
137
|
+
choice = prompt_for_required(
|
|
138
|
+
"Select coldkey", int, f"Enter a number from 1 to {len(coldkeys)}"
|
|
139
|
+
)
|
|
140
|
+
coldkey_name = coldkeys[choice - 1]["name"]
|
|
141
|
+
|
|
142
|
+
# For hotkeys, check if the selected coldkey already has a hotkey with this name
|
|
143
|
+
if wallet_type == "hotkey" and coldkey_name:
|
|
144
|
+
from ...utils.wallet.crypto import coldkey_has_hotkey, get_wallet_info_by_name
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
coldkey_info = get_wallet_info_by_name(coldkey_name, is_hotkey=False)
|
|
148
|
+
coldkey_address = coldkey_info.get("ss58_address") or coldkey_info.get(
|
|
149
|
+
"address"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if coldkey_has_hotkey(coldkey_address, name):
|
|
153
|
+
console.print(
|
|
154
|
+
error(
|
|
155
|
+
f"Coldkey '{coldkey_name}' already has a hotkey named '{name}'. "
|
|
156
|
+
"Please choose a different name."
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
# Re-prompt for name
|
|
160
|
+
while True:
|
|
161
|
+
name = text_prompt(
|
|
162
|
+
f"{wallet_type.capitalize()} name",
|
|
163
|
+
validator=validate_wallet_name,
|
|
164
|
+
error_message="Invalid wallet name. Use alphanumeric characters, hyphens, and underscores only.",
|
|
165
|
+
)
|
|
166
|
+
if not coldkey_has_hotkey(coldkey_address, name):
|
|
167
|
+
break
|
|
168
|
+
console.print(
|
|
169
|
+
error(
|
|
170
|
+
f"Coldkey '{coldkey_name}' already has a hotkey named '{name}'. "
|
|
171
|
+
"Please choose a different name."
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
except FileNotFoundError:
|
|
175
|
+
# Coldkey not found, will be handled later
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
# Get optional password
|
|
179
|
+
# If password is empty string, it means explicitly no password (from --no-password flag)
|
|
180
|
+
# If password is None, prompt the user
|
|
181
|
+
if password is None:
|
|
182
|
+
if confirm_prompt("Encrypt wallet with password?", default=True):
|
|
183
|
+
while True:
|
|
184
|
+
password = password_prompt("Enter password", confirm=True)
|
|
185
|
+
if validate_password_strength(password):
|
|
186
|
+
break
|
|
187
|
+
elif password == "":
|
|
188
|
+
# Explicitly no password (from --no-password flag), set to None for storage
|
|
189
|
+
password = None
|
|
190
|
+
|
|
191
|
+
return WalletCreateRequest(
|
|
192
|
+
name=name,
|
|
193
|
+
key_type=key_type,
|
|
194
|
+
wallet_type=wallet_type,
|
|
195
|
+
owner_address=coldkey_name,
|
|
196
|
+
password=password,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def prompt_wallet_restoration(
|
|
201
|
+
name: Optional[str] = None,
|
|
202
|
+
private_key: Optional[str] = None,
|
|
203
|
+
mnemonic: Optional[str] = None,
|
|
204
|
+
key_type: Optional[str] = None,
|
|
205
|
+
wallet_type: str = "coldkey",
|
|
206
|
+
coldkey_name: Optional[str] = None,
|
|
207
|
+
password: Optional[str] = None,
|
|
208
|
+
) -> WalletRestoreRequest:
|
|
209
|
+
"""Collect wallet restoration parameters from user."""
|
|
210
|
+
|
|
211
|
+
# Get wallet name
|
|
212
|
+
if not name:
|
|
213
|
+
prompt_text = "Hotkey name" if wallet_type == "hotkey" else "Wallet name"
|
|
214
|
+
name = text_prompt(prompt_text, validator=validate_wallet_name)
|
|
215
|
+
|
|
216
|
+
# Get restoration method
|
|
217
|
+
if not private_key and not mnemonic:
|
|
218
|
+
method = select_prompt(
|
|
219
|
+
"Restoration method",
|
|
220
|
+
choices=[
|
|
221
|
+
("mnemonic", "Mnemonic phrase (12 or 24 words)", "mnemonic"),
|
|
222
|
+
("private_key", "Private key (64-character hex)", "private_key"),
|
|
223
|
+
],
|
|
224
|
+
default="mnemonic",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
if method == "mnemonic":
|
|
228
|
+
mnemonic = text_prompt("Enter mnemonic phrase")
|
|
229
|
+
else:
|
|
230
|
+
private_key = text_prompt(
|
|
231
|
+
"Enter private key (64 hex characters)",
|
|
232
|
+
validator=lambda x: len(x) == 64
|
|
233
|
+
and all(c in "0123456789abcdefABCDEF" for c in x),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Get key type
|
|
237
|
+
if not key_type:
|
|
238
|
+
key_type = select_prompt(
|
|
239
|
+
"Select key type",
|
|
240
|
+
choices=[
|
|
241
|
+
("ecdsa", "ECDSA", "ecdsa"),
|
|
242
|
+
("sr25519", "SR25519", "sr25519"),
|
|
243
|
+
("ed25519", "Ed25519", "ed25519"),
|
|
244
|
+
],
|
|
245
|
+
default="ecdsa",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# Get coldkey for hotkeys
|
|
249
|
+
if wallet_type == "hotkey" and not coldkey_name:
|
|
250
|
+
coldkeys = [w for w in list_keys() if not w.get("is_hotkey")]
|
|
251
|
+
if not coldkeys:
|
|
252
|
+
raise ValueError(
|
|
253
|
+
"No coldkeys found. Please create a coldkey first to assign as coldkey."
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
console.print(info("Available coldkeys:"))
|
|
257
|
+
table = HTCLITable(title="Coldkeys")
|
|
258
|
+
table.add_column("Index", style="htcli.value")
|
|
259
|
+
table.add_column("Name", style="htcli.value")
|
|
260
|
+
table.add_column("Encrypted", style="htcli.subtitle")
|
|
261
|
+
for i, w in enumerate(coldkeys):
|
|
262
|
+
encrypted_status = "✅ Yes" if w.get("is_encrypted") else "❌ No"
|
|
263
|
+
table.add_row(str(i + 1), w["name"], encrypted_status)
|
|
264
|
+
table.render()
|
|
265
|
+
|
|
266
|
+
choice = prompt_for_required(
|
|
267
|
+
"Select coldkey for the restored hotkey",
|
|
268
|
+
int,
|
|
269
|
+
f"Enter a number from 1 to {len(coldkeys)}",
|
|
270
|
+
)
|
|
271
|
+
coldkey_name = coldkeys[choice - 1]["name"]
|
|
272
|
+
|
|
273
|
+
# Get optional password
|
|
274
|
+
if password is None:
|
|
275
|
+
if confirm_prompt("Encrypt wallet with password?", default=True):
|
|
276
|
+
while True:
|
|
277
|
+
password = password_prompt("Enter password", confirm=True)
|
|
278
|
+
if validate_password_strength(password):
|
|
279
|
+
break
|
|
280
|
+
|
|
281
|
+
return WalletRestoreRequest(
|
|
282
|
+
name=name,
|
|
283
|
+
private_key=private_key,
|
|
284
|
+
mnemonic=mnemonic,
|
|
285
|
+
key_type=key_type,
|
|
286
|
+
wallet_type=wallet_type,
|
|
287
|
+
owner_address=coldkey_name,
|
|
288
|
+
password=password,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def prompt_preseeded_wallet(
|
|
293
|
+
preseeded_name: str,
|
|
294
|
+
custom_name: Optional[str] = None,
|
|
295
|
+
key_type: Optional[str] = None,
|
|
296
|
+
password: Optional[str] = None,
|
|
297
|
+
) -> PreseededWalletRequest:
|
|
298
|
+
"""Collect preseeded wallet import parameters."""
|
|
299
|
+
|
|
300
|
+
# Get custom name if not provided
|
|
301
|
+
if not custom_name:
|
|
302
|
+
custom_name = prompt_for_optional(
|
|
303
|
+
"Custom wallet name", str, f"Leave empty to use '{preseeded_name}'"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Get key type with default
|
|
307
|
+
if not key_type:
|
|
308
|
+
key_type = "ecdsa" # Preseeded wallets default to ECDSA
|
|
309
|
+
|
|
310
|
+
# Get optional password
|
|
311
|
+
if password is None:
|
|
312
|
+
if confirm_prompt("Encrypt wallet with password?", default=False):
|
|
313
|
+
password = password_prompt("Enter password", confirm=True, min_length=8)
|
|
314
|
+
|
|
315
|
+
return PreseededWalletRequest(
|
|
316
|
+
preseeded_name=preseeded_name,
|
|
317
|
+
custom_name=custom_name,
|
|
318
|
+
key_type=key_type,
|
|
319
|
+
password=password,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def prompt_wallet_update(
|
|
324
|
+
current_name: Optional[str] = None,
|
|
325
|
+
new_name: Optional[str] = None,
|
|
326
|
+
new_password: Optional[str] = None,
|
|
327
|
+
remove_password: Optional[bool] = None,
|
|
328
|
+
new_coldkey: Optional[str] = None,
|
|
329
|
+
is_hotkey: bool = False,
|
|
330
|
+
coldkey_name: Optional[str] = None,
|
|
331
|
+
) -> WalletUpdateRequest:
|
|
332
|
+
"""Collect wallet update parameters."""
|
|
333
|
+
|
|
334
|
+
# Initialize disambiguation variables for hotkeys
|
|
335
|
+
owner_address = None
|
|
336
|
+
owner_coldkey_name = None
|
|
337
|
+
|
|
338
|
+
# Get current wallet name
|
|
339
|
+
if not current_name:
|
|
340
|
+
# Display list of existing wallets for user to choose from
|
|
341
|
+
console.print(info("📋 Available Wallets"))
|
|
342
|
+
|
|
343
|
+
all_keys = list_keys()
|
|
344
|
+
filtered_keys = [k for k in all_keys if k.get("is_hotkey", False) == is_hotkey]
|
|
345
|
+
|
|
346
|
+
if not filtered_keys:
|
|
347
|
+
wallet_type = "hotkeys" if is_hotkey else "coldkeys"
|
|
348
|
+
console.print(warning(f"No {wallet_type} found."))
|
|
349
|
+
raise ValueError(f"No {wallet_type} available to update")
|
|
350
|
+
|
|
351
|
+
# Display wallets in a table
|
|
352
|
+
table = HTCLITable(
|
|
353
|
+
title=f"Available {'Hotkeys' if is_hotkey else 'Coldkeys'}",
|
|
354
|
+
border_style="blue",
|
|
355
|
+
header_style="bold cyan",
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
if is_hotkey:
|
|
359
|
+
table.add_column("Index", style="bold white", width=6)
|
|
360
|
+
table.add_column("Name", style="bold cyan", width=25)
|
|
361
|
+
table.add_column("Coldkey", style="bold yellow", width=25)
|
|
362
|
+
table.add_column("Address", style="white", width=30)
|
|
363
|
+
table.add_column("Encrypted", style="dim", width=10)
|
|
364
|
+
else:
|
|
365
|
+
table.add_column("Index", style="bold white", width=8)
|
|
366
|
+
table.add_column("Name", style="bold cyan", width=30)
|
|
367
|
+
table.add_column("Address", style="white", width=40)
|
|
368
|
+
table.add_column("Encrypted", style="dim", width=12)
|
|
369
|
+
|
|
370
|
+
for i, wallet in enumerate(filtered_keys):
|
|
371
|
+
encrypted_status = "🔒 Yes" if wallet.get("is_encrypted", False) else "No"
|
|
372
|
+
# Format address in MetaMask style: 0x<first 5 chars>...<last 5 chars>
|
|
373
|
+
address = (
|
|
374
|
+
wallet.get("ss58_address")
|
|
375
|
+
or wallet.get("evm_address")
|
|
376
|
+
or wallet.get("address", "N/A")
|
|
377
|
+
)
|
|
378
|
+
if address != "N/A":
|
|
379
|
+
# Use the format_address_display function for consistent MetaMask-style formatting
|
|
380
|
+
address_display = format_address_display(address)
|
|
381
|
+
else:
|
|
382
|
+
address_display = address
|
|
383
|
+
|
|
384
|
+
if is_hotkey:
|
|
385
|
+
# Try to resolve coldkey name from coldkey address
|
|
386
|
+
coldkey_name_display = "Unknown"
|
|
387
|
+
coldkey_address = wallet.get("owner_address")
|
|
388
|
+
if coldkey_address:
|
|
389
|
+
# Try to find the coldkey with this address
|
|
390
|
+
for key in all_keys:
|
|
391
|
+
if key.get("ss58_address") == coldkey_address and not key.get(
|
|
392
|
+
"is_hotkey", False
|
|
393
|
+
):
|
|
394
|
+
coldkey_name_display = key.get(
|
|
395
|
+
"name", coldkey_address[:20] + "..."
|
|
396
|
+
)
|
|
397
|
+
break
|
|
398
|
+
else:
|
|
399
|
+
coldkey_name_display = coldkey_address[:20] + "..."
|
|
400
|
+
|
|
401
|
+
table.add_row(
|
|
402
|
+
str(i + 1),
|
|
403
|
+
wallet.get("name", "N/A"),
|
|
404
|
+
coldkey_name_display,
|
|
405
|
+
address_display,
|
|
406
|
+
encrypted_status,
|
|
407
|
+
)
|
|
408
|
+
else:
|
|
409
|
+
table.add_row(
|
|
410
|
+
str(i + 1),
|
|
411
|
+
wallet.get("name", "N/A"),
|
|
412
|
+
address_display,
|
|
413
|
+
encrypted_status,
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
console.print(table.table)
|
|
417
|
+
console.print()
|
|
418
|
+
|
|
419
|
+
# Let user select by index
|
|
420
|
+
choice = prompt_for_required(
|
|
421
|
+
f"Select wallet to update (1-{len(filtered_keys)})",
|
|
422
|
+
int,
|
|
423
|
+
f"Enter a number from 1 to {len(filtered_keys)}",
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
if not (1 <= choice <= len(filtered_keys)):
|
|
427
|
+
raise ValueError(
|
|
428
|
+
f"Invalid selection. Please choose between 1 and {len(filtered_keys)}"
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
selected_wallet = filtered_keys[choice - 1]
|
|
432
|
+
current_name = selected_wallet["name"]
|
|
433
|
+
|
|
434
|
+
# For hotkeys, extract coldkey info from the selected wallet
|
|
435
|
+
if is_hotkey and not coldkey_name:
|
|
436
|
+
coldkey_address = selected_wallet.get("owner_address")
|
|
437
|
+
if coldkey_address:
|
|
438
|
+
# Find the coldkey name from the owner address
|
|
439
|
+
for key in all_keys:
|
|
440
|
+
if key.get("ss58_address") == coldkey_address and not key.get(
|
|
441
|
+
"is_hotkey", False
|
|
442
|
+
):
|
|
443
|
+
coldkey_name = key.get("name")
|
|
444
|
+
break
|
|
445
|
+
else:
|
|
446
|
+
# If we can't find the coldkey name, we'll need to ask
|
|
447
|
+
# But try to get it from the wallet info
|
|
448
|
+
try:
|
|
449
|
+
wallet_info = get_wallet_info_by_name(
|
|
450
|
+
current_name, is_hotkey=True
|
|
451
|
+
)
|
|
452
|
+
# Try to find coldkey by address
|
|
453
|
+
for key in all_keys:
|
|
454
|
+
if key.get("ss58_address") == wallet_info.get(
|
|
455
|
+
"owner_address"
|
|
456
|
+
) and not key.get("is_hotkey", False):
|
|
457
|
+
coldkey_name = key.get("name")
|
|
458
|
+
break
|
|
459
|
+
except FileNotFoundError:
|
|
460
|
+
pass
|
|
461
|
+
|
|
462
|
+
# If we have coldkey_name, set owner_address and owner_coldkey_name for disambiguation
|
|
463
|
+
if is_hotkey and coldkey_name:
|
|
464
|
+
try:
|
|
465
|
+
coldkey_info = get_wallet_info_by_name(coldkey_name, is_hotkey=False)
|
|
466
|
+
owner_address = (
|
|
467
|
+
coldkey_info.get("evm_address")
|
|
468
|
+
or coldkey_info.get("ss58_address")
|
|
469
|
+
or coldkey_info.get("address")
|
|
470
|
+
)
|
|
471
|
+
owner_coldkey_name = coldkey_name
|
|
472
|
+
except FileNotFoundError:
|
|
473
|
+
pass
|
|
474
|
+
else:
|
|
475
|
+
# Validate that the provided wallet name exists
|
|
476
|
+
# For hotkeys, use coldkey_name to disambiguate if provided
|
|
477
|
+
# If coldkey_name is NOT provided and wallet is ambiguous, prompt user
|
|
478
|
+
|
|
479
|
+
if is_hotkey and coldkey_name:
|
|
480
|
+
# Get the coldkey address to use for disambiguation
|
|
481
|
+
try:
|
|
482
|
+
coldkey_info = get_wallet_info_by_name(coldkey_name, is_hotkey=False)
|
|
483
|
+
# Prefer evm_address for ECDSA keys (Hypertensor EVM uses ECDSA)
|
|
484
|
+
# But also check ss58_address and address fields
|
|
485
|
+
owner_address = (
|
|
486
|
+
coldkey_info.get("evm_address")
|
|
487
|
+
or coldkey_info.get("ss58_address")
|
|
488
|
+
or coldkey_info.get("address")
|
|
489
|
+
)
|
|
490
|
+
if not owner_address:
|
|
491
|
+
raise ValueError(
|
|
492
|
+
f"Could not determine address for coldkey '{coldkey_name}'"
|
|
493
|
+
)
|
|
494
|
+
owner_coldkey_name = coldkey_name
|
|
495
|
+
except FileNotFoundError:
|
|
496
|
+
raise ValueError(
|
|
497
|
+
f"Coldkey '{coldkey_name}' not found. Please check the coldkey name."
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
# Try to get wallet info with disambiguation
|
|
501
|
+
try:
|
|
502
|
+
wallet_info = get_wallet_info_by_name(
|
|
503
|
+
current_name,
|
|
504
|
+
is_hotkey=is_hotkey,
|
|
505
|
+
owner_address=owner_address,
|
|
506
|
+
owner_coldkey_name=owner_coldkey_name,
|
|
507
|
+
)
|
|
508
|
+
except AmbiguousWalletError as e:
|
|
509
|
+
# FIX: Handle ambiguity by prompting user to select owner coldkey
|
|
510
|
+
if is_hotkey and len(e.matches) > 1:
|
|
511
|
+
console.print(
|
|
512
|
+
warning(
|
|
513
|
+
f"Found {len(e.matches)} hotkeys named '{current_name}'. Please select which one to update:"
|
|
514
|
+
)
|
|
515
|
+
)
|
|
516
|
+
table = HTCLITable(title="Select Hotkey")
|
|
517
|
+
table.add_column("Index", style="bold white", width=6)
|
|
518
|
+
table.add_column("Coldkey", style="bold yellow", width=25)
|
|
519
|
+
table.add_column("Address", style="white", width=30)
|
|
520
|
+
|
|
521
|
+
for i, match in enumerate(e.matches):
|
|
522
|
+
owner_ck = match.get("owner_coldkey_name", "Unknown")
|
|
523
|
+
address = (
|
|
524
|
+
match.get("evm_address")
|
|
525
|
+
or match.get("ss58_address")
|
|
526
|
+
or match.get("address", "N/A")
|
|
527
|
+
)
|
|
528
|
+
if address != "N/A":
|
|
529
|
+
address_display = format_address_display(address)
|
|
530
|
+
else:
|
|
531
|
+
address_display = address
|
|
532
|
+
table.add_row(str(i + 1), owner_ck, address_display)
|
|
533
|
+
|
|
534
|
+
console.print(table.table)
|
|
535
|
+
console.print()
|
|
536
|
+
|
|
537
|
+
choice = prompt_for_required(
|
|
538
|
+
f"Select hotkey to update (1-{len(e.matches)})",
|
|
539
|
+
int,
|
|
540
|
+
f"Enter a number from 1 to {len(e.matches)}",
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
if not (1 <= choice <= len(e.matches)):
|
|
544
|
+
raise ValueError(
|
|
545
|
+
f"Invalid selection. Please choose between 1 and {len(e.matches)}"
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
selected_hotkey = e.matches[choice - 1]
|
|
549
|
+
# Update coldkey_name from the selected hotkey
|
|
550
|
+
coldkey_name = selected_hotkey.get("owner_coldkey_name")
|
|
551
|
+
if coldkey_name:
|
|
552
|
+
# Get the coldkey address for disambiguation
|
|
553
|
+
try:
|
|
554
|
+
coldkey_info = get_wallet_info_by_name(
|
|
555
|
+
coldkey_name, is_hotkey=False
|
|
556
|
+
)
|
|
557
|
+
owner_address = (
|
|
558
|
+
coldkey_info.get("evm_address")
|
|
559
|
+
or coldkey_info.get("ss58_address")
|
|
560
|
+
or coldkey_info.get("address")
|
|
561
|
+
)
|
|
562
|
+
owner_coldkey_name = coldkey_name
|
|
563
|
+
except FileNotFoundError:
|
|
564
|
+
pass
|
|
565
|
+
|
|
566
|
+
# Retry with the selected coldkey
|
|
567
|
+
wallet_info = get_wallet_info_by_name(
|
|
568
|
+
current_name,
|
|
569
|
+
is_hotkey=is_hotkey,
|
|
570
|
+
owner_address=owner_address,
|
|
571
|
+
owner_coldkey_name=owner_coldkey_name,
|
|
572
|
+
)
|
|
573
|
+
else:
|
|
574
|
+
# Re-raise if we can't handle it
|
|
575
|
+
raise
|
|
576
|
+
except ValueError as e:
|
|
577
|
+
# Handle old-style ValueError for backward compatibility
|
|
578
|
+
error_str = str(e)
|
|
579
|
+
if "ambiguous" in error_str.lower() and is_hotkey:
|
|
580
|
+
# Extract the matches from the error message or find them ourselves
|
|
581
|
+
all_keys = list_keys()
|
|
582
|
+
hotkey_matches = [
|
|
583
|
+
k
|
|
584
|
+
for k in all_keys
|
|
585
|
+
if k.get("is_hotkey", False) and k.get("name") == current_name
|
|
586
|
+
]
|
|
587
|
+
|
|
588
|
+
if len(hotkey_matches) > 1:
|
|
589
|
+
console.print(
|
|
590
|
+
warning(
|
|
591
|
+
f"Found {len(hotkey_matches)} hotkeys named '{current_name}'. Please select which one to update:"
|
|
592
|
+
)
|
|
593
|
+
)
|
|
594
|
+
table = HTCLITable(title="Select Hotkey")
|
|
595
|
+
table.add_column("Index", style="bold white", width=6)
|
|
596
|
+
table.add_column("Coldkey", style="bold yellow", width=25)
|
|
597
|
+
table.add_column("Address", style="white", width=30)
|
|
598
|
+
|
|
599
|
+
for i, match in enumerate(hotkey_matches):
|
|
600
|
+
owner_ck = match.get("owner_coldkey_name", "Unknown")
|
|
601
|
+
address = (
|
|
602
|
+
match.get("evm_address")
|
|
603
|
+
or match.get("ss58_address")
|
|
604
|
+
or match.get("address", "N/A")
|
|
605
|
+
)
|
|
606
|
+
if address != "N/A":
|
|
607
|
+
address_display = format_address_display(address)
|
|
608
|
+
else:
|
|
609
|
+
address_display = address
|
|
610
|
+
table.add_row(str(i + 1), owner_ck, address_display)
|
|
611
|
+
|
|
612
|
+
console.print(table.table)
|
|
613
|
+
console.print()
|
|
614
|
+
|
|
615
|
+
choice = prompt_for_required(
|
|
616
|
+
f"Select hotkey to update (1-{len(hotkey_matches)})",
|
|
617
|
+
int,
|
|
618
|
+
f"Enter a number from 1 to {len(hotkey_matches)}",
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
if not (1 <= choice <= len(hotkey_matches)):
|
|
622
|
+
raise ValueError(
|
|
623
|
+
f"Invalid selection. Please choose between 1 and {len(hotkey_matches)}"
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
selected_hotkey = hotkey_matches[choice - 1]
|
|
627
|
+
coldkey_name = selected_hotkey.get("owner_coldkey_name")
|
|
628
|
+
if coldkey_name:
|
|
629
|
+
try:
|
|
630
|
+
coldkey_info = get_wallet_info_by_name(
|
|
631
|
+
coldkey_name, is_hotkey=False
|
|
632
|
+
)
|
|
633
|
+
owner_address = (
|
|
634
|
+
coldkey_info.get("evm_address")
|
|
635
|
+
or coldkey_info.get("ss58_address")
|
|
636
|
+
or coldkey_info.get("address")
|
|
637
|
+
)
|
|
638
|
+
owner_coldkey_name = coldkey_name
|
|
639
|
+
except FileNotFoundError:
|
|
640
|
+
pass
|
|
641
|
+
|
|
642
|
+
wallet_info = get_wallet_info_by_name(
|
|
643
|
+
current_name,
|
|
644
|
+
is_hotkey=is_hotkey,
|
|
645
|
+
owner_address=owner_address,
|
|
646
|
+
owner_coldkey_name=owner_coldkey_name,
|
|
647
|
+
)
|
|
648
|
+
else:
|
|
649
|
+
raise
|
|
650
|
+
else:
|
|
651
|
+
raise
|
|
652
|
+
except FileNotFoundError as err:
|
|
653
|
+
# FIX: Check if wallet exists but under different owner to provide better error
|
|
654
|
+
found_broken = None
|
|
655
|
+
try:
|
|
656
|
+
# Try finding it without owner constraint
|
|
657
|
+
found_broken = get_wallet_info_by_name(
|
|
658
|
+
current_name, is_hotkey=is_hotkey
|
|
659
|
+
)
|
|
660
|
+
except Exception:
|
|
661
|
+
# Still not found or ambiguous
|
|
662
|
+
pass
|
|
663
|
+
|
|
664
|
+
if found_broken:
|
|
665
|
+
# If we get here, it exists!
|
|
666
|
+
msg = f"Wallet '{current_name}' not found"
|
|
667
|
+
if coldkey_name:
|
|
668
|
+
msg += f" under owner '{coldkey_name}'"
|
|
669
|
+
|
|
670
|
+
actual_owner = found_broken.get("owner_coldkey_name")
|
|
671
|
+
if actual_owner:
|
|
672
|
+
msg += f". It appears to be owned by '{actual_owner}'."
|
|
673
|
+
if is_hotkey:
|
|
674
|
+
msg += (
|
|
675
|
+
f"\n\nTry running with '--coldkey {actual_owner}' instead."
|
|
676
|
+
)
|
|
677
|
+
else:
|
|
678
|
+
msg += ". Please check the wallet owner."
|
|
679
|
+
|
|
680
|
+
raise ValueError(msg) from err
|
|
681
|
+
|
|
682
|
+
# Use specific generic error if totally not found
|
|
683
|
+
raise ValueError(
|
|
684
|
+
f"Wallet '{current_name}' not found. Please check the wallet name."
|
|
685
|
+
) from err
|
|
686
|
+
|
|
687
|
+
wallet_type_actual = wallet_info.get("is_hotkey", False)
|
|
688
|
+
|
|
689
|
+
# Validate wallet type matches expected type
|
|
690
|
+
if wallet_type_actual != is_hotkey:
|
|
691
|
+
expected = "hotkey" if is_hotkey else "coldkey"
|
|
692
|
+
actual = "hotkey" if wallet_type_actual else "coldkey"
|
|
693
|
+
raise ValueError(
|
|
694
|
+
f"Wallet '{current_name}' is a {actual}, but you're trying to update a {expected}. "
|
|
695
|
+
f"Use 'htcli wallet update-{'hotkey' if wallet_type_actual else 'coldkey'}' instead."
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
# For hotkeys, get the coldkey name only if we still don't have it (e.g., when wallet name provided via CLI)
|
|
699
|
+
if is_hotkey and not coldkey_name:
|
|
700
|
+
coldkey_name = prompt_for_required(
|
|
701
|
+
"Current coldkey name",
|
|
702
|
+
str,
|
|
703
|
+
"Specify which coldkey owns this hotkey",
|
|
704
|
+
validator=validate_wallet_name,
|
|
705
|
+
)
|
|
706
|
+
# Get owner address for disambiguation if we just got the coldkey name
|
|
707
|
+
if coldkey_name:
|
|
708
|
+
try:
|
|
709
|
+
coldkey_info = get_wallet_info_by_name(coldkey_name, is_hotkey=False)
|
|
710
|
+
owner_address = (
|
|
711
|
+
coldkey_info.get("evm_address")
|
|
712
|
+
or coldkey_info.get("ss58_address")
|
|
713
|
+
or coldkey_info.get("address")
|
|
714
|
+
)
|
|
715
|
+
owner_coldkey_name = coldkey_name
|
|
716
|
+
except FileNotFoundError:
|
|
717
|
+
raise ValueError(
|
|
718
|
+
f"Coldkey '{coldkey_name}' not found. Please check the coldkey name."
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
# Ask what to update
|
|
722
|
+
console.print(info("What would you like to update?"))
|
|
723
|
+
|
|
724
|
+
# Get new name if not provided
|
|
725
|
+
if new_name is None:
|
|
726
|
+
prompt_text = "New hotkey name" if is_hotkey else "New wallet name"
|
|
727
|
+
new_name = prompt_for_optional(
|
|
728
|
+
prompt_text,
|
|
729
|
+
str,
|
|
730
|
+
"Leave empty to keep current name",
|
|
731
|
+
validator=validate_wallet_name,
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
# Check if wallet is encrypted - use disambiguation if available
|
|
735
|
+
wallet_info = get_wallet_info_by_name(
|
|
736
|
+
current_name,
|
|
737
|
+
is_hotkey=is_hotkey,
|
|
738
|
+
owner_address=owner_address if is_hotkey else None,
|
|
739
|
+
owner_coldkey_name=owner_coldkey_name if is_hotkey else None,
|
|
740
|
+
)
|
|
741
|
+
is_currently_encrypted = wallet_info.get("is_encrypted", False)
|
|
742
|
+
|
|
743
|
+
# Password update options - only show if wallet has password protection
|
|
744
|
+
current_password = None
|
|
745
|
+
if new_password is None and remove_password is None:
|
|
746
|
+
if is_currently_encrypted:
|
|
747
|
+
# Wallet is password-protected, ask what to do
|
|
748
|
+
password_action = select_prompt(
|
|
749
|
+
"Password action",
|
|
750
|
+
choices=[
|
|
751
|
+
("keep", "Keep current password", "keep"),
|
|
752
|
+
("change", "Change password", "change"),
|
|
753
|
+
("remove", "Remove password protection", "remove"),
|
|
754
|
+
],
|
|
755
|
+
default="keep",
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
if password_action == "change":
|
|
759
|
+
# Ask for current password to decrypt (no min_length validation for current password)
|
|
760
|
+
current_password = password_prompt(
|
|
761
|
+
"Enter current password to decrypt wallet",
|
|
762
|
+
min_length=0, # Don't validate current password length
|
|
763
|
+
)
|
|
764
|
+
# Then ask for new password
|
|
765
|
+
while True:
|
|
766
|
+
new_password = password_prompt("Enter new password", confirm=True)
|
|
767
|
+
if validate_password_strength(new_password):
|
|
768
|
+
break
|
|
769
|
+
elif password_action == "remove":
|
|
770
|
+
# Ask for current password to decrypt before removing (no min_length validation for current password)
|
|
771
|
+
current_password = password_prompt(
|
|
772
|
+
"Enter current password to decrypt wallet",
|
|
773
|
+
min_length=0, # Don't validate current password length
|
|
774
|
+
)
|
|
775
|
+
remove_password = True
|
|
776
|
+
else:
|
|
777
|
+
# Wallet is not password-protected, ask if user wants to add protection
|
|
778
|
+
if confirm_prompt(
|
|
779
|
+
"This wallet is not password-protected. Would you like to add password protection?",
|
|
780
|
+
default=False,
|
|
781
|
+
):
|
|
782
|
+
while True:
|
|
783
|
+
new_password = password_prompt("Enter new password", confirm=True)
|
|
784
|
+
if validate_password_strength(new_password):
|
|
785
|
+
break
|
|
786
|
+
|
|
787
|
+
# Get new coldkey for hotkeys
|
|
788
|
+
if is_hotkey and new_coldkey is None:
|
|
789
|
+
# First, get current coldkey info to display
|
|
790
|
+
current_coldkey_name = "Unknown"
|
|
791
|
+
current_coldkey_address = "Unknown"
|
|
792
|
+
try:
|
|
793
|
+
wallet_info = get_wallet_info_by_name(current_name, is_hotkey=is_hotkey)
|
|
794
|
+
if coldkey_name: # If we have the coldkey name from earlier
|
|
795
|
+
try:
|
|
796
|
+
coldkey_info = get_wallet_info_by_name(
|
|
797
|
+
coldkey_name, is_hotkey=False
|
|
798
|
+
)
|
|
799
|
+
current_coldkey_name = coldkey_info.get("name", coldkey_name)
|
|
800
|
+
current_coldkey_address = coldkey_info.get(
|
|
801
|
+
"ss58_address", "Unknown"
|
|
802
|
+
)
|
|
803
|
+
except FileNotFoundError:
|
|
804
|
+
current_coldkey_name = coldkey_name
|
|
805
|
+
except (FileNotFoundError, KeyError):
|
|
806
|
+
pass
|
|
807
|
+
|
|
808
|
+
# Ask user if they want to change coldkey
|
|
809
|
+
console.print(info("\n💰 Current Coldkey Information:"))
|
|
810
|
+
console.print(f" Coldkey Name: {current_coldkey_name}")
|
|
811
|
+
console.print(f" Coldkey Address: {current_coldkey_address}\n")
|
|
812
|
+
|
|
813
|
+
change_coldkey = confirm_prompt(
|
|
814
|
+
"Do you want to change the coldkey for this hotkey?",
|
|
815
|
+
default=False,
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
if change_coldkey:
|
|
819
|
+
new_coldkey = prompt_for_required(
|
|
820
|
+
"New coldkey name",
|
|
821
|
+
str,
|
|
822
|
+
"Enter the name of the new coldkey",
|
|
823
|
+
validator=validate_wallet_name,
|
|
824
|
+
)
|
|
825
|
+
else:
|
|
826
|
+
new_coldkey = None
|
|
827
|
+
|
|
828
|
+
return WalletUpdateRequest(
|
|
829
|
+
current_name=current_name,
|
|
830
|
+
new_name=new_name,
|
|
831
|
+
new_password=new_password,
|
|
832
|
+
remove_password=remove_password or False,
|
|
833
|
+
current_password=current_password,
|
|
834
|
+
new_owner=new_coldkey,
|
|
835
|
+
owner_name=coldkey_name if is_hotkey else None,
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def prompt_wallet_deletion(
|
|
840
|
+
coldkey: Optional[str] = None,
|
|
841
|
+
hotkey: Optional[str] = None,
|
|
842
|
+
force: Optional[bool] = None,
|
|
843
|
+
) -> WalletDeleteRequest:
|
|
844
|
+
"""Collect wallet deletion parameters.
|
|
845
|
+
|
|
846
|
+
Args:
|
|
847
|
+
coldkey: Coldkey name to delete (required for coldkey deletion or non-orphaned hotkey deletion)
|
|
848
|
+
hotkey: Hotkey name to delete (optional, requires coldkey unless hotkey is orphaned)
|
|
849
|
+
force: Skip confirmation prompts
|
|
850
|
+
"""
|
|
851
|
+
# Validate parameters
|
|
852
|
+
if not coldkey and not hotkey:
|
|
853
|
+
raise ValueError("Either --coldkey or --hotkey must be provided")
|
|
854
|
+
|
|
855
|
+
if hotkey:
|
|
856
|
+
# Deleting a hotkey
|
|
857
|
+
if not validate_wallet_name(hotkey):
|
|
858
|
+
raise ValueError(f"Invalid hotkey name: {hotkey}")
|
|
859
|
+
wallet_name = hotkey
|
|
860
|
+
is_hotkey = True
|
|
861
|
+
|
|
862
|
+
if coldkey:
|
|
863
|
+
# Owner specified - validate coldkey name
|
|
864
|
+
if not validate_wallet_name(coldkey):
|
|
865
|
+
raise ValueError(f"Invalid coldkey name: {coldkey}")
|
|
866
|
+
# else: orphaned hotkey - no coldkey validation needed
|
|
867
|
+
else:
|
|
868
|
+
# Deleting a coldkey - coldkey is required
|
|
869
|
+
if not coldkey:
|
|
870
|
+
raise ValueError("--coldkey is required for coldkey deletion")
|
|
871
|
+
if not validate_wallet_name(coldkey):
|
|
872
|
+
raise ValueError(f"Invalid coldkey name: {coldkey}")
|
|
873
|
+
wallet_name = coldkey
|
|
874
|
+
is_hotkey = False
|
|
875
|
+
|
|
876
|
+
names = [wallet_name]
|
|
877
|
+
|
|
878
|
+
# Check if wallet exists
|
|
879
|
+
wallet_owners: dict[str, Optional[str]] = (
|
|
880
|
+
{}
|
|
881
|
+
) # name -> owner_address for disambiguation
|
|
882
|
+
owner_context: dict[str, dict[str, Optional[str]]] = {}
|
|
883
|
+
|
|
884
|
+
if is_hotkey:
|
|
885
|
+
# For hotkey deletion
|
|
886
|
+
if coldkey:
|
|
887
|
+
# Owner specified - we know the owner
|
|
888
|
+
wallet_owners[wallet_name] = None # Will be resolved from coldkey
|
|
889
|
+
owner_context[wallet_name] = {"address": None, "coldkey": coldkey}
|
|
890
|
+
else:
|
|
891
|
+
# Orphaned hotkey - no owner
|
|
892
|
+
wallet_owners[wallet_name] = None
|
|
893
|
+
owner_context[wallet_name] = {"address": None, "coldkey": None}
|
|
894
|
+
else:
|
|
895
|
+
# For coldkey deletion, no owner needed
|
|
896
|
+
wallet_owners[wallet_name] = None
|
|
897
|
+
owner_context[wallet_name] = {"address": None, "coldkey": wallet_name}
|
|
898
|
+
|
|
899
|
+
# Verify wallet exists
|
|
900
|
+
try:
|
|
901
|
+
if is_hotkey:
|
|
902
|
+
if coldkey:
|
|
903
|
+
# Get coldkey info to resolve owner address
|
|
904
|
+
coldkey_info = get_wallet_info_by_name(coldkey, is_hotkey=False)
|
|
905
|
+
owner_address = (
|
|
906
|
+
coldkey_info.get("evm_address")
|
|
907
|
+
or coldkey_info.get("ss58_address")
|
|
908
|
+
or coldkey_info.get("address")
|
|
909
|
+
)
|
|
910
|
+
owner_context[wallet_name]["address"] = owner_address
|
|
911
|
+
|
|
912
|
+
# Check if hotkey exists
|
|
913
|
+
hotkey_info = get_wallet_info_by_name(
|
|
914
|
+
wallet_name,
|
|
915
|
+
is_hotkey=True,
|
|
916
|
+
owner_address=owner_address,
|
|
917
|
+
owner_coldkey_name=coldkey,
|
|
918
|
+
)
|
|
919
|
+
else:
|
|
920
|
+
# Orphaned hotkey - find it without owner context
|
|
921
|
+
all_keys = list_keys()
|
|
922
|
+
matching_hotkeys = [
|
|
923
|
+
k
|
|
924
|
+
for k in all_keys
|
|
925
|
+
if k.get("is_hotkey", False) and k.get("name") == wallet_name
|
|
926
|
+
]
|
|
927
|
+
|
|
928
|
+
if not matching_hotkeys:
|
|
929
|
+
raise FileNotFoundError(f"Hotkey '{wallet_name}' not found")
|
|
930
|
+
|
|
931
|
+
# Use first match (for orphaned hotkeys, there should typically be only one)
|
|
932
|
+
hotkey_info = matching_hotkeys[0]
|
|
933
|
+
owner_address = hotkey_info.get("owner_address")
|
|
934
|
+
owner_coldkey_name = hotkey_info.get("owner_coldkey_name")
|
|
935
|
+
owner_context[wallet_name]["address"] = owner_address
|
|
936
|
+
owner_context[wallet_name]["coldkey"] = owner_coldkey_name
|
|
937
|
+
|
|
938
|
+
# Verify the hotkey file exists by trying to get wallet info
|
|
939
|
+
hotkey_info_verified = get_wallet_info_by_name(
|
|
940
|
+
wallet_name,
|
|
941
|
+
is_hotkey=True,
|
|
942
|
+
owner_address=owner_address,
|
|
943
|
+
owner_coldkey_name=owner_coldkey_name,
|
|
944
|
+
)
|
|
945
|
+
else:
|
|
946
|
+
# Check if coldkey exists
|
|
947
|
+
coldkey_info = get_wallet_info_by_name(wallet_name, is_hotkey=False)
|
|
948
|
+
except (ValueError, FileNotFoundError) as e:
|
|
949
|
+
console.print(warning(f"❌ Wallet does not exist: {str(e)}"))
|
|
950
|
+
raise KeyboardInterrupt("Operation cancelled")
|
|
951
|
+
|
|
952
|
+
# Get confirmation with granular control
|
|
953
|
+
if not force:
|
|
954
|
+
# Collect deletion decision
|
|
955
|
+
deletion_plan = {}
|
|
956
|
+
owner_meta = owner_context.get(wallet_name, {})
|
|
957
|
+
owner_address = owner_meta.get("address")
|
|
958
|
+
owner_coldkey_name = owner_meta.get("coldkey")
|
|
959
|
+
|
|
960
|
+
if is_hotkey:
|
|
961
|
+
# Deleting a hotkey
|
|
962
|
+
owner_display = (
|
|
963
|
+
owner_coldkey_name
|
|
964
|
+
if owner_coldkey_name
|
|
965
|
+
else "orphaned (no valid owner)"
|
|
966
|
+
)
|
|
967
|
+
console.print(
|
|
968
|
+
warning(f"⚠️ Deleting hotkey: {wallet_name} (owned by {owner_display})")
|
|
969
|
+
)
|
|
970
|
+
deletion_plan[wallet_name] = {
|
|
971
|
+
"type": "hotkey",
|
|
972
|
+
"delete_hotkeys": False,
|
|
973
|
+
"associated_hotkeys": [],
|
|
974
|
+
}
|
|
975
|
+
else:
|
|
976
|
+
# Deleting a coldkey - check for associated hotkeys
|
|
977
|
+
wallet_info = get_wallet_info_by_name(wallet_name, is_hotkey=False)
|
|
978
|
+
all_keys = list_keys()
|
|
979
|
+
|
|
980
|
+
# Get all possible address formats from coldkey info
|
|
981
|
+
coldkey_addresses = set()
|
|
982
|
+
if wallet_info.get("address"):
|
|
983
|
+
coldkey_addresses.add(wallet_info["address"])
|
|
984
|
+
if wallet_info.get("ss58_address"):
|
|
985
|
+
coldkey_addresses.add(wallet_info["ss58_address"])
|
|
986
|
+
if wallet_info.get("evm_address"):
|
|
987
|
+
coldkey_addresses.add(wallet_info["evm_address"])
|
|
988
|
+
|
|
989
|
+
# Find hotkeys by matching any address format OR owner_coldkey_name
|
|
990
|
+
associated_hotkeys = []
|
|
991
|
+
for key in all_keys:
|
|
992
|
+
if not key.get("is_hotkey", False):
|
|
993
|
+
continue
|
|
994
|
+
|
|
995
|
+
# Check if owner_address matches any of the coldkey's address formats
|
|
996
|
+
owner_address = key.get("owner_address")
|
|
997
|
+
owner_matches = owner_address and owner_address in coldkey_addresses
|
|
998
|
+
|
|
999
|
+
# Check if owner_coldkey_name matches
|
|
1000
|
+
owner_name_matches = key.get("owner_coldkey_name") == wallet_name
|
|
1001
|
+
|
|
1002
|
+
if owner_matches or owner_name_matches:
|
|
1003
|
+
associated_hotkeys.append(key)
|
|
1004
|
+
|
|
1005
|
+
console.print(warning(f"⚠️ Deleting coldkey: {wallet_name}"))
|
|
1006
|
+
|
|
1007
|
+
if associated_hotkeys:
|
|
1008
|
+
console.print(f"Found {len(associated_hotkeys)} associated hotkey(s):")
|
|
1009
|
+
for hotkey in associated_hotkeys:
|
|
1010
|
+
console.print(f" • {hotkey['name']}")
|
|
1011
|
+
|
|
1012
|
+
# Ask about deleting associated hotkeys
|
|
1013
|
+
delete_hotkeys = confirm_prompt(
|
|
1014
|
+
f"Delete {len(associated_hotkeys)} associated hotkey(s) along with coldkey '{wallet_name}'?",
|
|
1015
|
+
default=True,
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
deletion_plan[wallet_name] = {
|
|
1019
|
+
"type": "coldkey",
|
|
1020
|
+
"delete_hotkeys": delete_hotkeys,
|
|
1021
|
+
"associated_hotkeys": associated_hotkeys if delete_hotkeys else [],
|
|
1022
|
+
}
|
|
1023
|
+
else:
|
|
1024
|
+
console.print(
|
|
1025
|
+
f"No associated hotkeys found for coldkey '{wallet_name}'"
|
|
1026
|
+
)
|
|
1027
|
+
deletion_plan[wallet_name] = {
|
|
1028
|
+
"type": "coldkey",
|
|
1029
|
+
"delete_hotkeys": False,
|
|
1030
|
+
"associated_hotkeys": [],
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
console.print()
|
|
1034
|
+
console.print(warning("🗑️ Deletion Summary:"))
|
|
1035
|
+
for name, plan in deletion_plan.items():
|
|
1036
|
+
if plan["type"] == "coldkey":
|
|
1037
|
+
if plan["delete_hotkeys"] and plan["associated_hotkeys"]:
|
|
1038
|
+
console.print(
|
|
1039
|
+
f" • {name} (coldkey) + {len(plan['associated_hotkeys'])} hotkey(s):"
|
|
1040
|
+
)
|
|
1041
|
+
# Show hotkey names and owner
|
|
1042
|
+
for hotkey in plan["associated_hotkeys"]:
|
|
1043
|
+
hotkey_name = hotkey.get("name", "Unknown")
|
|
1044
|
+
console.print(f" - {hotkey_name} (owned by {name})")
|
|
1045
|
+
else:
|
|
1046
|
+
console.print(
|
|
1047
|
+
f" • {name} (coldkey only - hotkeys will become orphaned)"
|
|
1048
|
+
)
|
|
1049
|
+
else:
|
|
1050
|
+
console.print(f" • {name} (hotkey)")
|
|
1051
|
+
|
|
1052
|
+
console.print()
|
|
1053
|
+
console.print(warning("This action cannot be undone!"))
|
|
1054
|
+
|
|
1055
|
+
confirmed = confirm_prompt(
|
|
1056
|
+
"Proceed with the deletion plan above?", default=False
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1059
|
+
if not confirmed:
|
|
1060
|
+
raise KeyboardInterrupt("Operation cancelled")
|
|
1061
|
+
|
|
1062
|
+
# Store the deletion plan in the request
|
|
1063
|
+
return WalletDeleteRequest(
|
|
1064
|
+
names=names,
|
|
1065
|
+
force=force,
|
|
1066
|
+
deletion_plan=deletion_plan,
|
|
1067
|
+
wallet_owners=wallet_owners,
|
|
1068
|
+
wallet_owner_context=owner_context,
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
return WalletDeleteRequest(
|
|
1072
|
+
names=names,
|
|
1073
|
+
force=force,
|
|
1074
|
+
wallet_owners=wallet_owners,
|
|
1075
|
+
wallet_owner_context=owner_context,
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
def prompt_wallet_transfer(
|
|
1080
|
+
from_wallet: Optional[str] = None,
|
|
1081
|
+
to_address: Optional[str] = None,
|
|
1082
|
+
amount: Optional[str] = None,
|
|
1083
|
+
password: Optional[str] = None,
|
|
1084
|
+
) -> WalletTransferRequest:
|
|
1085
|
+
"""Collect wallet transfer parameters."""
|
|
1086
|
+
# Get source wallet
|
|
1087
|
+
if not from_wallet:
|
|
1088
|
+
keys = list_keys()
|
|
1089
|
+
if not keys:
|
|
1090
|
+
raise Exception("No wallets found.")
|
|
1091
|
+
|
|
1092
|
+
# Filter out hotkeys for transfer (only coldkeys have balances)
|
|
1093
|
+
coldkeys = [key for key in keys if not key.get("is_hotkey", False)]
|
|
1094
|
+
|
|
1095
|
+
if not coldkeys:
|
|
1096
|
+
raise Exception(
|
|
1097
|
+
"No coldkey wallets found. Only coldkeys can be used for transfers."
|
|
1098
|
+
)
|
|
1099
|
+
|
|
1100
|
+
console.print("\n[bold]Available coldkey wallets:[/bold]")
|
|
1101
|
+
for i, key in enumerate(coldkeys, 1):
|
|
1102
|
+
# Coldkey styling - blue/cyan theme
|
|
1103
|
+
key_type = "[bold cyan]Coldkey[/bold cyan]"
|
|
1104
|
+
name_color = "[bold cyan]"
|
|
1105
|
+
address_color = "[cyan]"
|
|
1106
|
+
|
|
1107
|
+
console.print(
|
|
1108
|
+
f"{i}. {name_color}{key['name']}[/] ({key_type}) - {address_color}{key['ss58_address']}[/]"
|
|
1109
|
+
)
|
|
1110
|
+
|
|
1111
|
+
while True:
|
|
1112
|
+
try:
|
|
1113
|
+
choice = integer_prompt(
|
|
1114
|
+
f"\nEnter wallet number (1-{len(coldkeys)})",
|
|
1115
|
+
min_value=1,
|
|
1116
|
+
max_value=len(coldkeys),
|
|
1117
|
+
)
|
|
1118
|
+
from_wallet = coldkeys[choice - 1]["name"]
|
|
1119
|
+
break
|
|
1120
|
+
except (ValueError, IndexError):
|
|
1121
|
+
console.print(error("Invalid selection. Please try again."))
|
|
1122
|
+
|
|
1123
|
+
# Get destination address
|
|
1124
|
+
if not to_address:
|
|
1125
|
+
# Show destination options
|
|
1126
|
+
dest_choice = select_prompt(
|
|
1127
|
+
"Destination type",
|
|
1128
|
+
choices=[
|
|
1129
|
+
("wallet", "Select wallet", "wallet"),
|
|
1130
|
+
("address", "Enter address", "address"),
|
|
1131
|
+
],
|
|
1132
|
+
default="wallet",
|
|
1133
|
+
)
|
|
1134
|
+
|
|
1135
|
+
if dest_choice == "wallet":
|
|
1136
|
+
# Show coldkey wallets for destination selection
|
|
1137
|
+
keys = list_keys()
|
|
1138
|
+
coldkeys = [key for key in keys if not key.get("is_hotkey", False)]
|
|
1139
|
+
|
|
1140
|
+
if not coldkeys:
|
|
1141
|
+
raise Exception("No coldkey wallets found for destination.")
|
|
1142
|
+
|
|
1143
|
+
console.print("\n[bold]Available destination wallets:[/bold]")
|
|
1144
|
+
for i, key in enumerate(coldkeys, 1):
|
|
1145
|
+
# Coldkey styling - blue/cyan theme
|
|
1146
|
+
key_type = "[bold cyan]Coldkey[/bold cyan]"
|
|
1147
|
+
name_color = "[bold cyan]"
|
|
1148
|
+
address_color = "[cyan]"
|
|
1149
|
+
|
|
1150
|
+
console.print(
|
|
1151
|
+
f"{i}. {name_color}{key['name']}[/] ({key_type}) - {address_color}{key['ss58_address']}[/]"
|
|
1152
|
+
)
|
|
1153
|
+
|
|
1154
|
+
while True:
|
|
1155
|
+
try:
|
|
1156
|
+
choice = integer_prompt(
|
|
1157
|
+
f"\nEnter destination wallet number (1-{len(coldkeys)})",
|
|
1158
|
+
min_value=1,
|
|
1159
|
+
max_value=len(coldkeys),
|
|
1160
|
+
)
|
|
1161
|
+
dest_wallet_info = coldkeys[choice - 1]
|
|
1162
|
+
|
|
1163
|
+
# Check if destination is the same as source wallet
|
|
1164
|
+
if dest_wallet_info["name"] == from_wallet:
|
|
1165
|
+
console.print(
|
|
1166
|
+
error(
|
|
1167
|
+
"❌ Cannot transfer to the same wallet. Please select a different destination."
|
|
1168
|
+
)
|
|
1169
|
+
)
|
|
1170
|
+
continue
|
|
1171
|
+
|
|
1172
|
+
to_address = dest_wallet_info["ss58_address"]
|
|
1173
|
+
console.print(
|
|
1174
|
+
f"[green]✓[/green] Selected destination: {dest_wallet_info['name']} ({to_address})"
|
|
1175
|
+
)
|
|
1176
|
+
break
|
|
1177
|
+
except (ValueError, IndexError):
|
|
1178
|
+
console.print(error("Invalid selection. Please try again."))
|
|
1179
|
+
else:
|
|
1180
|
+
# Use text_prompt for direct address input
|
|
1181
|
+
to_address = text_prompt("Destination address", required=True)
|
|
1182
|
+
|
|
1183
|
+
# Try to resolve as wallet name if not a valid address
|
|
1184
|
+
from ...utils.blockchain.validation import detect_address_type
|
|
1185
|
+
|
|
1186
|
+
if detect_address_type(to_address)[0] == "unknown":
|
|
1187
|
+
# Check if it's a valid wallet name
|
|
1188
|
+
from ...utils.blockchain.validation import validate_wallet_name
|
|
1189
|
+
|
|
1190
|
+
if validate_wallet_name(to_address):
|
|
1191
|
+
try:
|
|
1192
|
+
# Try to resolve as wallet name
|
|
1193
|
+
dest_wallet_info = get_wallet_info_by_name(
|
|
1194
|
+
to_address, is_hotkey=False
|
|
1195
|
+
)
|
|
1196
|
+
|
|
1197
|
+
# Check if destination is the same as source wallet
|
|
1198
|
+
if dest_wallet_info["name"] == from_wallet:
|
|
1199
|
+
console.print(
|
|
1200
|
+
error(
|
|
1201
|
+
"❌ Cannot transfer to the same wallet. Please enter a different destination."
|
|
1202
|
+
)
|
|
1203
|
+
)
|
|
1204
|
+
raise Exception("Cannot transfer to the same wallet.")
|
|
1205
|
+
|
|
1206
|
+
console.print(
|
|
1207
|
+
f"[green]✓[/green] Resolved wallet '{to_address}' to address: {dest_wallet_info['ss58_address']}"
|
|
1208
|
+
)
|
|
1209
|
+
to_address = dest_wallet_info["ss58_address"]
|
|
1210
|
+
except FileNotFoundError:
|
|
1211
|
+
console.print(
|
|
1212
|
+
error(f"Destination wallet '{to_address}' not found.")
|
|
1213
|
+
)
|
|
1214
|
+
raise Exception(
|
|
1215
|
+
f"Destination wallet '{to_address}' not found."
|
|
1216
|
+
) from None
|
|
1217
|
+
except Exception as e:
|
|
1218
|
+
console.print(
|
|
1219
|
+
error(
|
|
1220
|
+
f"Error accessing destination wallet '{to_address}': {str(e)}"
|
|
1221
|
+
)
|
|
1222
|
+
)
|
|
1223
|
+
raise Exception(
|
|
1224
|
+
f"Error accessing destination wallet '{to_address}': {str(e)}"
|
|
1225
|
+
) from e
|
|
1226
|
+
else:
|
|
1227
|
+
console.print(
|
|
1228
|
+
error("Invalid destination address or wallet name format.")
|
|
1229
|
+
)
|
|
1230
|
+
raise Exception(
|
|
1231
|
+
"Invalid destination address or wallet name format."
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
# Get amount with validation
|
|
1235
|
+
if not amount:
|
|
1236
|
+
# Get sender's balance for validation
|
|
1237
|
+
try:
|
|
1238
|
+
from ...dependencies import get_client
|
|
1239
|
+
|
|
1240
|
+
client = get_client()
|
|
1241
|
+
wallet_info = get_wallet_info_by_name(from_wallet, is_hotkey=False)
|
|
1242
|
+
balance_response = client.rpc.wallet.get_balance(
|
|
1243
|
+
wallet_info["ss58_address"]
|
|
1244
|
+
)
|
|
1245
|
+
|
|
1246
|
+
if balance_response.success:
|
|
1247
|
+
max_balance = balance_response.balance / 1e18 # Convert wei to TENSOR
|
|
1248
|
+
console.print(info(f"💰 Your balance: {max_balance:,.4f} TENSOR"))
|
|
1249
|
+
|
|
1250
|
+
# Set reasonable maximum (either balance or 1 billion TENSOR, whichever is lower)
|
|
1251
|
+
max_amount = min(max_balance, 1_000_000_000) # 1 billion TENSOR max
|
|
1252
|
+
else:
|
|
1253
|
+
# If can't get balance, set a reasonable default max
|
|
1254
|
+
max_amount = 1_000_000_000 # 1 billion TENSOR
|
|
1255
|
+
console.print(info("⚠️ Could not fetch balance, using default limit"))
|
|
1256
|
+
except Exception as e:
|
|
1257
|
+
console.print(info(f"⚠️ Balance check failed: {str(e)}"))
|
|
1258
|
+
max_amount = 1_000_000_000 # 1 billion TENSOR default
|
|
1259
|
+
|
|
1260
|
+
amount_float = amount_prompt(
|
|
1261
|
+
"Transfer amount",
|
|
1262
|
+
currency="TENSOR",
|
|
1263
|
+
min_amount=0.000001,
|
|
1264
|
+
max_amount=max_amount,
|
|
1265
|
+
)
|
|
1266
|
+
amount = str(amount_float)
|
|
1267
|
+
else:
|
|
1268
|
+
# Validate CLI-provided amount
|
|
1269
|
+
try:
|
|
1270
|
+
amount_float = float(amount)
|
|
1271
|
+
if amount_float <= 0:
|
|
1272
|
+
raise ValueError("Amount must be positive")
|
|
1273
|
+
if amount_float > 1_000_000_000: # 1 billion TENSOR
|
|
1274
|
+
raise ValueError("Amount too large. Maximum: 1,000,000,000 TENSOR")
|
|
1275
|
+
if amount_float < 0.000001:
|
|
1276
|
+
raise ValueError("Amount too small. Minimum: 0.000001 TENSOR")
|
|
1277
|
+
except ValueError as e:
|
|
1278
|
+
console.print(error(f"Invalid amount: {str(e)}"))
|
|
1279
|
+
raise
|
|
1280
|
+
|
|
1281
|
+
# Get password if needed
|
|
1282
|
+
if password is None:
|
|
1283
|
+
# Check if the wallet is encrypted
|
|
1284
|
+
try:
|
|
1285
|
+
wallet_info = get_wallet_info_by_name(from_wallet, is_hotkey=False)
|
|
1286
|
+
if wallet_info.get("encrypted", False):
|
|
1287
|
+
console.print(info("Wallet password required for encrypted wallet"))
|
|
1288
|
+
password = password_prompt("Enter wallet password", min_length=1)
|
|
1289
|
+
else:
|
|
1290
|
+
console.print(info("Wallet is not encrypted, no password needed"))
|
|
1291
|
+
password = None
|
|
1292
|
+
except Exception as e:
|
|
1293
|
+
# If we can't determine encryption status, skip password prompt
|
|
1294
|
+
console.print(
|
|
1295
|
+
info(f"Could not determine wallet encryption status: {str(e)}")
|
|
1296
|
+
)
|
|
1297
|
+
console.print(info("Skipping password prompt"))
|
|
1298
|
+
password = None
|
|
1299
|
+
|
|
1300
|
+
return WalletTransferRequest(
|
|
1301
|
+
from_wallet=from_wallet, to_address=to_address, amount=amount, password=password
|
|
1302
|
+
)
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
def prompt_wallet_selection(prompt_text: str = "Select wallet") -> str:
|
|
1306
|
+
"""Interactive prompt for wallet selection."""
|
|
1307
|
+
keys = list_keys()
|
|
1308
|
+
if not keys:
|
|
1309
|
+
raise Exception("No wallets found.")
|
|
1310
|
+
|
|
1311
|
+
console.print(f"\n[bold]{prompt_text}:[/bold]")
|
|
1312
|
+
for i, key in enumerate(keys, 1):
|
|
1313
|
+
if key.get("is_hotkey", False):
|
|
1314
|
+
# Hotkey styling - yellow/orange theme
|
|
1315
|
+
key_type = "[bold yellow]Hotkey[/bold yellow]"
|
|
1316
|
+
name_color = "[bold yellow]"
|
|
1317
|
+
address_color = "[yellow]"
|
|
1318
|
+
else:
|
|
1319
|
+
# Coldkey styling - blue/cyan theme
|
|
1320
|
+
key_type = "[bold cyan]Coldkey[/bold cyan]"
|
|
1321
|
+
name_color = "[bold cyan]"
|
|
1322
|
+
address_color = "[cyan]"
|
|
1323
|
+
|
|
1324
|
+
console.print(
|
|
1325
|
+
f"{i}. {name_color}{key['name']}[/] ({key_type}) - {address_color}{key['ss58_address']}[/]"
|
|
1326
|
+
)
|
|
1327
|
+
|
|
1328
|
+
while True:
|
|
1329
|
+
try:
|
|
1330
|
+
choice = integer_prompt(
|
|
1331
|
+
f"Enter wallet number (1-{len(keys)})",
|
|
1332
|
+
min_value=1,
|
|
1333
|
+
max_value=len(keys),
|
|
1334
|
+
)
|
|
1335
|
+
return keys[choice - 1]["name"]
|
|
1336
|
+
except (ValueError, IndexError):
|
|
1337
|
+
console.print(error("Invalid selection. Please try again."))
|
|
1338
|
+
|
|
1339
|
+
|
|
1340
|
+
def prompt_wallet_balance(
|
|
1341
|
+
wallet_name: Optional[str] = None,
|
|
1342
|
+
address: Optional[str] = None,
|
|
1343
|
+
show_all: bool = False,
|
|
1344
|
+
format_type: Optional[str] = None,
|
|
1345
|
+
show_guidance: Optional[bool] = None,
|
|
1346
|
+
) -> tuple[Optional[str], Optional[str], bool, str, bool]:
|
|
1347
|
+
"""Interactive prompt for balance arguments."""
|
|
1348
|
+
# If wallet_name is provided, check if it's actually an address
|
|
1349
|
+
if wallet_name:
|
|
1350
|
+
from ...utils.blockchain.validation import detect_address_type
|
|
1351
|
+
|
|
1352
|
+
addr_type, _, is_valid = detect_address_type(wallet_name)
|
|
1353
|
+
if is_valid and addr_type == "ecdsa":
|
|
1354
|
+
# It's an Ethereum address, move it to address parameter
|
|
1355
|
+
address = wallet_name
|
|
1356
|
+
wallet_name = None
|
|
1357
|
+
|
|
1358
|
+
if not wallet_name and not address and not show_all:
|
|
1359
|
+
choice = select_prompt(
|
|
1360
|
+
"Check balance by",
|
|
1361
|
+
choices=[
|
|
1362
|
+
("wallet", "Specific wallet", "wallet"),
|
|
1363
|
+
("address", "Specific address", "address"),
|
|
1364
|
+
("all", "All coldkey wallets", "all"),
|
|
1365
|
+
],
|
|
1366
|
+
default="wallet",
|
|
1367
|
+
)
|
|
1368
|
+
if choice == "wallet":
|
|
1369
|
+
keys = list_keys()
|
|
1370
|
+
if not keys:
|
|
1371
|
+
raise Exception("No wallets found.")
|
|
1372
|
+
|
|
1373
|
+
# Filter out hotkeys for balance check (only coldkeys have balances)
|
|
1374
|
+
coldkeys = [key for key in keys if not key.get("is_hotkey", False)]
|
|
1375
|
+
|
|
1376
|
+
if not coldkeys:
|
|
1377
|
+
raise Exception(
|
|
1378
|
+
"No coldkey wallets found. Only coldkeys have balances."
|
|
1379
|
+
)
|
|
1380
|
+
|
|
1381
|
+
console.print("\n[bold]Available coldkey wallets:[/bold]")
|
|
1382
|
+
for i, key in enumerate(coldkeys, 1):
|
|
1383
|
+
# Coldkey styling - blue/cyan theme
|
|
1384
|
+
key_type = "[bold cyan]Coldkey[/bold cyan]"
|
|
1385
|
+
name_color = "[bold cyan]"
|
|
1386
|
+
address_color = "[cyan]"
|
|
1387
|
+
|
|
1388
|
+
console.print(
|
|
1389
|
+
f"{i}. {name_color}{key['name']}[/] ({key_type}) - {address_color}{key['ss58_address']}[/]"
|
|
1390
|
+
)
|
|
1391
|
+
while True:
|
|
1392
|
+
try:
|
|
1393
|
+
choice = integer_prompt(
|
|
1394
|
+
f"\nEnter wallet number (1-{len(coldkeys)})",
|
|
1395
|
+
min_value=1,
|
|
1396
|
+
max_value=len(coldkeys),
|
|
1397
|
+
)
|
|
1398
|
+
wallet_name = coldkeys[choice - 1]["name"]
|
|
1399
|
+
break
|
|
1400
|
+
except (ValueError, IndexError):
|
|
1401
|
+
console.print(error("Invalid selection. Please try again."))
|
|
1402
|
+
elif choice == "address":
|
|
1403
|
+
from ...utils.blockchain.validation import detect_address_type
|
|
1404
|
+
|
|
1405
|
+
while True:
|
|
1406
|
+
address = text_prompt("Enter address to check balance")
|
|
1407
|
+
if detect_address_type(address)[0] != "unknown":
|
|
1408
|
+
break
|
|
1409
|
+
else:
|
|
1410
|
+
console.print("Invalid address format.")
|
|
1411
|
+
else: # all
|
|
1412
|
+
show_all = True
|
|
1413
|
+
|
|
1414
|
+
if format_type is None:
|
|
1415
|
+
format_type = select_prompt(
|
|
1416
|
+
"Output format", choices=["table", "json"], default="table"
|
|
1417
|
+
)
|
|
1418
|
+
|
|
1419
|
+
if show_guidance is None:
|
|
1420
|
+
show_guidance = confirm_prompt("Show comprehensive guidance?", default=True)
|
|
1421
|
+
|
|
1422
|
+
return wallet_name, address, show_all, format_type, show_guidance
|
|
1423
|
+
|
|
1424
|
+
|
|
1425
|
+
def prompt_identity_update(
|
|
1426
|
+
hotkey_name: Optional[str] = None,
|
|
1427
|
+
name: Optional[str] = None,
|
|
1428
|
+
url: Optional[str] = None,
|
|
1429
|
+
image: Optional[str] = None,
|
|
1430
|
+
discord: Optional[str] = None,
|
|
1431
|
+
x: Optional[str] = None,
|
|
1432
|
+
telegram: Optional[str] = None,
|
|
1433
|
+
github: Optional[str] = None,
|
|
1434
|
+
hugging_face: Optional[str] = None,
|
|
1435
|
+
description: Optional[str] = None,
|
|
1436
|
+
misc: Optional[str] = None,
|
|
1437
|
+
password: Optional[str] = None,
|
|
1438
|
+
):
|
|
1439
|
+
"""Collect identity update parameters."""
|
|
1440
|
+
from ...models.requests.identity import IdentityRegisterRequest
|
|
1441
|
+
|
|
1442
|
+
# Get hotkey name if not provided
|
|
1443
|
+
if not hotkey_name:
|
|
1444
|
+
hotkey_name = prompt_wallet_selection("Select hotkey to update identity")
|
|
1445
|
+
|
|
1446
|
+
# Get identity fields
|
|
1447
|
+
if name is None:
|
|
1448
|
+
name = prompt_for_optional(
|
|
1449
|
+
"Display Name", str, "Enter display name for identity"
|
|
1450
|
+
)
|
|
1451
|
+
|
|
1452
|
+
if description is None:
|
|
1453
|
+
description = prompt_for_optional("Description", str, "Enter short description")
|
|
1454
|
+
|
|
1455
|
+
if url is None:
|
|
1456
|
+
url = prompt_for_optional("Website URL", str, "Enter website URL")
|
|
1457
|
+
|
|
1458
|
+
if image is None:
|
|
1459
|
+
image = prompt_for_optional("Image URL", str, "Enter profile image URL")
|
|
1460
|
+
|
|
1461
|
+
if discord is None:
|
|
1462
|
+
discord = prompt_for_optional("Discord Handle", str, "Enter Discord handle")
|
|
1463
|
+
|
|
1464
|
+
if x is None:
|
|
1465
|
+
x = prompt_for_optional("X (Twitter) Handle", str, "Enter X handle")
|
|
1466
|
+
|
|
1467
|
+
if telegram is None:
|
|
1468
|
+
telegram = prompt_for_optional("Telegram Handle", str, "Enter Telegram handle")
|
|
1469
|
+
|
|
1470
|
+
if github is None:
|
|
1471
|
+
github = prompt_for_optional("GitHub URL", str, "Enter GitHub profile URL")
|
|
1472
|
+
|
|
1473
|
+
if hugging_face is None:
|
|
1474
|
+
hugging_face = prompt_for_optional(
|
|
1475
|
+
"Hugging Face URL", str, "Enter Hugging Face profile URL"
|
|
1476
|
+
)
|
|
1477
|
+
|
|
1478
|
+
if misc is None:
|
|
1479
|
+
misc = prompt_for_optional("Misc Data", str, "Enter any additional data")
|
|
1480
|
+
|
|
1481
|
+
# Get password if needed
|
|
1482
|
+
if password is None:
|
|
1483
|
+
# Check if the wallet is encrypted
|
|
1484
|
+
try:
|
|
1485
|
+
wallet_info = get_wallet_info_by_name(hotkey_name, is_hotkey=True)
|
|
1486
|
+
if wallet_info.get("is_encrypted", False) or wallet_info.get(
|
|
1487
|
+
"encrypted", False
|
|
1488
|
+
):
|
|
1489
|
+
console.print(info("Wallet password required for encrypted wallet"))
|
|
1490
|
+
password = password_prompt("Enter wallet password", min_length=1)
|
|
1491
|
+
else:
|
|
1492
|
+
password = None
|
|
1493
|
+
except Exception:
|
|
1494
|
+
# If we can't determine encryption status, prompt just in case
|
|
1495
|
+
if confirm_prompt("Is your wallet password protected?", default=False):
|
|
1496
|
+
password = password_prompt("Enter wallet password", min_length=1)
|
|
1497
|
+
else:
|
|
1498
|
+
password = None
|
|
1499
|
+
|
|
1500
|
+
# Construct the request model
|
|
1501
|
+
# Note: We return a tuple of (hotkey_name, password, request_model)
|
|
1502
|
+
# because the IdentityRegisterRequest doesn't include the hotkey name/password
|
|
1503
|
+
# in the way we need for the handler
|
|
1504
|
+
request = IdentityRegisterRequest(
|
|
1505
|
+
hotkey="", # Will be filled by handler with address
|
|
1506
|
+
name=name or "",
|
|
1507
|
+
url=url or "",
|
|
1508
|
+
image=image or "",
|
|
1509
|
+
discord=discord or "",
|
|
1510
|
+
x=x or "",
|
|
1511
|
+
telegram=telegram or "",
|
|
1512
|
+
github=github or "",
|
|
1513
|
+
hugging_face=hugging_face or "",
|
|
1514
|
+
description=description or "",
|
|
1515
|
+
misc=misc or "",
|
|
1516
|
+
)
|
|
1517
|
+
|
|
1518
|
+
return hotkey_name, password, request
|