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,3040 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wallet command execution handlers.
|
|
3
|
+
|
|
4
|
+
Contains the core business logic for wallet operations using the client system.
|
|
5
|
+
Follows the 3-step pattern: prompts → handlers → display.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import traceback
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
# Removed direct utils import - use client layer instead
|
|
14
|
+
# Use client layer instead of direct utils imports
|
|
15
|
+
from ...client.offchain.wallet import WalletManager
|
|
16
|
+
from ...errors.base import AmbiguousWalletError
|
|
17
|
+
from ...errors.handlers import handle_substrate_error
|
|
18
|
+
from .error_handling import handle_wallet_extrinsic_error
|
|
19
|
+
from ...models.responses import (
|
|
20
|
+
WalletCreateResponse,
|
|
21
|
+
WalletDeleteResponse,
|
|
22
|
+
WalletListResponse,
|
|
23
|
+
WalletStatusResponse,
|
|
24
|
+
WalletTransferResponse,
|
|
25
|
+
WalletUpdateResponse,
|
|
26
|
+
)
|
|
27
|
+
from ...ui.colors import address, info, success, warning
|
|
28
|
+
from ...ui.components import HTCLISpinner
|
|
29
|
+
from ...ui.display import HTCLIConsole
|
|
30
|
+
from ...utils.logging import get_logger
|
|
31
|
+
from ...utils.wallet.crypto import get_wallet_info_by_name, list_keys
|
|
32
|
+
|
|
33
|
+
# All utils functions now accessed through client layer - no direct imports needed
|
|
34
|
+
from .display import (
|
|
35
|
+
display_all_wallet_balances,
|
|
36
|
+
display_single_wallet_balance,
|
|
37
|
+
display_wallet_creation_result,
|
|
38
|
+
display_wallet_deletion_result,
|
|
39
|
+
display_wallet_describe_result,
|
|
40
|
+
display_wallet_generation_result,
|
|
41
|
+
display_wallet_list,
|
|
42
|
+
display_wallet_restoration_result,
|
|
43
|
+
display_wallet_status,
|
|
44
|
+
display_wallet_transfer_result,
|
|
45
|
+
display_wallet_update_result,
|
|
46
|
+
display_identity_update_result,
|
|
47
|
+
)
|
|
48
|
+
from .prompts import (
|
|
49
|
+
prompt_preseeded_wallet,
|
|
50
|
+
prompt_wallet_balance,
|
|
51
|
+
prompt_wallet_creation,
|
|
52
|
+
prompt_wallet_deletion,
|
|
53
|
+
prompt_wallet_restoration,
|
|
54
|
+
prompt_wallet_transfer,
|
|
55
|
+
prompt_wallet_update,
|
|
56
|
+
prompt_identity_update,
|
|
57
|
+
select_prompt,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
console = HTCLIConsole()
|
|
61
|
+
logger = get_logger(__name__)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_network_name(client) -> str:
|
|
65
|
+
"""Best-effort fetch of the connected network name."""
|
|
66
|
+
try:
|
|
67
|
+
if client and client.substrate and getattr(client.substrate, "chain", None):
|
|
68
|
+
return client.substrate.chain
|
|
69
|
+
except Exception:
|
|
70
|
+
logger.debug("Unable to determine network name from substrate connection.")
|
|
71
|
+
return "Hypertensor"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _get_staking_breakdown(client, address: Optional[str]) -> dict:
|
|
75
|
+
"""Fetch staking breakdown (in wei) for a coldkey address.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
dict with keys: direct_stake, delegate_stake, node_delegate_stake, overwatch_stake, unbonding, total
|
|
79
|
+
"""
|
|
80
|
+
result = {
|
|
81
|
+
"direct_stake": 0,
|
|
82
|
+
"delegate_stake": 0,
|
|
83
|
+
"node_delegate_stake": 0,
|
|
84
|
+
"overwatch_stake": 0,
|
|
85
|
+
"unbonding": 0,
|
|
86
|
+
"total": 0,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if not address or not client or not getattr(client, "rpc", None):
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
# 1. Get direct stakes from nodes owned by this coldkey
|
|
94
|
+
stake_response = client.rpc.wallet.network_get_coldkey_stakes(address)
|
|
95
|
+
if stake_response and stake_response.success and stake_response.data:
|
|
96
|
+
for stake in stake_response.data:
|
|
97
|
+
if isinstance(stake, dict):
|
|
98
|
+
result["direct_stake"] += int(stake.get("balance", 0))
|
|
99
|
+
else:
|
|
100
|
+
result["direct_stake"] += int(getattr(stake, "balance", 0))
|
|
101
|
+
except Exception as exc:
|
|
102
|
+
logger.debug(f"Failed to fetch direct stakes for {address}: {exc}")
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
# 2. Get delegate stakes (subnet delegate stakes)
|
|
106
|
+
delegate_response = client.rpc.wallet.network_get_delegate_stakes(address)
|
|
107
|
+
if delegate_response and delegate_response.success and delegate_response.data:
|
|
108
|
+
for stake in delegate_response.data:
|
|
109
|
+
if isinstance(stake, dict):
|
|
110
|
+
result["delegate_stake"] += int(stake.get("balance", 0))
|
|
111
|
+
else:
|
|
112
|
+
result["delegate_stake"] += int(getattr(stake, "balance", 0))
|
|
113
|
+
except Exception as exc:
|
|
114
|
+
logger.debug(f"Failed to fetch delegate stakes for {address}: {exc}")
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
# 3. Get node delegate stakes
|
|
118
|
+
node_delegate_response = client.rpc.wallet.network_get_node_delegate_stakes(
|
|
119
|
+
address
|
|
120
|
+
)
|
|
121
|
+
if (
|
|
122
|
+
node_delegate_response
|
|
123
|
+
and node_delegate_response.success
|
|
124
|
+
and node_delegate_response.data
|
|
125
|
+
):
|
|
126
|
+
for stake in node_delegate_response.data:
|
|
127
|
+
if isinstance(stake, dict):
|
|
128
|
+
result["node_delegate_stake"] += int(stake.get("balance", 0))
|
|
129
|
+
else:
|
|
130
|
+
result["node_delegate_stake"] += int(getattr(stake, "balance", 0))
|
|
131
|
+
except Exception as exc:
|
|
132
|
+
logger.debug(f"Failed to fetch node delegate stakes for {address}: {exc}")
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
# 4. Get overwatch stakes for this coldkey
|
|
136
|
+
# Overwatch stake is stored per hotkey in AccountOverwatchStake
|
|
137
|
+
# We need to find overwatch nodes owned by this coldkey
|
|
138
|
+
overwatch_nodes = client.rpc.overwatch.list_overwatch_nodes()
|
|
139
|
+
if overwatch_nodes:
|
|
140
|
+
# Normalize address for comparison
|
|
141
|
+
normalized_address = address.lower() if address else ""
|
|
142
|
+
for node in overwatch_nodes:
|
|
143
|
+
node_coldkey = getattr(node, "coldkey", None) or (
|
|
144
|
+
node.get("coldkey") if isinstance(node, dict) else None
|
|
145
|
+
)
|
|
146
|
+
if node_coldkey:
|
|
147
|
+
if node_coldkey.lower() == normalized_address:
|
|
148
|
+
stake_balance = getattr(node, "stake_balance", 0) or (
|
|
149
|
+
node.get("stake_balance", 0)
|
|
150
|
+
if isinstance(node, dict)
|
|
151
|
+
else 0
|
|
152
|
+
)
|
|
153
|
+
result["overwatch_stake"] += int(stake_balance)
|
|
154
|
+
except Exception as exc:
|
|
155
|
+
logger.debug(f"Failed to fetch overwatch stakes for {address}: {exc}")
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
# 5. Get unbonding balance (tokens in unbonding waiting period)
|
|
159
|
+
unbonding_info = client.rpc.wallet.get_unbonding_info(address)
|
|
160
|
+
if (
|
|
161
|
+
unbonding_info
|
|
162
|
+
and unbonding_info.get("success")
|
|
163
|
+
and unbonding_info.get("data")
|
|
164
|
+
):
|
|
165
|
+
unbondings = unbonding_info["data"].get("unbondings", [])
|
|
166
|
+
if unbondings:
|
|
167
|
+
# Sum all unbonding amounts
|
|
168
|
+
for unbonding in unbondings:
|
|
169
|
+
if isinstance(unbonding, dict):
|
|
170
|
+
result["unbonding"] += int(unbonding.get("amount", 0))
|
|
171
|
+
elif isinstance(unbonding, (list, tuple)) and len(unbonding) >= 2:
|
|
172
|
+
# Could be (amount, block) tuple format
|
|
173
|
+
result["unbonding"] += int(unbonding[0])
|
|
174
|
+
else:
|
|
175
|
+
# Try to get value directly
|
|
176
|
+
result["unbonding"] += int(getattr(unbonding, "amount", 0) or 0)
|
|
177
|
+
except Exception as exc:
|
|
178
|
+
logger.debug(f"Failed to fetch unbonding info for {address}: {exc}")
|
|
179
|
+
|
|
180
|
+
result["total"] = (
|
|
181
|
+
result["direct_stake"]
|
|
182
|
+
+ result["delegate_stake"]
|
|
183
|
+
+ result["node_delegate_stake"]
|
|
184
|
+
+ result["overwatch_stake"]
|
|
185
|
+
+ result["unbonding"]
|
|
186
|
+
)
|
|
187
|
+
return result
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _get_total_staked_balance(client, address: Optional[str]) -> int:
|
|
191
|
+
"""Fetch total staked balance (in wei) for a coldkey address.
|
|
192
|
+
|
|
193
|
+
This is a backward-compatible wrapper around _get_staking_breakdown.
|
|
194
|
+
"""
|
|
195
|
+
breakdown = _get_staking_breakdown(client, address)
|
|
196
|
+
return breakdown["total"]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def generate_coldkey_handler(
|
|
200
|
+
name: Optional[str] = None,
|
|
201
|
+
key_type: Optional[str] = None,
|
|
202
|
+
password: Optional[str] = None,
|
|
203
|
+
copy_mnemonic: bool = False,
|
|
204
|
+
):
|
|
205
|
+
"""Handle coldkey generation command."""
|
|
206
|
+
try:
|
|
207
|
+
# Early check: if name is provided via CLI, check existence before prompting for password
|
|
208
|
+
from ...ui.display import print_error
|
|
209
|
+
from ...utils.wallet.crypto import get_wallet_info_by_name
|
|
210
|
+
|
|
211
|
+
if name:
|
|
212
|
+
try:
|
|
213
|
+
get_wallet_info_by_name(name, is_hotkey=False)
|
|
214
|
+
# If we get here, wallet exists
|
|
215
|
+
print_error(
|
|
216
|
+
f"Wallet '{name}' already exists! Please choose a different name or use the existing wallet."
|
|
217
|
+
)
|
|
218
|
+
return
|
|
219
|
+
except FileNotFoundError:
|
|
220
|
+
# Good - wallet doesn't exist, can proceed
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
# STEP 1: Collect input parameters
|
|
224
|
+
request = prompt_wallet_creation(
|
|
225
|
+
name=name, key_type=key_type, wallet_type="coldkey", password=password
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# STEP 2: Execute via client layer
|
|
229
|
+
wallet_manager = WalletManager()
|
|
230
|
+
result = wallet_manager.create_coldkey_wallet(
|
|
231
|
+
name=request.name, password=request.password
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Create response model
|
|
235
|
+
response = WalletCreateResponse(
|
|
236
|
+
name=result["data"]["name"],
|
|
237
|
+
address=result["data"]["ss58_address"],
|
|
238
|
+
public_key=result["data"]["public_key"],
|
|
239
|
+
key_type=result["data"]["key_type"],
|
|
240
|
+
wallet_type="coldkey",
|
|
241
|
+
mnemonic=result["data"]["mnemonic"],
|
|
242
|
+
encrypted=result["data"]["is_encrypted"],
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# STEP 3: Display results
|
|
246
|
+
display_wallet_creation_result(response, copy_mnemonic=copy_mnemonic)
|
|
247
|
+
|
|
248
|
+
except Exception as e:
|
|
249
|
+
from ...errors.handlers import handle_wallet_error
|
|
250
|
+
from ...ui.display import print_error
|
|
251
|
+
|
|
252
|
+
error = handle_wallet_error(e, "update")
|
|
253
|
+
print_error(error.message, suggestions=error.suggestions)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def generate_hotkey_handler(
|
|
257
|
+
name: Optional[str] = None,
|
|
258
|
+
coldkey_name: Optional[str] = None,
|
|
259
|
+
key_type: Optional[str] = None,
|
|
260
|
+
password: Optional[str] = None,
|
|
261
|
+
copy_mnemonic: bool = False,
|
|
262
|
+
):
|
|
263
|
+
"""Handle hotkey generation command."""
|
|
264
|
+
try:
|
|
265
|
+
# STEP 1: Collect input parameters
|
|
266
|
+
request = prompt_wallet_creation(
|
|
267
|
+
name=name,
|
|
268
|
+
key_type=key_type,
|
|
269
|
+
wallet_type="hotkey",
|
|
270
|
+
coldkey_name=coldkey_name,
|
|
271
|
+
password=password,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Resolve coldkey name if provided to address
|
|
275
|
+
wallet_manager = WalletManager()
|
|
276
|
+
if request.owner_address:
|
|
277
|
+
coldkey_info = wallet_manager.get_wallet_info(
|
|
278
|
+
request.owner_address, is_hotkey=False
|
|
279
|
+
)
|
|
280
|
+
if coldkey_info["data"].get("is_hotkey", False):
|
|
281
|
+
raise ValueError(
|
|
282
|
+
f"'{request.owner_address}' is a hotkey. Please provide a coldkey name."
|
|
283
|
+
)
|
|
284
|
+
coldkey_address = coldkey_info["data"]["ss58_address"]
|
|
285
|
+
else:
|
|
286
|
+
raise ValueError("Coldkey name is required for hotkey creation")
|
|
287
|
+
|
|
288
|
+
# Check if this specific coldkey already has a hotkey with this name
|
|
289
|
+
from ...ui.display import print_error
|
|
290
|
+
from ...utils.wallet.crypto import coldkey_has_hotkey
|
|
291
|
+
|
|
292
|
+
if coldkey_has_hotkey(coldkey_address, request.name):
|
|
293
|
+
print_error(
|
|
294
|
+
f"Coldkey '{request.owner_address}' already has a hotkey named '{request.name}'. "
|
|
295
|
+
"Please choose a different name for this hotkey."
|
|
296
|
+
)
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
# STEP 2: Execute via client layer
|
|
300
|
+
result = wallet_manager.create_hotkey_wallet(
|
|
301
|
+
name=request.name,
|
|
302
|
+
owner_address=coldkey_address,
|
|
303
|
+
password=request.password,
|
|
304
|
+
owner_coldkey_name=request.owner_address,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Create response model
|
|
308
|
+
response = WalletCreateResponse(
|
|
309
|
+
name=result["data"]["name"],
|
|
310
|
+
address=result["data"]["ss58_address"],
|
|
311
|
+
public_key=result["data"]["public_key"],
|
|
312
|
+
key_type=result["data"]["key_type"],
|
|
313
|
+
wallet_type="hotkey",
|
|
314
|
+
mnemonic=result["data"]["mnemonic"],
|
|
315
|
+
encrypted=result["data"]["is_encrypted"],
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
# STEP 3: Display results
|
|
319
|
+
display_wallet_creation_result(
|
|
320
|
+
response,
|
|
321
|
+
copy_mnemonic=copy_mnemonic,
|
|
322
|
+
owner_name=request.owner_address,
|
|
323
|
+
owner_address=coldkey_address,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
except Exception as e:
|
|
327
|
+
from ...errors.handlers import handle_wallet_error
|
|
328
|
+
from ...ui.display import print_error
|
|
329
|
+
|
|
330
|
+
error = handle_wallet_error(e, "update")
|
|
331
|
+
print_error(error.message, suggestions=error.suggestions)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def generate_handler(
|
|
335
|
+
coldkey_name: Optional[str] = None,
|
|
336
|
+
hotkey_name: Optional[str] = None,
|
|
337
|
+
key_type: Optional[str] = None,
|
|
338
|
+
password: Optional[str] = None,
|
|
339
|
+
copy_mnemonic: bool = False,
|
|
340
|
+
):
|
|
341
|
+
"""Handle wallet generation command - creates both coldkey and hotkey at once.
|
|
342
|
+
|
|
343
|
+
Provides comprehensive error handling with clear feedback on what succeeded and what failed.
|
|
344
|
+
"""
|
|
345
|
+
from ...ui.display import print_error, print_info, print_warning
|
|
346
|
+
from ...utils.wallet.crypto import get_wallet_info_by_name, coldkey_has_hotkey
|
|
347
|
+
from ...ui.prompts import confirm_prompt
|
|
348
|
+
|
|
349
|
+
# Track creation state
|
|
350
|
+
creation_state = {
|
|
351
|
+
"coldkey_created": False,
|
|
352
|
+
"hotkey_created": False,
|
|
353
|
+
"coldkey_name": None,
|
|
354
|
+
"hotkey_name": None,
|
|
355
|
+
"coldkey_address": None,
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
try:
|
|
359
|
+
# STEP 0: Pre-validation - check for conflicts before starting
|
|
360
|
+
console.print("[bold cyan]Creating new wallet (coldkey + hotkey)[/bold cyan]\n")
|
|
361
|
+
|
|
362
|
+
# Collect input parameters for coldkey (this will prompt if not provided)
|
|
363
|
+
coldkey_request = prompt_wallet_creation(
|
|
364
|
+
name=coldkey_name,
|
|
365
|
+
key_type=key_type,
|
|
366
|
+
wallet_type="coldkey",
|
|
367
|
+
password=password,
|
|
368
|
+
)
|
|
369
|
+
creation_state["coldkey_name"] = coldkey_request.name
|
|
370
|
+
|
|
371
|
+
# Validate coldkey doesn't exist
|
|
372
|
+
try:
|
|
373
|
+
existing_coldkey = get_wallet_info_by_name(
|
|
374
|
+
coldkey_request.name, is_hotkey=False
|
|
375
|
+
)
|
|
376
|
+
print_error(
|
|
377
|
+
f"Coldkey '{coldkey_request.name}' already exists!",
|
|
378
|
+
suggestions=[
|
|
379
|
+
f"Use a different name for the coldkey",
|
|
380
|
+
f"Use the existing coldkey: htcli wallet list",
|
|
381
|
+
f"Delete the existing coldkey first: htcli wallet delete --coldkey {coldkey_request.name}",
|
|
382
|
+
],
|
|
383
|
+
)
|
|
384
|
+
return
|
|
385
|
+
except FileNotFoundError:
|
|
386
|
+
# Good - coldkey doesn't exist, can proceed
|
|
387
|
+
pass
|
|
388
|
+
|
|
389
|
+
# If hotkey name is already known, check for conflicts early
|
|
390
|
+
if hotkey_name:
|
|
391
|
+
# We need the coldkey address to check, but we haven't created it yet
|
|
392
|
+
# So we'll check after coldkey creation but before hotkey creation
|
|
393
|
+
pass
|
|
394
|
+
|
|
395
|
+
# STEP 1: Create coldkey first
|
|
396
|
+
console.print(info("📝 Step 1/2: Creating coldkey..."))
|
|
397
|
+
wallet_manager = WalletManager()
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
coldkey_result = wallet_manager.create_coldkey_wallet(
|
|
401
|
+
name=coldkey_request.name, password=coldkey_request.password
|
|
402
|
+
)
|
|
403
|
+
creation_state["coldkey_created"] = True
|
|
404
|
+
creation_state["coldkey_address"] = coldkey_result["data"]["ss58_address"]
|
|
405
|
+
console.print(
|
|
406
|
+
success(f"✅ Coldkey '{coldkey_request.name}' created successfully")
|
|
407
|
+
)
|
|
408
|
+
except Exception as e:
|
|
409
|
+
print_error(
|
|
410
|
+
f"❌ Failed to create coldkey '{coldkey_request.name}': {str(e)}",
|
|
411
|
+
suggestions=[
|
|
412
|
+
"• Check that the wallet name is valid",
|
|
413
|
+
"• Ensure you have write permissions to the wallet directory",
|
|
414
|
+
"• Try a different wallet name",
|
|
415
|
+
],
|
|
416
|
+
)
|
|
417
|
+
return
|
|
418
|
+
|
|
419
|
+
coldkey_address = creation_state["coldkey_address"]
|
|
420
|
+
coldkey_response = WalletCreateResponse(
|
|
421
|
+
name=coldkey_result["data"]["name"],
|
|
422
|
+
address=coldkey_result["data"]["ss58_address"],
|
|
423
|
+
public_key=coldkey_result["data"]["public_key"],
|
|
424
|
+
key_type=coldkey_result["data"]["key_type"],
|
|
425
|
+
wallet_type="coldkey",
|
|
426
|
+
mnemonic=coldkey_result["data"]["mnemonic"],
|
|
427
|
+
encrypted=coldkey_result["data"]["is_encrypted"],
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# STEP 2: Collect input parameters for hotkey and validate
|
|
431
|
+
console.print(info("\n📝 Step 2/2: Creating hotkey..."))
|
|
432
|
+
|
|
433
|
+
# Check if this specific coldkey already has a hotkey with the provided name
|
|
434
|
+
if hotkey_name and coldkey_has_hotkey(coldkey_address, hotkey_name):
|
|
435
|
+
print_error(
|
|
436
|
+
f"Hotkey '{hotkey_name}' already exists for coldkey '{coldkey_request.name}'",
|
|
437
|
+
suggestions=[
|
|
438
|
+
f"Choose a different hotkey name",
|
|
439
|
+
f"Delete the existing hotkey first: htcli wallet delete --coldkey {coldkey_request.name} --hotkey {hotkey_name}",
|
|
440
|
+
f"Use 'htcli wallet list' to see existing hotkeys",
|
|
441
|
+
],
|
|
442
|
+
)
|
|
443
|
+
# Offer cleanup option
|
|
444
|
+
console.print()
|
|
445
|
+
print_warning(
|
|
446
|
+
f"⚠️ Coldkey '{coldkey_request.name}' was created successfully, but hotkey creation failed."
|
|
447
|
+
)
|
|
448
|
+
if confirm_prompt(
|
|
449
|
+
f"Would you like to delete the coldkey '{coldkey_request.name}' that was just created?",
|
|
450
|
+
default=False,
|
|
451
|
+
):
|
|
452
|
+
try:
|
|
453
|
+
wallet_manager.delete_wallet(coldkey_request.name, is_hotkey=False)
|
|
454
|
+
console.print(
|
|
455
|
+
success(f"✅ Coldkey '{coldkey_request.name}' has been deleted")
|
|
456
|
+
)
|
|
457
|
+
except Exception as cleanup_error:
|
|
458
|
+
print_error(
|
|
459
|
+
f"❌ Failed to delete coldkey during cleanup: {str(cleanup_error)}",
|
|
460
|
+
suggestions=[
|
|
461
|
+
f"• Manually delete the coldkey: htcli wallet delete --coldkey {coldkey_request.name}",
|
|
462
|
+
],
|
|
463
|
+
)
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
hotkey_request = prompt_wallet_creation(
|
|
467
|
+
name=hotkey_name,
|
|
468
|
+
key_type=key_type,
|
|
469
|
+
wallet_type="hotkey",
|
|
470
|
+
coldkey_name=coldkey_request.name,
|
|
471
|
+
password=password, # Hotkeys typically don't use passwords, but allow it
|
|
472
|
+
)
|
|
473
|
+
creation_state["hotkey_name"] = hotkey_request.name
|
|
474
|
+
|
|
475
|
+
# STEP 3: Create hotkey
|
|
476
|
+
try:
|
|
477
|
+
hotkey_result = wallet_manager.create_hotkey_wallet(
|
|
478
|
+
name=hotkey_request.name,
|
|
479
|
+
owner_address=coldkey_address,
|
|
480
|
+
password=hotkey_request.password,
|
|
481
|
+
owner_coldkey_name=coldkey_request.name,
|
|
482
|
+
)
|
|
483
|
+
creation_state["hotkey_created"] = True
|
|
484
|
+
console.print(
|
|
485
|
+
success(f"✅ Hotkey '{hotkey_request.name}' created successfully")
|
|
486
|
+
)
|
|
487
|
+
except Exception as e:
|
|
488
|
+
print_error(
|
|
489
|
+
f"❌ Failed to create hotkey '{hotkey_request.name}': {str(e)}",
|
|
490
|
+
suggestions=[
|
|
491
|
+
f"• Check that the hotkey name is valid",
|
|
492
|
+
f"• Ensure the coldkey '{coldkey_request.name}' is valid",
|
|
493
|
+
f"• Try a different hotkey name",
|
|
494
|
+
],
|
|
495
|
+
)
|
|
496
|
+
# Offer cleanup option
|
|
497
|
+
console.print()
|
|
498
|
+
print_warning(
|
|
499
|
+
f"⚠️ Coldkey '{coldkey_request.name}' was created successfully, but hotkey creation failed."
|
|
500
|
+
)
|
|
501
|
+
if confirm_prompt(
|
|
502
|
+
f"Would you like to delete the coldkey '{coldkey_request.name}' that was just created?",
|
|
503
|
+
default=False,
|
|
504
|
+
):
|
|
505
|
+
try:
|
|
506
|
+
wallet_manager.delete_wallet(coldkey_request.name, is_hotkey=False)
|
|
507
|
+
console.print(
|
|
508
|
+
success(f"✅ Coldkey '{coldkey_request.name}' has been deleted")
|
|
509
|
+
)
|
|
510
|
+
except Exception as cleanup_error:
|
|
511
|
+
print_error(
|
|
512
|
+
f"❌ Failed to delete coldkey during cleanup: {str(cleanup_error)}",
|
|
513
|
+
suggestions=[
|
|
514
|
+
f"• Manually delete the coldkey: htcli wallet delete --coldkey {coldkey_request.name}",
|
|
515
|
+
],
|
|
516
|
+
)
|
|
517
|
+
return
|
|
518
|
+
|
|
519
|
+
hotkey_response = WalletCreateResponse(
|
|
520
|
+
name=hotkey_result["data"]["name"],
|
|
521
|
+
address=hotkey_result["data"]["ss58_address"],
|
|
522
|
+
public_key=hotkey_result["data"]["public_key"],
|
|
523
|
+
key_type=hotkey_result["data"]["key_type"],
|
|
524
|
+
wallet_type="hotkey",
|
|
525
|
+
mnemonic=hotkey_result["data"]["mnemonic"],
|
|
526
|
+
encrypted=hotkey_result["data"]["is_encrypted"],
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# STEP 4: Display unified results
|
|
530
|
+
console.print()
|
|
531
|
+
display_wallet_generation_result(
|
|
532
|
+
coldkey_response,
|
|
533
|
+
hotkey_response,
|
|
534
|
+
copy_mnemonic=copy_mnemonic,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
except KeyboardInterrupt:
|
|
538
|
+
# User cancelled - provide cleanup option if partial creation occurred
|
|
539
|
+
console.print()
|
|
540
|
+
print_warning("Operation cancelled")
|
|
541
|
+
|
|
542
|
+
if creation_state["coldkey_created"] and not creation_state["hotkey_created"]:
|
|
543
|
+
print_warning(
|
|
544
|
+
f"⚠️ Coldkey '{creation_state['coldkey_name']}' was created before cancellation."
|
|
545
|
+
)
|
|
546
|
+
if confirm_prompt(
|
|
547
|
+
f"Would you like to delete the coldkey '{creation_state['coldkey_name']}'?",
|
|
548
|
+
default=False,
|
|
549
|
+
):
|
|
550
|
+
try:
|
|
551
|
+
wallet_manager = WalletManager()
|
|
552
|
+
wallet_manager.delete_wallet(
|
|
553
|
+
creation_state["coldkey_name"], is_hotkey=False
|
|
554
|
+
)
|
|
555
|
+
console.print(
|
|
556
|
+
success(
|
|
557
|
+
f"✅ Coldkey '{creation_state['coldkey_name']}' has been deleted"
|
|
558
|
+
)
|
|
559
|
+
)
|
|
560
|
+
except Exception as cleanup_error:
|
|
561
|
+
print_error(
|
|
562
|
+
f"❌ Failed to delete coldkey during cleanup: {str(cleanup_error)}",
|
|
563
|
+
suggestions=[
|
|
564
|
+
f"• Manually delete the coldkey: htcli wallet delete --coldkey {creation_state['coldkey_name']}",
|
|
565
|
+
],
|
|
566
|
+
)
|
|
567
|
+
return
|
|
568
|
+
|
|
569
|
+
except Exception as e:
|
|
570
|
+
from ...errors.handlers import handle_wallet_error
|
|
571
|
+
|
|
572
|
+
# Provide detailed error information based on what was created
|
|
573
|
+
error = handle_wallet_error(e, "generate")
|
|
574
|
+
|
|
575
|
+
console.print()
|
|
576
|
+
if creation_state["coldkey_created"] and not creation_state["hotkey_created"]:
|
|
577
|
+
print_error(
|
|
578
|
+
f"❌ {error.message}",
|
|
579
|
+
suggestions=error.suggestions
|
|
580
|
+
+ [
|
|
581
|
+
f"⚠️ Note: Coldkey '{creation_state['coldkey_name']}' was created successfully",
|
|
582
|
+
f"• You can delete it: htcli wallet delete --coldkey {creation_state['coldkey_name']}",
|
|
583
|
+
f"• Or use it with a different hotkey name",
|
|
584
|
+
],
|
|
585
|
+
)
|
|
586
|
+
# Offer cleanup
|
|
587
|
+
if confirm_prompt(
|
|
588
|
+
f"Would you like to delete the coldkey '{creation_state['coldkey_name']}' that was created?",
|
|
589
|
+
default=False,
|
|
590
|
+
):
|
|
591
|
+
try:
|
|
592
|
+
wallet_manager = WalletManager()
|
|
593
|
+
wallet_manager.delete_wallet(
|
|
594
|
+
creation_state["coldkey_name"], is_hotkey=False
|
|
595
|
+
)
|
|
596
|
+
console.print(
|
|
597
|
+
success(
|
|
598
|
+
f"✅ Coldkey '{creation_state['coldkey_name']}' has been deleted"
|
|
599
|
+
)
|
|
600
|
+
)
|
|
601
|
+
except Exception as cleanup_error:
|
|
602
|
+
print_error(
|
|
603
|
+
f"❌ Failed to delete coldkey during cleanup: {str(cleanup_error)}",
|
|
604
|
+
suggestions=[
|
|
605
|
+
f"• Manually delete the coldkey: htcli wallet delete --coldkey {creation_state['coldkey_name']}",
|
|
606
|
+
],
|
|
607
|
+
)
|
|
608
|
+
elif creation_state["coldkey_created"] and creation_state["hotkey_created"]:
|
|
609
|
+
# Both created but something else failed - unlikely but handle it
|
|
610
|
+
print_error(
|
|
611
|
+
f"❌ {error.message}",
|
|
612
|
+
suggestions=error.suggestions
|
|
613
|
+
+ [
|
|
614
|
+
"⚠️ Note: Both coldkey and hotkey were created successfully",
|
|
615
|
+
"• The wallets should be available in your wallet list",
|
|
616
|
+
],
|
|
617
|
+
)
|
|
618
|
+
else:
|
|
619
|
+
# Nothing was created
|
|
620
|
+
print_error(error.message, suggestions=error.suggestions)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def restore_coldkey_handler(
|
|
624
|
+
name: Optional[str] = None,
|
|
625
|
+
private_key: Optional[str] = None,
|
|
626
|
+
mnemonic: Optional[str] = None,
|
|
627
|
+
key_type: Optional[str] = None,
|
|
628
|
+
password: Optional[str] = None,
|
|
629
|
+
):
|
|
630
|
+
"""Handle coldkey restoration command."""
|
|
631
|
+
try:
|
|
632
|
+
# Early check: if name is provided via CLI, check existence before prompting
|
|
633
|
+
from ...ui.display import print_error
|
|
634
|
+
from ...utils.wallet.crypto import get_wallet_info_by_name
|
|
635
|
+
|
|
636
|
+
if name:
|
|
637
|
+
try:
|
|
638
|
+
get_wallet_info_by_name(name, is_hotkey=False)
|
|
639
|
+
# If we get here, wallet exists
|
|
640
|
+
print_error(
|
|
641
|
+
f"Wallet '{name}' already exists! Please choose a different name or delete the existing wallet first."
|
|
642
|
+
)
|
|
643
|
+
return
|
|
644
|
+
except FileNotFoundError:
|
|
645
|
+
# Good - wallet doesn't exist, can proceed
|
|
646
|
+
pass
|
|
647
|
+
|
|
648
|
+
# STEP 1: Collect input parameters
|
|
649
|
+
request = prompt_wallet_restoration(
|
|
650
|
+
name=name,
|
|
651
|
+
private_key=private_key,
|
|
652
|
+
mnemonic=mnemonic,
|
|
653
|
+
key_type=key_type,
|
|
654
|
+
wallet_type="coldkey",
|
|
655
|
+
password=password,
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
# STEP 2: Execute via client layer
|
|
659
|
+
wallet_manager = WalletManager()
|
|
660
|
+
if request.private_key:
|
|
661
|
+
result = wallet_manager.import_coldkey_from_private_key(
|
|
662
|
+
name=request.name,
|
|
663
|
+
private_key=request.private_key,
|
|
664
|
+
key_type=request.key_type,
|
|
665
|
+
password=request.password,
|
|
666
|
+
)
|
|
667
|
+
keypair_info = type(
|
|
668
|
+
"KeypairInfo", (), result["data"]
|
|
669
|
+
)() # Convert dict to object
|
|
670
|
+
import_method = "private key"
|
|
671
|
+
else:
|
|
672
|
+
result = wallet_manager.import_coldkey_from_mnemonic(
|
|
673
|
+
name=request.name,
|
|
674
|
+
mnemonic=request.mnemonic,
|
|
675
|
+
key_type=request.key_type,
|
|
676
|
+
password=request.password,
|
|
677
|
+
)
|
|
678
|
+
keypair_info = type(
|
|
679
|
+
"KeypairInfo", (), result["data"]
|
|
680
|
+
)() # Convert dict to object
|
|
681
|
+
import_method = "mnemonic phrase"
|
|
682
|
+
response = WalletCreateResponse(
|
|
683
|
+
name=keypair_info.name,
|
|
684
|
+
address=keypair_info.ss58_address,
|
|
685
|
+
public_key=keypair_info.public_key,
|
|
686
|
+
key_type=keypair_info.key_type,
|
|
687
|
+
wallet_type="coldkey",
|
|
688
|
+
mnemonic=getattr(
|
|
689
|
+
keypair_info, "mnemonic", None
|
|
690
|
+
), # Include mnemonic if available
|
|
691
|
+
encrypted=(request.password is not None),
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
# Check if there are any existing hotkeys that might be associated with this coldkey
|
|
695
|
+
# (This can happen if hotkey files weren't fully deleted)
|
|
696
|
+
from ...utils.wallet.crypto import list_keys, coldkey_has_hotkey
|
|
697
|
+
|
|
698
|
+
coldkey_address = keypair_info.ss58_address
|
|
699
|
+
all_keys = list_keys()
|
|
700
|
+
existing_hotkeys = [
|
|
701
|
+
k
|
|
702
|
+
for k in all_keys
|
|
703
|
+
if k.get("is_hotkey", False)
|
|
704
|
+
and (
|
|
705
|
+
k.get("owner_address") == coldkey_address
|
|
706
|
+
or k.get("owner_coldkey_name") == keypair_info.name
|
|
707
|
+
)
|
|
708
|
+
]
|
|
709
|
+
|
|
710
|
+
if existing_hotkeys:
|
|
711
|
+
console.print()
|
|
712
|
+
console.print(
|
|
713
|
+
warning(
|
|
714
|
+
f"⚠️ Found {len(existing_hotkeys)} existing hotkey(s) that may be associated with this coldkey:"
|
|
715
|
+
)
|
|
716
|
+
)
|
|
717
|
+
for hk in existing_hotkeys:
|
|
718
|
+
console.print(f" • {hk.get('name')} ({hk.get('ss58_address', 'N/A')})")
|
|
719
|
+
console.print(
|
|
720
|
+
info(
|
|
721
|
+
"Note: Restoring a coldkey does NOT automatically create hotkeys. "
|
|
722
|
+
"These hotkeys were already present. Use 'htcli wallet restore-hotkey' to restore hotkeys separately."
|
|
723
|
+
)
|
|
724
|
+
)
|
|
725
|
+
console.print()
|
|
726
|
+
|
|
727
|
+
# STEP 3: Display results
|
|
728
|
+
display_wallet_restoration_result(response, import_method)
|
|
729
|
+
|
|
730
|
+
except Exception as e:
|
|
731
|
+
from ...errors.handlers import handle_wallet_error
|
|
732
|
+
from ...ui.display import print_error
|
|
733
|
+
|
|
734
|
+
error = handle_wallet_error(e, "update")
|
|
735
|
+
print_error(error.message, suggestions=error.suggestions)
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def restore_hotkey_handler(
|
|
739
|
+
name: Optional[str] = None,
|
|
740
|
+
private_key: Optional[str] = None,
|
|
741
|
+
mnemonic: Optional[str] = None,
|
|
742
|
+
owner_name: Optional[
|
|
743
|
+
str
|
|
744
|
+
] = None, # Keep for backward compatibility, maps to coldkey_name
|
|
745
|
+
key_type: Optional[str] = None,
|
|
746
|
+
password: Optional[str] = None,
|
|
747
|
+
):
|
|
748
|
+
"""Handle hotkey restoration command."""
|
|
749
|
+
try:
|
|
750
|
+
# STEP 1: Collect input parameters
|
|
751
|
+
request = prompt_wallet_restoration(
|
|
752
|
+
name=name,
|
|
753
|
+
private_key=private_key,
|
|
754
|
+
mnemonic=mnemonic,
|
|
755
|
+
key_type=key_type,
|
|
756
|
+
wallet_type="hotkey",
|
|
757
|
+
coldkey_name=owner_name, # Map owner_name to coldkey_name
|
|
758
|
+
password=password,
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
# Resolve coldkey name to address
|
|
762
|
+
coldkey_info = get_wallet_info_by_name(request.owner_address, is_hotkey=False)
|
|
763
|
+
if coldkey_info.get("is_hotkey", False):
|
|
764
|
+
raise ValueError(
|
|
765
|
+
f"'{request.owner_address}' is a hotkey. Please provide a coldkey name."
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
# Check if this specific coldkey already has a hotkey with this name
|
|
769
|
+
from ...ui.display import print_error
|
|
770
|
+
from ...utils.wallet.crypto import coldkey_has_hotkey
|
|
771
|
+
|
|
772
|
+
coldkey_address = coldkey_info.get("ss58_address") or coldkey_info.get(
|
|
773
|
+
"address"
|
|
774
|
+
)
|
|
775
|
+
if not coldkey_address:
|
|
776
|
+
raise ValueError(
|
|
777
|
+
f"Could not determine address for coldkey '{request.owner_address}'"
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
if coldkey_has_hotkey(coldkey_address, request.name):
|
|
781
|
+
print_error(
|
|
782
|
+
f"Coldkey '{request.owner_address}' already has a hotkey named '{request.name}'. "
|
|
783
|
+
"Please choose a different name for this hotkey."
|
|
784
|
+
)
|
|
785
|
+
return
|
|
786
|
+
|
|
787
|
+
# STEP 2: Execute via client layer
|
|
788
|
+
wallet_manager = WalletManager()
|
|
789
|
+
if request.private_key:
|
|
790
|
+
result = wallet_manager.import_hotkey_from_private_key(
|
|
791
|
+
name=request.name,
|
|
792
|
+
private_key=request.private_key,
|
|
793
|
+
owner_address=coldkey_address,
|
|
794
|
+
key_type=request.key_type,
|
|
795
|
+
password=request.password,
|
|
796
|
+
)
|
|
797
|
+
keypair_info = type(
|
|
798
|
+
"KeypairInfo", (), result["data"]
|
|
799
|
+
)() # Convert dict to object
|
|
800
|
+
import_method = "private key"
|
|
801
|
+
else:
|
|
802
|
+
result = wallet_manager.import_hotkey_from_mnemonic(
|
|
803
|
+
name=request.name,
|
|
804
|
+
mnemonic=request.mnemonic,
|
|
805
|
+
owner_address=coldkey_address,
|
|
806
|
+
key_type=request.key_type,
|
|
807
|
+
password=request.password,
|
|
808
|
+
)
|
|
809
|
+
keypair_info = type(
|
|
810
|
+
"KeypairInfo", (), result["data"]
|
|
811
|
+
)() # Convert dict to object
|
|
812
|
+
import_method = "mnemonic phrase"
|
|
813
|
+
|
|
814
|
+
# Create response model - use evm_address if available, otherwise ss58_address
|
|
815
|
+
address = (
|
|
816
|
+
getattr(keypair_info, "evm_address", None) or keypair_info.ss58_address
|
|
817
|
+
)
|
|
818
|
+
response = WalletCreateResponse(
|
|
819
|
+
name=keypair_info.name,
|
|
820
|
+
address=address,
|
|
821
|
+
public_key=keypair_info.public_key,
|
|
822
|
+
key_type=keypair_info.key_type,
|
|
823
|
+
wallet_type="hotkey",
|
|
824
|
+
mnemonic=getattr(
|
|
825
|
+
keypair_info, "mnemonic", None
|
|
826
|
+
), # Include mnemonic if available
|
|
827
|
+
encrypted=(request.password is not None),
|
|
828
|
+
)
|
|
829
|
+
|
|
830
|
+
# STEP 3: Display results
|
|
831
|
+
display_wallet_restoration_result(
|
|
832
|
+
response,
|
|
833
|
+
import_method,
|
|
834
|
+
owner_name=request.owner_address,
|
|
835
|
+
owner_address=coldkey_address,
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
except Exception as e:
|
|
839
|
+
from ...errors.handlers import handle_wallet_error
|
|
840
|
+
from ...ui.display import print_error
|
|
841
|
+
|
|
842
|
+
error = handle_wallet_error(e, "restore")
|
|
843
|
+
print_error(error.message, suggestions=error.suggestions)
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def import_preseeded_handler(
|
|
847
|
+
preseeded_name: str,
|
|
848
|
+
custom_name: Optional[str] = None,
|
|
849
|
+
key_type: Optional[str] = None,
|
|
850
|
+
):
|
|
851
|
+
"""Handle preseeded wallet import command."""
|
|
852
|
+
try:
|
|
853
|
+
# STEP 1: Collect input parameters
|
|
854
|
+
request = prompt_preseeded_wallet(
|
|
855
|
+
preseeded_name=preseeded_name, custom_name=custom_name, key_type=key_type
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
# STEP 2: Execute via client layer
|
|
859
|
+
wallet_manager = WalletManager()
|
|
860
|
+
result = wallet_manager.import_preseeded_wallet(
|
|
861
|
+
preseeded_name=request.preseeded_name,
|
|
862
|
+
wallet_name=request.custom_name or request.preseeded_name,
|
|
863
|
+
password=request.password,
|
|
864
|
+
)
|
|
865
|
+
keypair_info = type(
|
|
866
|
+
"KeypairInfo", (), result["data"]
|
|
867
|
+
)() # Convert dict to object
|
|
868
|
+
|
|
869
|
+
# Create response model
|
|
870
|
+
response = WalletCreateResponse(
|
|
871
|
+
name=keypair_info.name,
|
|
872
|
+
address=keypair_info.ss58_address,
|
|
873
|
+
public_key=keypair_info.public_key,
|
|
874
|
+
key_type=keypair_info.key_type,
|
|
875
|
+
wallet_type="coldkey",
|
|
876
|
+
mnemonic=getattr(
|
|
877
|
+
keypair_info, "mnemonic", None
|
|
878
|
+
), # Include mnemonic if available
|
|
879
|
+
encrypted=(request.password is not None),
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
# STEP 3: Display results
|
|
883
|
+
display_wallet_creation_result(response, preseeded_name=preseeded_name)
|
|
884
|
+
|
|
885
|
+
except Exception as e:
|
|
886
|
+
from ...errors.handlers import handle_wallet_error
|
|
887
|
+
from ...ui.display import print_error
|
|
888
|
+
|
|
889
|
+
error = handle_wallet_error(e, "update")
|
|
890
|
+
print_error(error.message, suggestions=error.suggestions)
|
|
891
|
+
|
|
892
|
+
|
|
893
|
+
def list_wallets_handler(
|
|
894
|
+
format_type: str = "tree",
|
|
895
|
+
show_balances: bool = False,
|
|
896
|
+
):
|
|
897
|
+
"""Handle wallet listing command."""
|
|
898
|
+
try:
|
|
899
|
+
# STEP 1: No input needed for listing
|
|
900
|
+
|
|
901
|
+
# STEP 2: Execute via client layer
|
|
902
|
+
wallet_manager = WalletManager()
|
|
903
|
+
result = wallet_manager.list_wallets()
|
|
904
|
+
wallets = result["data"]
|
|
905
|
+
|
|
906
|
+
# Count wallet types
|
|
907
|
+
coldkeys = sum(1 for w in wallets if not w.get("is_hotkey", False))
|
|
908
|
+
hotkeys = sum(1 for w in wallets if w.get("is_hotkey", False))
|
|
909
|
+
|
|
910
|
+
# Create response model
|
|
911
|
+
response = WalletListResponse(
|
|
912
|
+
wallets=wallets,
|
|
913
|
+
total_count=len(wallets),
|
|
914
|
+
coldkeys=coldkeys,
|
|
915
|
+
hotkeys=hotkeys,
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
# STEP 3: Display results
|
|
919
|
+
display_wallet_list(response, format_type=format_type)
|
|
920
|
+
|
|
921
|
+
except Exception as e:
|
|
922
|
+
from ...errors.handlers import handle_wallet_error
|
|
923
|
+
from ...ui.display import print_error
|
|
924
|
+
|
|
925
|
+
error = handle_wallet_error(e, "update")
|
|
926
|
+
print_error(error.message, suggestions=error.suggestions)
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
def status_handler(
|
|
930
|
+
format_type: str = "table",
|
|
931
|
+
check_balances: bool = False,
|
|
932
|
+
):
|
|
933
|
+
"""Handle wallet status command."""
|
|
934
|
+
try:
|
|
935
|
+
# STEP 1: No input needed for status
|
|
936
|
+
|
|
937
|
+
# STEP 2: Execute via client layer
|
|
938
|
+
wallet_manager = WalletManager()
|
|
939
|
+
result = wallet_manager.list_wallets()
|
|
940
|
+
wallets = result["data"]
|
|
941
|
+
|
|
942
|
+
# Create response model
|
|
943
|
+
response = WalletStatusResponse(
|
|
944
|
+
wallets=wallets, total_keys=len(wallets), total_addresses=len(wallets)
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
# STEP 3: Display results
|
|
948
|
+
display_wallet_status(response, format_type=format_type)
|
|
949
|
+
|
|
950
|
+
except Exception as e:
|
|
951
|
+
from ...errors.handlers import handle_wallet_error
|
|
952
|
+
from ...ui.display import print_error
|
|
953
|
+
|
|
954
|
+
error = handle_wallet_error(e, "update")
|
|
955
|
+
print_error(error.message, suggestions=error.suggestions)
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
def delete_handler(
|
|
959
|
+
coldkey: Optional[str] = None,
|
|
960
|
+
hotkey: Optional[str] = None,
|
|
961
|
+
force: bool = False,
|
|
962
|
+
):
|
|
963
|
+
"""Handle wallet deletion command.
|
|
964
|
+
|
|
965
|
+
Args:
|
|
966
|
+
coldkey: Coldkey name to delete (required for coldkey deletion)
|
|
967
|
+
hotkey: Hotkey name to delete (requires coldkey to specify owner)
|
|
968
|
+
force: Skip confirmation prompts
|
|
969
|
+
"""
|
|
970
|
+
try:
|
|
971
|
+
# Validate parameters
|
|
972
|
+
if not coldkey and not hotkey:
|
|
973
|
+
from ...ui.display import print_error
|
|
974
|
+
|
|
975
|
+
print_error(
|
|
976
|
+
"Either --coldkey or both --coldkey and --hotkey must be provided",
|
|
977
|
+
suggestions=[
|
|
978
|
+
"To delete a coldkey: htcli wallet delete --coldkey <name>",
|
|
979
|
+
"To delete a hotkey: htcli wallet delete --coldkey <owner_name> --hotkey <name>",
|
|
980
|
+
],
|
|
981
|
+
)
|
|
982
|
+
return
|
|
983
|
+
|
|
984
|
+
if hotkey and not coldkey:
|
|
985
|
+
# Check if this is an orphaned hotkey (owner doesn't exist)
|
|
986
|
+
wallet_manager = WalletManager()
|
|
987
|
+
all_keys = list_keys()
|
|
988
|
+
|
|
989
|
+
# Find all hotkeys with this name
|
|
990
|
+
matching_hotkeys = [
|
|
991
|
+
k
|
|
992
|
+
for k in all_keys
|
|
993
|
+
if k.get("is_hotkey", False) and k.get("name") == hotkey
|
|
994
|
+
]
|
|
995
|
+
|
|
996
|
+
if not matching_hotkeys:
|
|
997
|
+
from ...ui.display import print_error
|
|
998
|
+
|
|
999
|
+
print_error(
|
|
1000
|
+
f"Hotkey '{hotkey}' not found",
|
|
1001
|
+
suggestions=[
|
|
1002
|
+
"Use: htcli wallet list to see all wallets",
|
|
1003
|
+
],
|
|
1004
|
+
)
|
|
1005
|
+
return
|
|
1006
|
+
|
|
1007
|
+
# Check if any of the matching hotkeys are orphaned
|
|
1008
|
+
orphaned_hotkeys = []
|
|
1009
|
+
valid_hotkeys = []
|
|
1010
|
+
|
|
1011
|
+
# Get all coldkey addresses for comparison
|
|
1012
|
+
coldkey_addresses = {
|
|
1013
|
+
k.get("evm_address") or k.get("ss58_address") or k.get("address")
|
|
1014
|
+
for k in all_keys
|
|
1015
|
+
if not k.get("is_hotkey", False)
|
|
1016
|
+
}
|
|
1017
|
+
coldkey_names = {
|
|
1018
|
+
k.get("name") for k in all_keys if not k.get("is_hotkey", False)
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
for hk in matching_hotkeys:
|
|
1022
|
+
owner_address = hk.get("owner_address")
|
|
1023
|
+
owner_coldkey_name = hk.get("owner_coldkey_name")
|
|
1024
|
+
|
|
1025
|
+
# Check if owner exists
|
|
1026
|
+
owner_found = False
|
|
1027
|
+
if owner_address and owner_address in coldkey_addresses:
|
|
1028
|
+
owner_found = True
|
|
1029
|
+
elif owner_coldkey_name and owner_coldkey_name in coldkey_names:
|
|
1030
|
+
owner_found = True
|
|
1031
|
+
|
|
1032
|
+
if owner_found:
|
|
1033
|
+
valid_hotkeys.append(hk)
|
|
1034
|
+
else:
|
|
1035
|
+
orphaned_hotkeys.append(hk)
|
|
1036
|
+
|
|
1037
|
+
if orphaned_hotkeys and not valid_hotkeys:
|
|
1038
|
+
# All matching hotkeys are orphaned - allow deletion without coldkey
|
|
1039
|
+
# We'll handle this in the deletion logic below
|
|
1040
|
+
pass
|
|
1041
|
+
elif valid_hotkeys:
|
|
1042
|
+
# At least one hotkey has a valid owner - require coldkey for disambiguation
|
|
1043
|
+
from ...ui.display import print_error
|
|
1044
|
+
|
|
1045
|
+
if len(valid_hotkeys) > 1:
|
|
1046
|
+
print_error(
|
|
1047
|
+
f"Found {len(valid_hotkeys)} hotkeys named '{hotkey}' with valid owners. Please specify --coldkey to disambiguate.",
|
|
1048
|
+
suggestions=[
|
|
1049
|
+
"Use: htcli wallet delete --coldkey <owner_name> --hotkey <name>",
|
|
1050
|
+
"Or use: htcli wallet list to see all wallets",
|
|
1051
|
+
],
|
|
1052
|
+
)
|
|
1053
|
+
else:
|
|
1054
|
+
print_error(
|
|
1055
|
+
f"Hotkey '{hotkey}' has a valid owner. Please specify --coldkey to delete it.",
|
|
1056
|
+
suggestions=[
|
|
1057
|
+
f"Use: htcli wallet delete --coldkey <owner_name> --hotkey {hotkey}",
|
|
1058
|
+
],
|
|
1059
|
+
)
|
|
1060
|
+
return
|
|
1061
|
+
# If we get here, all matching hotkeys are orphaned - continue with deletion
|
|
1062
|
+
|
|
1063
|
+
# STEP 1: Collect input parameters
|
|
1064
|
+
# If coldkey is provided but doesn't exist, check if hotkey is orphaned
|
|
1065
|
+
if hotkey and coldkey:
|
|
1066
|
+
try:
|
|
1067
|
+
# Try to verify coldkey exists
|
|
1068
|
+
wallet_manager_temp = WalletManager()
|
|
1069
|
+
wallet_manager_temp.get_wallet_info(coldkey, is_hotkey=False)
|
|
1070
|
+
except (ValueError, FileNotFoundError):
|
|
1071
|
+
# Coldkey doesn't exist - check if hotkey is orphaned
|
|
1072
|
+
all_keys = list_keys()
|
|
1073
|
+
matching_hotkeys = [
|
|
1074
|
+
k
|
|
1075
|
+
for k in all_keys
|
|
1076
|
+
if k.get("is_hotkey", False) and k.get("name") == hotkey
|
|
1077
|
+
]
|
|
1078
|
+
|
|
1079
|
+
if matching_hotkeys:
|
|
1080
|
+
# Get all coldkey addresses and names for comparison
|
|
1081
|
+
coldkey_addresses = {
|
|
1082
|
+
k.get("evm_address")
|
|
1083
|
+
or k.get("ss58_address")
|
|
1084
|
+
or k.get("address")
|
|
1085
|
+
for k in all_keys
|
|
1086
|
+
if not k.get("is_hotkey", False)
|
|
1087
|
+
}
|
|
1088
|
+
coldkey_names = {
|
|
1089
|
+
k.get("name") for k in all_keys if not k.get("is_hotkey", False)
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
# Check if any matching hotkey is orphaned
|
|
1093
|
+
is_orphaned = True
|
|
1094
|
+
for hk in matching_hotkeys:
|
|
1095
|
+
owner_address = hk.get("owner_address")
|
|
1096
|
+
owner_coldkey_name = hk.get("owner_coldkey_name")
|
|
1097
|
+
|
|
1098
|
+
if owner_address and owner_address in coldkey_addresses:
|
|
1099
|
+
is_orphaned = False
|
|
1100
|
+
break
|
|
1101
|
+
elif owner_coldkey_name and owner_coldkey_name in coldkey_names:
|
|
1102
|
+
is_orphaned = False
|
|
1103
|
+
break
|
|
1104
|
+
|
|
1105
|
+
if is_orphaned:
|
|
1106
|
+
# Hotkey is orphaned - treat as if no coldkey was provided
|
|
1107
|
+
from ...ui.display import print_warning
|
|
1108
|
+
|
|
1109
|
+
console.print()
|
|
1110
|
+
print_warning(
|
|
1111
|
+
f"Coldkey '{coldkey}' does not exist, but hotkey '{hotkey}' appears to be orphaned. "
|
|
1112
|
+
"Proceeding with orphaned hotkey deletion."
|
|
1113
|
+
)
|
|
1114
|
+
console.print()
|
|
1115
|
+
coldkey = None # Clear coldkey so it's treated as orphaned
|
|
1116
|
+
|
|
1117
|
+
try:
|
|
1118
|
+
request = prompt_wallet_deletion(
|
|
1119
|
+
coldkey=coldkey, hotkey=hotkey, force=force
|
|
1120
|
+
)
|
|
1121
|
+
except KeyboardInterrupt:
|
|
1122
|
+
# User cancelled the deletion
|
|
1123
|
+
from ...ui.display import print_info
|
|
1124
|
+
|
|
1125
|
+
console.print()
|
|
1126
|
+
print_info("✋ Deletion cancelled - no wallets were deleted")
|
|
1127
|
+
return
|
|
1128
|
+
|
|
1129
|
+
# STEP 2: Execute via client layer
|
|
1130
|
+
wallet_manager = WalletManager()
|
|
1131
|
+
total_deleted = 0
|
|
1132
|
+
coldkeys_deleted = 0
|
|
1133
|
+
hotkeys_deleted = 0
|
|
1134
|
+
associated_hotkeys_deleted = 0
|
|
1135
|
+
associated_hotkey_details = [] # List of dicts with name and owner
|
|
1136
|
+
successfully_deleted = []
|
|
1137
|
+
|
|
1138
|
+
try:
|
|
1139
|
+
# Determine what we're deleting
|
|
1140
|
+
if hotkey:
|
|
1141
|
+
# Deleting a hotkey
|
|
1142
|
+
wallet_name = hotkey
|
|
1143
|
+
|
|
1144
|
+
if coldkey:
|
|
1145
|
+
# Owner specified - use it for disambiguation
|
|
1146
|
+
owner_coldkey_name = coldkey
|
|
1147
|
+
|
|
1148
|
+
# Get owner address from coldkey name
|
|
1149
|
+
try:
|
|
1150
|
+
coldkey_info_result = wallet_manager.get_wallet_info(
|
|
1151
|
+
coldkey,
|
|
1152
|
+
is_hotkey=False,
|
|
1153
|
+
)
|
|
1154
|
+
owner_address = (
|
|
1155
|
+
coldkey_info_result["data"].get("evm_address")
|
|
1156
|
+
or coldkey_info_result["data"].get("ss58_address")
|
|
1157
|
+
or coldkey_info_result["data"].get("address")
|
|
1158
|
+
)
|
|
1159
|
+
except (ValueError, FileNotFoundError) as e:
|
|
1160
|
+
console.print(
|
|
1161
|
+
warning(f"❌ Coldkey '{coldkey}' does not exist.")
|
|
1162
|
+
)
|
|
1163
|
+
return
|
|
1164
|
+
|
|
1165
|
+
# Check if hotkey exists
|
|
1166
|
+
try:
|
|
1167
|
+
wallet_info_result = wallet_manager.get_wallet_info(
|
|
1168
|
+
wallet_name,
|
|
1169
|
+
is_hotkey=True,
|
|
1170
|
+
owner_address=owner_address,
|
|
1171
|
+
owner_coldkey_name=owner_coldkey_name,
|
|
1172
|
+
)
|
|
1173
|
+
except (ValueError, FileNotFoundError) as e:
|
|
1174
|
+
console.print(
|
|
1175
|
+
warning(
|
|
1176
|
+
f"❌ Hotkey '{hotkey}' owned by '{coldkey}' does not exist."
|
|
1177
|
+
)
|
|
1178
|
+
)
|
|
1179
|
+
return
|
|
1180
|
+
|
|
1181
|
+
# Delete hotkey
|
|
1182
|
+
result = wallet_manager.delete_wallet(
|
|
1183
|
+
wallet_name,
|
|
1184
|
+
is_hotkey=True,
|
|
1185
|
+
owner_address=owner_address,
|
|
1186
|
+
owner_coldkey_name=owner_coldkey_name,
|
|
1187
|
+
)
|
|
1188
|
+
else:
|
|
1189
|
+
# No owner specified - this is an orphaned hotkey
|
|
1190
|
+
# Find the hotkey without owner context
|
|
1191
|
+
try:
|
|
1192
|
+
# Try to get wallet info - this will work for orphaned hotkeys
|
|
1193
|
+
# since they don't have a valid owner to disambiguate
|
|
1194
|
+
all_keys = list_keys()
|
|
1195
|
+
matching_hotkeys = [
|
|
1196
|
+
k
|
|
1197
|
+
for k in all_keys
|
|
1198
|
+
if k.get("is_hotkey", False) and k.get("name") == hotkey
|
|
1199
|
+
]
|
|
1200
|
+
|
|
1201
|
+
if not matching_hotkeys:
|
|
1202
|
+
console.print(warning(f"❌ Hotkey '{hotkey}' not found."))
|
|
1203
|
+
return
|
|
1204
|
+
|
|
1205
|
+
# For orphaned hotkeys, use the owner_address from the file itself
|
|
1206
|
+
# (even though the owner doesn't exist)
|
|
1207
|
+
hotkey_info = matching_hotkeys[0] # Use first match
|
|
1208
|
+
owner_address = hotkey_info.get("owner_address")
|
|
1209
|
+
owner_coldkey_name = hotkey_info.get("owner_coldkey_name")
|
|
1210
|
+
|
|
1211
|
+
# Get wallet info using the owner_address from the file
|
|
1212
|
+
wallet_info_result = wallet_manager.get_wallet_info(
|
|
1213
|
+
wallet_name,
|
|
1214
|
+
is_hotkey=True,
|
|
1215
|
+
owner_address=owner_address,
|
|
1216
|
+
owner_coldkey_name=owner_coldkey_name,
|
|
1217
|
+
)
|
|
1218
|
+
|
|
1219
|
+
# Delete orphaned hotkey
|
|
1220
|
+
result = wallet_manager.delete_wallet(
|
|
1221
|
+
wallet_name,
|
|
1222
|
+
is_hotkey=True,
|
|
1223
|
+
owner_address=owner_address,
|
|
1224
|
+
owner_coldkey_name=owner_coldkey_name,
|
|
1225
|
+
)
|
|
1226
|
+
except (ValueError, FileNotFoundError) as e:
|
|
1227
|
+
console.print(warning(f"❌ Hotkey '{hotkey}' not found."))
|
|
1228
|
+
return
|
|
1229
|
+
|
|
1230
|
+
hotkeys_deleted += 1
|
|
1231
|
+
total_deleted += 1
|
|
1232
|
+
successfully_deleted.append(wallet_name)
|
|
1233
|
+
else:
|
|
1234
|
+
# Deleting a coldkey
|
|
1235
|
+
wallet_name = coldkey
|
|
1236
|
+
|
|
1237
|
+
# Check if coldkey exists
|
|
1238
|
+
try:
|
|
1239
|
+
wallet_info_result = wallet_manager.get_wallet_info(
|
|
1240
|
+
wallet_name,
|
|
1241
|
+
is_hotkey=False,
|
|
1242
|
+
)
|
|
1243
|
+
except (ValueError, FileNotFoundError) as e:
|
|
1244
|
+
console.print(warning(f"❌ Coldkey '{coldkey}' does not exist."))
|
|
1245
|
+
return
|
|
1246
|
+
|
|
1247
|
+
# Use deletion plan if available
|
|
1248
|
+
if request.deletion_plan and wallet_name in request.deletion_plan:
|
|
1249
|
+
plan = request.deletion_plan[wallet_name]
|
|
1250
|
+
if plan["delete_hotkeys"]:
|
|
1251
|
+
result = wallet_manager.delete_coldkey_and_hotkeys(wallet_name)
|
|
1252
|
+
hotkeys_count = len(plan["associated_hotkeys"])
|
|
1253
|
+
associated_hotkeys_deleted += hotkeys_count
|
|
1254
|
+
# Collect hotkey details
|
|
1255
|
+
for hotkey_info in plan["associated_hotkeys"]:
|
|
1256
|
+
associated_hotkey_details.append(
|
|
1257
|
+
{"name": hotkey_info["name"], "owner": wallet_name}
|
|
1258
|
+
)
|
|
1259
|
+
else:
|
|
1260
|
+
# Delete only coldkey, leave hotkeys orphaned
|
|
1261
|
+
result = wallet_manager.delete_wallet(
|
|
1262
|
+
wallet_name, is_hotkey=False
|
|
1263
|
+
)
|
|
1264
|
+
coldkeys_deleted += 1
|
|
1265
|
+
else:
|
|
1266
|
+
# Default: delete coldkey and all associated hotkeys
|
|
1267
|
+
result = wallet_manager.delete_coldkey_and_hotkeys(wallet_name)
|
|
1268
|
+
coldkeys_deleted += 1
|
|
1269
|
+
total_hotkeys = result["data"].get("total_hotkeys_deleted", 0)
|
|
1270
|
+
associated_hotkeys_deleted += total_hotkeys
|
|
1271
|
+
# Try to get hotkey details from result if available
|
|
1272
|
+
if "hotkeys_deleted" in result["data"]:
|
|
1273
|
+
for hotkey_name in result["data"]["hotkeys_deleted"]:
|
|
1274
|
+
associated_hotkey_details.append(
|
|
1275
|
+
{"name": hotkey_name, "owner": wallet_name}
|
|
1276
|
+
)
|
|
1277
|
+
total_deleted += 1
|
|
1278
|
+
successfully_deleted.append(wallet_name)
|
|
1279
|
+
|
|
1280
|
+
except FileNotFoundError as e:
|
|
1281
|
+
# Wallet doesn't exist
|
|
1282
|
+
console.print(warning(f"❌ Wallet does not exist: {str(e)}"))
|
|
1283
|
+
except Exception as e:
|
|
1284
|
+
# If deletion fails
|
|
1285
|
+
error_msg = str(e) if str(e) else repr(e)
|
|
1286
|
+
tb_str = traceback.format_exc()
|
|
1287
|
+
console.print(warning(f"❌ Failed to delete wallet: {error_msg}"))
|
|
1288
|
+
console.print(f"[dim]Traceback:[/dim]\n{tb_str}")
|
|
1289
|
+
logger.error(f"Delete error: {tb_str}")
|
|
1290
|
+
|
|
1291
|
+
# Create response model
|
|
1292
|
+
response = WalletDeleteResponse(
|
|
1293
|
+
deleted_wallets=successfully_deleted,
|
|
1294
|
+
total_deleted=total_deleted,
|
|
1295
|
+
coldkeys_deleted=coldkeys_deleted,
|
|
1296
|
+
hotkeys_deleted=hotkeys_deleted,
|
|
1297
|
+
associated_hotkeys_deleted=associated_hotkeys_deleted,
|
|
1298
|
+
associated_hotkey_details=associated_hotkey_details,
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
# STEP 3: Display results
|
|
1302
|
+
if total_deleted > 0:
|
|
1303
|
+
display_wallet_deletion_result(response)
|
|
1304
|
+
else:
|
|
1305
|
+
console.print(warning("❌ No wallets were deleted."))
|
|
1306
|
+
|
|
1307
|
+
except Exception as e:
|
|
1308
|
+
from ...errors.handlers import handle_wallet_error
|
|
1309
|
+
from ...ui.display import print_error
|
|
1310
|
+
|
|
1311
|
+
# Show full traceback for debugging
|
|
1312
|
+
tb_str = traceback.format_exc()
|
|
1313
|
+
logger.error(f"Delete handler error: {tb_str}")
|
|
1314
|
+
console.print(f"[red]Full traceback:[/red]\n{tb_str}")
|
|
1315
|
+
error = handle_wallet_error(e, "delete")
|
|
1316
|
+
print_error(error.message, suggestions=error.suggestions)
|
|
1317
|
+
|
|
1318
|
+
|
|
1319
|
+
def _update_spinner_messages(spinner: HTCLISpinner, stop_event: threading.Event):
|
|
1320
|
+
"""Update spinner messages every 2 seconds."""
|
|
1321
|
+
messages = [
|
|
1322
|
+
"Checking wallet balance...",
|
|
1323
|
+
"Connecting to blockchain...",
|
|
1324
|
+
"Fetching balance data...",
|
|
1325
|
+
"Processing wallet information...",
|
|
1326
|
+
"Retrieving account details...",
|
|
1327
|
+
"Verifying wallet status...",
|
|
1328
|
+
]
|
|
1329
|
+
|
|
1330
|
+
message_index = 0
|
|
1331
|
+
while not stop_event.is_set():
|
|
1332
|
+
try:
|
|
1333
|
+
spinner.update(messages[message_index])
|
|
1334
|
+
message_index = (message_index + 1) % len(messages)
|
|
1335
|
+
time.sleep(2)
|
|
1336
|
+
except Exception:
|
|
1337
|
+
break
|
|
1338
|
+
|
|
1339
|
+
|
|
1340
|
+
def balance_handler(
|
|
1341
|
+
wallet_name: Optional[str] = None,
|
|
1342
|
+
address: Optional[str] = None,
|
|
1343
|
+
show_all: bool = False,
|
|
1344
|
+
format_type: str = "table",
|
|
1345
|
+
show_guidance: Optional[bool] = None,
|
|
1346
|
+
):
|
|
1347
|
+
"""Handle wallet balance command."""
|
|
1348
|
+
try:
|
|
1349
|
+
from ...dependencies import get_client
|
|
1350
|
+
from ...ui.display import print_error
|
|
1351
|
+
from ...utils.wallet.auth import get_wallet_with_retry
|
|
1352
|
+
|
|
1353
|
+
# STEP 1: Collect input parameters
|
|
1354
|
+
wallet_name, address, show_all, format_type, show_guidance = (
|
|
1355
|
+
prompt_wallet_balance(
|
|
1356
|
+
wallet_name, address, show_all, format_type, show_guidance
|
|
1357
|
+
)
|
|
1358
|
+
)
|
|
1359
|
+
|
|
1360
|
+
# STEP 2: Get client and ensure connection
|
|
1361
|
+
client = get_client()
|
|
1362
|
+
|
|
1363
|
+
# Ensure connection is established
|
|
1364
|
+
if not client.substrate:
|
|
1365
|
+
if not client.connect():
|
|
1366
|
+
print_error("Failed to connect to blockchain. Cannot fetch balances.")
|
|
1367
|
+
return
|
|
1368
|
+
|
|
1369
|
+
# Verify RPC layer is initialized
|
|
1370
|
+
if not client.rpc or not client.rpc.wallet:
|
|
1371
|
+
print_error("RPC client not initialized. Cannot fetch balances.")
|
|
1372
|
+
return
|
|
1373
|
+
|
|
1374
|
+
# STEP 3: Execute
|
|
1375
|
+
network_name = _get_network_name(client)
|
|
1376
|
+
if show_all:
|
|
1377
|
+
wallets = [w for w in list_keys() if not w.get("is_hotkey")]
|
|
1378
|
+
if not wallets:
|
|
1379
|
+
console.print(warning("No coldkeys found."))
|
|
1380
|
+
return
|
|
1381
|
+
|
|
1382
|
+
# Iterate through all wallets and get balances with spinner
|
|
1383
|
+
wallet_balances = []
|
|
1384
|
+
total_free_balance = 0
|
|
1385
|
+
total_direct_stake = 0
|
|
1386
|
+
total_delegate_stake = 0
|
|
1387
|
+
total_node_delegate_stake = 0
|
|
1388
|
+
total_overwatch_stake = 0
|
|
1389
|
+
total_unbonding = 0
|
|
1390
|
+
total_staked_balance = 0
|
|
1391
|
+
|
|
1392
|
+
from ...ui.components import HTCLILoadingContext
|
|
1393
|
+
|
|
1394
|
+
# Use a single spinner that updates in place
|
|
1395
|
+
loading = HTCLILoadingContext("Checking wallet balances...", transient=True)
|
|
1396
|
+
loading.__enter__()
|
|
1397
|
+
|
|
1398
|
+
try:
|
|
1399
|
+
for idx, wallet in enumerate(wallets):
|
|
1400
|
+
wallet_name = wallet["name"]
|
|
1401
|
+
wallet_info = get_wallet_info_by_name(wallet_name)
|
|
1402
|
+
|
|
1403
|
+
# Update spinner message to show current wallet
|
|
1404
|
+
loading.update_message(
|
|
1405
|
+
f"Checking wallet '{wallet_name}' ({idx + 1}/{len(wallets)})..."
|
|
1406
|
+
)
|
|
1407
|
+
|
|
1408
|
+
# Check if wallet is encrypted
|
|
1409
|
+
is_encrypted = wallet_info.get(
|
|
1410
|
+
"is_encrypted", False
|
|
1411
|
+
) or wallet_info.get("encrypted", False)
|
|
1412
|
+
|
|
1413
|
+
display_address = wallet_info.get(
|
|
1414
|
+
"ss58_address"
|
|
1415
|
+
) or wallet_info.get("address")
|
|
1416
|
+
query_address = wallet_info.get("address") or display_address
|
|
1417
|
+
|
|
1418
|
+
# Debug print to show what address is being queried
|
|
1419
|
+
from ...utils.logging import get_logger
|
|
1420
|
+
|
|
1421
|
+
logger = get_logger(__name__)
|
|
1422
|
+
logger.debug(
|
|
1423
|
+
f"[show_all] Wallet '{wallet_name}' - query_address: {query_address}, display_address: {display_address}"
|
|
1424
|
+
)
|
|
1425
|
+
|
|
1426
|
+
# Get address - for encrypted wallets, unlock them first
|
|
1427
|
+
try:
|
|
1428
|
+
if is_encrypted:
|
|
1429
|
+
# Stop spinner before password prompt to avoid mixing output
|
|
1430
|
+
loading.__exit__(None, None, None)
|
|
1431
|
+
# Prompt for password (this will print normally)
|
|
1432
|
+
# Use silent=True to suppress intermediate error messages
|
|
1433
|
+
_keypair, _ = get_wallet_with_retry(
|
|
1434
|
+
wallet_name, silent=True
|
|
1435
|
+
)
|
|
1436
|
+
# Restart spinner after password entry
|
|
1437
|
+
loading.__enter__()
|
|
1438
|
+
loading.update_message(
|
|
1439
|
+
f"Fetching balance for '{wallet_name}'..."
|
|
1440
|
+
)
|
|
1441
|
+
else:
|
|
1442
|
+
loading.update_message(
|
|
1443
|
+
f"Fetching balance for '{wallet_name}'..."
|
|
1444
|
+
)
|
|
1445
|
+
|
|
1446
|
+
# Get balance from blockchain
|
|
1447
|
+
response = client.rpc.wallet.get_balance(query_address)
|
|
1448
|
+
|
|
1449
|
+
# Ensure displayed address matches wallet list output
|
|
1450
|
+
if response and display_address:
|
|
1451
|
+
try:
|
|
1452
|
+
response.address = display_address
|
|
1453
|
+
except Exception:
|
|
1454
|
+
pass
|
|
1455
|
+
|
|
1456
|
+
if response and response.success:
|
|
1457
|
+
free_balance = response.balance or 0
|
|
1458
|
+
staking_breakdown = _get_staking_breakdown(
|
|
1459
|
+
client, query_address
|
|
1460
|
+
)
|
|
1461
|
+
staked_balance = staking_breakdown["total"]
|
|
1462
|
+
wallet_balances.append(
|
|
1463
|
+
{
|
|
1464
|
+
"name": wallet_name,
|
|
1465
|
+
"address": display_address or query_address,
|
|
1466
|
+
"free_balance": free_balance,
|
|
1467
|
+
"direct_stake": staking_breakdown["direct_stake"],
|
|
1468
|
+
"delegate_stake": staking_breakdown[
|
|
1469
|
+
"delegate_stake"
|
|
1470
|
+
],
|
|
1471
|
+
"node_delegate_stake": staking_breakdown[
|
|
1472
|
+
"node_delegate_stake"
|
|
1473
|
+
],
|
|
1474
|
+
"overwatch_stake": staking_breakdown[
|
|
1475
|
+
"overwatch_stake"
|
|
1476
|
+
],
|
|
1477
|
+
"unbonding": staking_breakdown["unbonding"],
|
|
1478
|
+
"staked_balance": staked_balance,
|
|
1479
|
+
"total_balance": free_balance + staked_balance,
|
|
1480
|
+
}
|
|
1481
|
+
)
|
|
1482
|
+
total_free_balance += free_balance
|
|
1483
|
+
total_direct_stake += staking_breakdown["direct_stake"]
|
|
1484
|
+
total_delegate_stake += staking_breakdown["delegate_stake"]
|
|
1485
|
+
total_node_delegate_stake += staking_breakdown[
|
|
1486
|
+
"node_delegate_stake"
|
|
1487
|
+
]
|
|
1488
|
+
total_overwatch_stake += staking_breakdown[
|
|
1489
|
+
"overwatch_stake"
|
|
1490
|
+
]
|
|
1491
|
+
total_unbonding += staking_breakdown["unbonding"]
|
|
1492
|
+
total_staked_balance += staked_balance
|
|
1493
|
+
else:
|
|
1494
|
+
error_msg = response.error if response else "Unknown Error"
|
|
1495
|
+
wallet_balances.append(
|
|
1496
|
+
{
|
|
1497
|
+
"name": wallet_name,
|
|
1498
|
+
"address": display_address or query_address,
|
|
1499
|
+
"free_balance": 0,
|
|
1500
|
+
"staked_balance": 0,
|
|
1501
|
+
"total_balance": 0,
|
|
1502
|
+
"display_free": f"[red]{error_msg}[/red]",
|
|
1503
|
+
"display_staked": "[red]--[/red]",
|
|
1504
|
+
"display_total": "[red]--[/red]",
|
|
1505
|
+
"error": error_msg,
|
|
1506
|
+
}
|
|
1507
|
+
)
|
|
1508
|
+
except Exception as e:
|
|
1509
|
+
# Stop spinner before printing error
|
|
1510
|
+
if (
|
|
1511
|
+
loading.spinner
|
|
1512
|
+
and hasattr(loading.spinner, "_running")
|
|
1513
|
+
and loading.spinner._running
|
|
1514
|
+
):
|
|
1515
|
+
loading.__exit__(None, None, None)
|
|
1516
|
+
# Skip wallet if it can't be unlocked or balance can't be fetched
|
|
1517
|
+
console.print(warning(f"Skipping '{wallet_name}': {str(e)}"))
|
|
1518
|
+
# Restart spinner for next wallet
|
|
1519
|
+
if idx < len(wallets) - 1:
|
|
1520
|
+
loading.__enter__()
|
|
1521
|
+
continue
|
|
1522
|
+
finally:
|
|
1523
|
+
# Ensure spinner is stopped
|
|
1524
|
+
if (
|
|
1525
|
+
loading.spinner
|
|
1526
|
+
and hasattr(loading.spinner, "_running")
|
|
1527
|
+
and loading.spinner._running
|
|
1528
|
+
):
|
|
1529
|
+
loading.__exit__(None, None, None)
|
|
1530
|
+
|
|
1531
|
+
# Display results
|
|
1532
|
+
display_all_wallet_balances(
|
|
1533
|
+
wallet_balances,
|
|
1534
|
+
{
|
|
1535
|
+
"free": total_free_balance,
|
|
1536
|
+
"direct_stake": total_direct_stake,
|
|
1537
|
+
"delegate_stake": total_delegate_stake,
|
|
1538
|
+
"node_delegate_stake": total_node_delegate_stake,
|
|
1539
|
+
"overwatch_stake": total_overwatch_stake,
|
|
1540
|
+
"unbonding": total_unbonding,
|
|
1541
|
+
"staked": total_staked_balance,
|
|
1542
|
+
"total": total_free_balance + total_staked_balance,
|
|
1543
|
+
},
|
|
1544
|
+
format_type,
|
|
1545
|
+
network_name=network_name,
|
|
1546
|
+
)
|
|
1547
|
+
else:
|
|
1548
|
+
# Single wallet balance check
|
|
1549
|
+
from ...ui.components import HTCLILoadingContext
|
|
1550
|
+
from ...ui.display import print_error
|
|
1551
|
+
|
|
1552
|
+
wallet_type = "External Address"
|
|
1553
|
+
if wallet_name:
|
|
1554
|
+
wallet_info = get_wallet_info_by_name(wallet_name)
|
|
1555
|
+
|
|
1556
|
+
# Check encryption status explicitly
|
|
1557
|
+
is_encrypted = wallet_info.get(
|
|
1558
|
+
"is_encrypted", False
|
|
1559
|
+
) or wallet_info.get("encrypted", False)
|
|
1560
|
+
|
|
1561
|
+
# Use spinner for single wallet check too
|
|
1562
|
+
loading = HTCLILoadingContext(
|
|
1563
|
+
f"Checking wallet '{wallet_name}'...", transient=True
|
|
1564
|
+
)
|
|
1565
|
+
loading.__enter__()
|
|
1566
|
+
|
|
1567
|
+
try:
|
|
1568
|
+
display_address = wallet_info.get(
|
|
1569
|
+
"ss58_address"
|
|
1570
|
+
) or wallet_info.get("address")
|
|
1571
|
+
query_address = wallet_info.get("address") or display_address
|
|
1572
|
+
|
|
1573
|
+
# Debug print to show what address is being queried
|
|
1574
|
+
from ...utils.logging import get_logger
|
|
1575
|
+
|
|
1576
|
+
logger = get_logger(__name__)
|
|
1577
|
+
logger.debug(
|
|
1578
|
+
f"[single wallet] Wallet '{wallet_name}' - query_address: {query_address}, display_address: {display_address}"
|
|
1579
|
+
)
|
|
1580
|
+
logger.debug(f"[single wallet] Wallet info: {wallet_info}")
|
|
1581
|
+
|
|
1582
|
+
if is_encrypted:
|
|
1583
|
+
try:
|
|
1584
|
+
# Stop spinner before password prompt
|
|
1585
|
+
loading.__exit__(None, None, None)
|
|
1586
|
+
# Use silent=True to suppress intermediate error messages
|
|
1587
|
+
_keypair, _ = get_wallet_with_retry(
|
|
1588
|
+
wallet_name, silent=True
|
|
1589
|
+
)
|
|
1590
|
+
# Restart spinner after password entry
|
|
1591
|
+
loading.__enter__()
|
|
1592
|
+
loading.update_message(
|
|
1593
|
+
f"Fetching balance for '{wallet_name}'..."
|
|
1594
|
+
)
|
|
1595
|
+
except Exception as e:
|
|
1596
|
+
print_error(
|
|
1597
|
+
f"Failed to unlock wallet '{wallet_name}': {str(e)}"
|
|
1598
|
+
)
|
|
1599
|
+
return
|
|
1600
|
+
else:
|
|
1601
|
+
loading.update_message(
|
|
1602
|
+
f"Fetching balance for '{wallet_name}'..."
|
|
1603
|
+
)
|
|
1604
|
+
|
|
1605
|
+
wallet_type = (
|
|
1606
|
+
"Hotkey" if wallet_info.get("is_hotkey", False) else "Coldkey"
|
|
1607
|
+
)
|
|
1608
|
+
|
|
1609
|
+
# Get balance for single wallet
|
|
1610
|
+
response = None
|
|
1611
|
+
try:
|
|
1612
|
+
# Ensure client is connected
|
|
1613
|
+
if not client.substrate:
|
|
1614
|
+
if not client.connect():
|
|
1615
|
+
loading.__exit__(None, None, None)
|
|
1616
|
+
print_error("Failed to connect to blockchain")
|
|
1617
|
+
return
|
|
1618
|
+
|
|
1619
|
+
# Get balance from blockchain
|
|
1620
|
+
response = client.rpc.wallet.get_balance(query_address)
|
|
1621
|
+
except TimeoutError as e:
|
|
1622
|
+
loading.__exit__(None, None, None)
|
|
1623
|
+
print_error(f"Balance query timed out: {str(e)}")
|
|
1624
|
+
print_error(
|
|
1625
|
+
"The blockchain RPC endpoint may be slow or unavailable"
|
|
1626
|
+
)
|
|
1627
|
+
return
|
|
1628
|
+
except Exception as e:
|
|
1629
|
+
loading.__exit__(None, None, None)
|
|
1630
|
+
print_error(f"Balance query failed: {str(e)}")
|
|
1631
|
+
raise
|
|
1632
|
+
|
|
1633
|
+
if response is None:
|
|
1634
|
+
loading.__exit__(None, None, None)
|
|
1635
|
+
print_error("Balance query returned no response")
|
|
1636
|
+
return
|
|
1637
|
+
|
|
1638
|
+
if response and display_address:
|
|
1639
|
+
try:
|
|
1640
|
+
response.address = display_address
|
|
1641
|
+
except Exception:
|
|
1642
|
+
pass
|
|
1643
|
+
|
|
1644
|
+
loading.__exit__(None, None, None)
|
|
1645
|
+
if response.success:
|
|
1646
|
+
staking_breakdown = _get_staking_breakdown(
|
|
1647
|
+
client, query_address
|
|
1648
|
+
)
|
|
1649
|
+
staking_balance = staking_breakdown["total"]
|
|
1650
|
+
display_single_wallet_balance(
|
|
1651
|
+
response,
|
|
1652
|
+
wallet_name,
|
|
1653
|
+
wallet_type,
|
|
1654
|
+
format_type,
|
|
1655
|
+
show_guidance,
|
|
1656
|
+
staking_balance=staking_balance,
|
|
1657
|
+
staking_breakdown=staking_breakdown,
|
|
1658
|
+
network_name=network_name,
|
|
1659
|
+
)
|
|
1660
|
+
else:
|
|
1661
|
+
error_msg = response.error or "Unknown Error"
|
|
1662
|
+
print_error(f"Balance query failed: {error_msg}")
|
|
1663
|
+
raise Exception(error_msg)
|
|
1664
|
+
finally:
|
|
1665
|
+
# Ensure spinner is stopped
|
|
1666
|
+
if (
|
|
1667
|
+
loading.spinner
|
|
1668
|
+
and hasattr(loading.spinner, "_running")
|
|
1669
|
+
and loading.spinner._running
|
|
1670
|
+
):
|
|
1671
|
+
loading.__exit__(None, None, None)
|
|
1672
|
+
else:
|
|
1673
|
+
# External address check (no wallet name)
|
|
1674
|
+
response = None
|
|
1675
|
+
with HTCLILoadingContext(
|
|
1676
|
+
"Fetching balance for address...", transient=True
|
|
1677
|
+
):
|
|
1678
|
+
try:
|
|
1679
|
+
if not client.substrate:
|
|
1680
|
+
if not client.connect():
|
|
1681
|
+
print_error("Failed to connect to blockchain")
|
|
1682
|
+
return
|
|
1683
|
+
|
|
1684
|
+
response = client.rpc.wallet.get_balance(address)
|
|
1685
|
+
except TimeoutError as e:
|
|
1686
|
+
print_error(f"Balance query timed out: {str(e)}")
|
|
1687
|
+
print_error(
|
|
1688
|
+
"The blockchain RPC endpoint may be slow or unavailable"
|
|
1689
|
+
)
|
|
1690
|
+
return
|
|
1691
|
+
except Exception as e:
|
|
1692
|
+
print_error(f"Balance query failed: {str(e)}")
|
|
1693
|
+
raise
|
|
1694
|
+
|
|
1695
|
+
if response is None:
|
|
1696
|
+
print_error("Balance query returned no response")
|
|
1697
|
+
return
|
|
1698
|
+
|
|
1699
|
+
# Display results after spinner has stopped
|
|
1700
|
+
if response.success:
|
|
1701
|
+
staking_breakdown = _get_staking_breakdown(client, address)
|
|
1702
|
+
staking_balance = staking_breakdown["total"]
|
|
1703
|
+
display_single_wallet_balance(
|
|
1704
|
+
response,
|
|
1705
|
+
None, # No wallet name for external address
|
|
1706
|
+
wallet_type, # "External Address"
|
|
1707
|
+
format_type,
|
|
1708
|
+
show_guidance,
|
|
1709
|
+
staking_balance=staking_balance,
|
|
1710
|
+
staking_breakdown=staking_breakdown,
|
|
1711
|
+
network_name=network_name,
|
|
1712
|
+
)
|
|
1713
|
+
else:
|
|
1714
|
+
error_msg = response.error or "Unknown Error"
|
|
1715
|
+
print_error(f"Balance query failed: {error_msg}")
|
|
1716
|
+
raise Exception(error_msg)
|
|
1717
|
+
|
|
1718
|
+
except RuntimeError as e:
|
|
1719
|
+
# Handle connection errors specifically
|
|
1720
|
+
from ...ui.display import print_error
|
|
1721
|
+
|
|
1722
|
+
error_str = str(e).lower()
|
|
1723
|
+
if "connect" in error_str or "blockchain" in error_str:
|
|
1724
|
+
print_error(f"Blockchain Connection Error: {str(e)}")
|
|
1725
|
+
else:
|
|
1726
|
+
print_error(f"Error: {str(e)}")
|
|
1727
|
+
except Exception as e:
|
|
1728
|
+
from ...errors.handlers import handle_wallet_error
|
|
1729
|
+
from ...ui.display import print_error
|
|
1730
|
+
|
|
1731
|
+
error = handle_wallet_error(e, "update")
|
|
1732
|
+
print_error(error.message, suggestions=error.suggestions)
|
|
1733
|
+
|
|
1734
|
+
|
|
1735
|
+
def transfer_handler(
|
|
1736
|
+
from_wallet: Optional[str] = None,
|
|
1737
|
+
to_address: Optional[str] = None,
|
|
1738
|
+
amount: Optional[float] = None,
|
|
1739
|
+
password: Optional[str] = None,
|
|
1740
|
+
dry_run: bool = False,
|
|
1741
|
+
):
|
|
1742
|
+
"""Handle wallet transfer command."""
|
|
1743
|
+
try:
|
|
1744
|
+
# Convert amount to string if provided
|
|
1745
|
+
amount_str = str(amount) if amount is not None else None
|
|
1746
|
+
|
|
1747
|
+
# STEP 1: Collect input parameters
|
|
1748
|
+
# Create request directly to avoid hanging in prompts
|
|
1749
|
+
from ...models.requests import WalletTransferRequest
|
|
1750
|
+
from ...ui.display import print_error
|
|
1751
|
+
|
|
1752
|
+
# Handle None values - use prompts if any required field is missing
|
|
1753
|
+
if not from_wallet or not to_address or not amount_str:
|
|
1754
|
+
request = prompt_wallet_transfer(
|
|
1755
|
+
from_wallet=from_wallet,
|
|
1756
|
+
to_address=to_address,
|
|
1757
|
+
amount=amount_str,
|
|
1758
|
+
password=password,
|
|
1759
|
+
)
|
|
1760
|
+
else:
|
|
1761
|
+
# Validate CLI-provided amount before creating request
|
|
1762
|
+
try:
|
|
1763
|
+
amount_float = float(amount_str)
|
|
1764
|
+
if amount_float <= 0:
|
|
1765
|
+
print_error("Amount must be positive")
|
|
1766
|
+
return
|
|
1767
|
+
if amount_float > 1_000_000_000: # 1 billion TENSOR
|
|
1768
|
+
print_error(f"Amount too large: {amount_float:,.2f} TENSOR")
|
|
1769
|
+
print_error("Maximum allowed: 1,000,000,000 TENSOR")
|
|
1770
|
+
print_error("This prevents blockchain runtime errors")
|
|
1771
|
+
return
|
|
1772
|
+
if amount_float < 0.000001:
|
|
1773
|
+
print_error("Amount too small. Minimum: 0.000001 TENSOR")
|
|
1774
|
+
return
|
|
1775
|
+
except ValueError:
|
|
1776
|
+
print_error(f"Invalid amount format: {amount_str}")
|
|
1777
|
+
return
|
|
1778
|
+
|
|
1779
|
+
request = WalletTransferRequest(
|
|
1780
|
+
from_wallet=from_wallet,
|
|
1781
|
+
to_address=to_address,
|
|
1782
|
+
amount=amount_str,
|
|
1783
|
+
password=password,
|
|
1784
|
+
)
|
|
1785
|
+
|
|
1786
|
+
# STEP 2: Execute
|
|
1787
|
+
wallet_info = get_wallet_info_by_name(request.from_wallet, is_hotkey=False)
|
|
1788
|
+
|
|
1789
|
+
# Resolve destination address if it's a wallet name
|
|
1790
|
+
to_address = request.to_address
|
|
1791
|
+
if request.from_wallet == to_address:
|
|
1792
|
+
print_error("Source and destination cannot be the same wallet")
|
|
1793
|
+
return
|
|
1794
|
+
|
|
1795
|
+
# Try to resolve as wallet name first (simpler approach)
|
|
1796
|
+
if not to_address.startswith("0x") and len(to_address) < 50:
|
|
1797
|
+
# Likely a wallet name, try to resolve it
|
|
1798
|
+
try:
|
|
1799
|
+
dest_wallet_info = get_wallet_info_by_name(to_address, is_hotkey=False)
|
|
1800
|
+
to_address = dest_wallet_info["ss58_address"]
|
|
1801
|
+
except FileNotFoundError:
|
|
1802
|
+
pass
|
|
1803
|
+
|
|
1804
|
+
if (
|
|
1805
|
+
wallet_info.get("address") == to_address
|
|
1806
|
+
or wallet_info.get("ss58_address") == to_address
|
|
1807
|
+
):
|
|
1808
|
+
print_error("Source and destination cannot be the same address")
|
|
1809
|
+
return
|
|
1810
|
+
|
|
1811
|
+
# Always load keypair for transfers (needed to sign transaction)
|
|
1812
|
+
# Transfers must use coldkeys (only coldkeys have balances)
|
|
1813
|
+
from ...ui.display import print_error
|
|
1814
|
+
from ...utils.wallet.crypto import load_keypair
|
|
1815
|
+
|
|
1816
|
+
# Get password from request if it exists, otherwise None
|
|
1817
|
+
wallet_password = getattr(request, "password", None)
|
|
1818
|
+
|
|
1819
|
+
try:
|
|
1820
|
+
# Explicitly specify is_hotkey=False to disambiguate when both coldkey and hotkey exist with same name
|
|
1821
|
+
keypair = load_keypair(
|
|
1822
|
+
request.from_wallet, wallet_password, is_hotkey=False
|
|
1823
|
+
)
|
|
1824
|
+
except Exception as e:
|
|
1825
|
+
print_error(f"Failed to load keypair: {str(e)}")
|
|
1826
|
+
raise
|
|
1827
|
+
|
|
1828
|
+
# Call transfer client with loading context
|
|
1829
|
+
from ...dependencies import get_client
|
|
1830
|
+
from ...ui.components import HTCLILoadingContext
|
|
1831
|
+
|
|
1832
|
+
client = get_client()
|
|
1833
|
+
|
|
1834
|
+
try:
|
|
1835
|
+
with HTCLILoadingContext(f"Transferring {request.amount} TENSOR..."):
|
|
1836
|
+
response = client.extrinsics.wallet.transfer_funds(
|
|
1837
|
+
from_address=wallet_info["address"],
|
|
1838
|
+
to_address=to_address,
|
|
1839
|
+
amount=request.amount,
|
|
1840
|
+
keypair=keypair,
|
|
1841
|
+
)
|
|
1842
|
+
except Exception as e:
|
|
1843
|
+
# Let the outer handler display the error
|
|
1844
|
+
raise
|
|
1845
|
+
|
|
1846
|
+
if not response["success"]:
|
|
1847
|
+
error_msg = response.get("message") or response.get(
|
|
1848
|
+
"error", "Unknown error"
|
|
1849
|
+
)
|
|
1850
|
+
# Let the outer handler display the error
|
|
1851
|
+
raise Exception(error_msg)
|
|
1852
|
+
|
|
1853
|
+
# Create response model from dict with proper field mapping
|
|
1854
|
+
transfer_response = WalletTransferResponse(
|
|
1855
|
+
success=response["success"],
|
|
1856
|
+
from_address=response["data"]["from_address"],
|
|
1857
|
+
to_address=response["data"]["to_address"],
|
|
1858
|
+
amount=str(response["data"]["amount"]), # Convert to string
|
|
1859
|
+
unit="TENSOR", # Add required unit field
|
|
1860
|
+
transaction_hash=response.get("transaction_hash"), # Correct field name
|
|
1861
|
+
block_hash=response.get("block_hash"),
|
|
1862
|
+
)
|
|
1863
|
+
|
|
1864
|
+
# STEP 3: Display results
|
|
1865
|
+
console.print() # Add blank line after spinner
|
|
1866
|
+
display_wallet_transfer_result(transfer_response, dry_run=dry_run)
|
|
1867
|
+
|
|
1868
|
+
except KeyboardInterrupt:
|
|
1869
|
+
from ...ui.display import print_warning
|
|
1870
|
+
|
|
1871
|
+
console.print()
|
|
1872
|
+
print_warning("Operation cancelled")
|
|
1873
|
+
raise
|
|
1874
|
+
except RuntimeError as e:
|
|
1875
|
+
# Handle connection errors specifically
|
|
1876
|
+
from ...ui.display import print_error
|
|
1877
|
+
|
|
1878
|
+
console.print()
|
|
1879
|
+
error_str = str(e).lower()
|
|
1880
|
+
if "connect" in error_str or "blockchain" in error_str:
|
|
1881
|
+
print_error(f"Blockchain Connection Error: {str(e)}")
|
|
1882
|
+
else:
|
|
1883
|
+
print_error(f"Error: {str(e)}")
|
|
1884
|
+
except Exception as e:
|
|
1885
|
+
# Use centralized error handling
|
|
1886
|
+
from ...errors import handle_and_display_error
|
|
1887
|
+
|
|
1888
|
+
console.print()
|
|
1889
|
+
|
|
1890
|
+
# Check specific wallet/transfer error handlers first
|
|
1891
|
+
error_msg = str(e)
|
|
1892
|
+
if not handle_wallet_extrinsic_error(
|
|
1893
|
+
error_msg, "transfer", client if "client" in locals() else None
|
|
1894
|
+
):
|
|
1895
|
+
handle_and_display_error(e, "transfer")
|
|
1896
|
+
|
|
1897
|
+
|
|
1898
|
+
def rotate_coldkey_handler(name: str, password: Optional[str] = None):
|
|
1899
|
+
"""Handle coldkey rotation command."""
|
|
1900
|
+
try:
|
|
1901
|
+
from substrateinterface import Keypair
|
|
1902
|
+
|
|
1903
|
+
from ...dependencies import get_client
|
|
1904
|
+
from ...models.requests.identity import IdentityUpdateColdkeyRequest
|
|
1905
|
+
from ...ui.display import print_error, print_success, print_warning
|
|
1906
|
+
from ...ui.prompts import confirm_prompt
|
|
1907
|
+
from ...utils.wallet.core import _get_keypair_with_password_retry
|
|
1908
|
+
|
|
1909
|
+
# STEP 1: Connect to blockchain
|
|
1910
|
+
client = get_client()
|
|
1911
|
+
if not client.substrate:
|
|
1912
|
+
if not client.connect():
|
|
1913
|
+
print_error("Failed to connect to blockchain. Cannot rotate key.")
|
|
1914
|
+
return
|
|
1915
|
+
|
|
1916
|
+
wallet_manager = WalletManager()
|
|
1917
|
+
|
|
1918
|
+
# STEP 2: Load current wallet (verify password)
|
|
1919
|
+
console.print(info(f"🔐 Unlocking coldkey '{name}'..."))
|
|
1920
|
+
wallet_info = wallet_manager.get_wallet_info(name, is_hotkey=False)
|
|
1921
|
+
_, current_keypair = _get_keypair_with_password_retry(
|
|
1922
|
+
client, name, wallet_info["data"], 3, password
|
|
1923
|
+
)
|
|
1924
|
+
|
|
1925
|
+
# STEP 3: Generate new keypair
|
|
1926
|
+
console.print(info("🎲 Generating new keypair..."))
|
|
1927
|
+
mnemonic = Keypair.generate_mnemonic()
|
|
1928
|
+
new_keypair = Keypair.create_from_mnemonic(mnemonic, crypto_type=2) # ECDSA
|
|
1929
|
+
new_address = new_keypair.ss58_address # Same as EVM address for ECDSA
|
|
1930
|
+
|
|
1931
|
+
console.print()
|
|
1932
|
+
print_warning("⚠️ CRITICAL: BACKUP YOUR NEW MNEMONIC")
|
|
1933
|
+
console.print(
|
|
1934
|
+
"If the rotation succeeds but the file save fails, this is your ONLY way to access the wallet."
|
|
1935
|
+
)
|
|
1936
|
+
console.print(f"[bold green]{mnemonic}[/bold green]")
|
|
1937
|
+
console.print()
|
|
1938
|
+
|
|
1939
|
+
if not confirm_prompt("Have you backed up this mnemonic?", default=False):
|
|
1940
|
+
print_error("Rotation cancelled. Please backup the mnemonic first.")
|
|
1941
|
+
return
|
|
1942
|
+
|
|
1943
|
+
# STEP 4: Submit rotation extrinsic
|
|
1944
|
+
console.print()
|
|
1945
|
+
with HTCLISpinner(f"Rotating coldkey to {new_address}..."):
|
|
1946
|
+
request = IdentityUpdateColdkeyRequest(new_coldkey=new_address)
|
|
1947
|
+
response = client.extrinsics.identity.update_coldkey(
|
|
1948
|
+
request, current_keypair
|
|
1949
|
+
)
|
|
1950
|
+
|
|
1951
|
+
if not response["success"]:
|
|
1952
|
+
print_error(f"Rotation failed: {response.get('error')}")
|
|
1953
|
+
return
|
|
1954
|
+
|
|
1955
|
+
# STEP 5: Update local wallet file
|
|
1956
|
+
console.print(print_success("✅ On-chain rotation successful!"))
|
|
1957
|
+
console.print(info("💾 Updating local wallet file..."))
|
|
1958
|
+
|
|
1959
|
+
# Backup old wallet first
|
|
1960
|
+
try:
|
|
1961
|
+
backup_path = (
|
|
1962
|
+
wallet_manager.wallet_dir / "backups" / f"{name}.coldkey.bak.json"
|
|
1963
|
+
)
|
|
1964
|
+
wallet_manager.backup_wallet(name, str(backup_path), password or "")
|
|
1965
|
+
console.print(f" • Old wallet backed up to: {backup_path}")
|
|
1966
|
+
except Exception as e:
|
|
1967
|
+
print_warning(f" • Backup failed: {e}")
|
|
1968
|
+
|
|
1969
|
+
# Save new wallet (using same password)
|
|
1970
|
+
# We need to temporarily delete the old file to allow saving the new one with the same name
|
|
1971
|
+
wallet_manager.delete_wallet(name, is_hotkey=False)
|
|
1972
|
+
wallet_manager.save_coldkey(name, new_keypair, password)
|
|
1973
|
+
|
|
1974
|
+
console.print(print_success(f"✅ Local wallet '{name}' updated successfully!"))
|
|
1975
|
+
console.print(f" • New Address: {new_address}")
|
|
1976
|
+
console.print(f" • Old Address: {current_keypair.ss58_address}")
|
|
1977
|
+
|
|
1978
|
+
except Exception as e:
|
|
1979
|
+
from ...errors.handlers import handle_wallet_error
|
|
1980
|
+
from ...ui.display import print_error
|
|
1981
|
+
|
|
1982
|
+
error = handle_wallet_error(e, "rotate")
|
|
1983
|
+
print_error(error.message, suggestions=error.suggestions)
|
|
1984
|
+
|
|
1985
|
+
|
|
1986
|
+
def rotate_hotkey_handler(
|
|
1987
|
+
name: str,
|
|
1988
|
+
owner_name: Optional[str] = None,
|
|
1989
|
+
password: Optional[str] = None,
|
|
1990
|
+
):
|
|
1991
|
+
"""Handle hotkey rotation command."""
|
|
1992
|
+
try:
|
|
1993
|
+
from substrateinterface import Keypair
|
|
1994
|
+
|
|
1995
|
+
from ...dependencies import get_client
|
|
1996
|
+
from ...models.requests.identity import IdentityUpdateHotkeyRequest
|
|
1997
|
+
from ...ui.display import print_error, print_success, print_warning
|
|
1998
|
+
from ...ui.prompts import confirm_prompt
|
|
1999
|
+
from ...utils.wallet.core import _get_keypair_with_password_retry
|
|
2000
|
+
|
|
2001
|
+
# STEP 1: Connect to blockchain
|
|
2002
|
+
client = get_client()
|
|
2003
|
+
if not client.substrate:
|
|
2004
|
+
if not client.connect():
|
|
2005
|
+
print_error("Failed to connect to blockchain. Cannot rotate key.")
|
|
2006
|
+
return
|
|
2007
|
+
|
|
2008
|
+
wallet_manager = WalletManager()
|
|
2009
|
+
|
|
2010
|
+
# Resolve owner if not provided
|
|
2011
|
+
if not owner_name:
|
|
2012
|
+
# Try to infer owner from hotkey file
|
|
2013
|
+
hotkey_info = wallet_manager.get_wallet_info(name, is_hotkey=True)
|
|
2014
|
+
owner_name = hotkey_info["data"].get("owner_coldkey_name")
|
|
2015
|
+
if not owner_name:
|
|
2016
|
+
print_error(
|
|
2017
|
+
"Could not determine owner coldkey. Please specify --owner <coldkey_name>"
|
|
2018
|
+
)
|
|
2019
|
+
return
|
|
2020
|
+
|
|
2021
|
+
# STEP 2: Load wallets
|
|
2022
|
+
# For hotkey rotation, we need:
|
|
2023
|
+
# 1. The coldkey (owner) to sign the transaction
|
|
2024
|
+
# 2. The hotkey wallet info to get the file path/name
|
|
2025
|
+
|
|
2026
|
+
console.print(info(f"🔐 Unlocking owner coldkey '{owner_name}'..."))
|
|
2027
|
+
owner_info = wallet_manager.get_wallet_info(owner_name, is_hotkey=False)
|
|
2028
|
+
owner_address = (
|
|
2029
|
+
owner_info["data"].get("evm_address")
|
|
2030
|
+
or owner_info["data"].get("ss58_address")
|
|
2031
|
+
or owner_info["data"].get("address")
|
|
2032
|
+
)
|
|
2033
|
+
|
|
2034
|
+
# Get owner keypair (may prompt for password)
|
|
2035
|
+
_, owner_keypair = _get_keypair_with_password_retry(
|
|
2036
|
+
client, owner_name, owner_info["data"], 3
|
|
2037
|
+
)
|
|
2038
|
+
|
|
2039
|
+
# STEP 3: Generate new keypair
|
|
2040
|
+
console.print(info("🎲 Generating new hotkey pair..."))
|
|
2041
|
+
mnemonic = Keypair.generate_mnemonic()
|
|
2042
|
+
new_keypair = Keypair.create_from_mnemonic(mnemonic, crypto_type=2) # ECDSA
|
|
2043
|
+
new_address = new_keypair.ss58_address
|
|
2044
|
+
|
|
2045
|
+
console.print()
|
|
2046
|
+
print_warning("⚠️ CRITICAL: BACKUP YOUR NEW MNEMONIC")
|
|
2047
|
+
console.print(f"[bold green]{mnemonic}[/bold green]")
|
|
2048
|
+
console.print()
|
|
2049
|
+
|
|
2050
|
+
if not confirm_prompt("Have you backed up this mnemonic?", default=False):
|
|
2051
|
+
print_error("Rotation cancelled. Please backup the mnemonic first.")
|
|
2052
|
+
return
|
|
2053
|
+
|
|
2054
|
+
# STEP 4: Submit rotation extrinsic
|
|
2055
|
+
console.print()
|
|
2056
|
+
with HTCLISpinner(f"Rotating hotkey to {new_address}..."):
|
|
2057
|
+
request = IdentityUpdateHotkeyRequest(new_hotkey=new_address)
|
|
2058
|
+
# Sign with OWNER (coldkey)
|
|
2059
|
+
response = client.extrinsics.identity.update_hotkey(request, owner_keypair)
|
|
2060
|
+
|
|
2061
|
+
if not response["success"]:
|
|
2062
|
+
print_error(f"Rotation failed: {response.get('error')}")
|
|
2063
|
+
return
|
|
2064
|
+
|
|
2065
|
+
# STEP 5: Update local wallet file
|
|
2066
|
+
console.print(print_success("✅ On-chain rotation successful!"))
|
|
2067
|
+
console.print(info("💾 Updating local wallet file..."))
|
|
2068
|
+
|
|
2069
|
+
# Backup old wallet first
|
|
2070
|
+
try:
|
|
2071
|
+
backup_path = (
|
|
2072
|
+
wallet_manager.wallet_dir / "backups" / f"{name}.hotkey.bak.json"
|
|
2073
|
+
)
|
|
2074
|
+
# Hotkeys usually don't have passwords, but pass empty string if needed
|
|
2075
|
+
wallet_manager.backup_wallet(name, str(backup_path), password or "")
|
|
2076
|
+
console.print(f" • Old wallet backed up to: {backup_path}")
|
|
2077
|
+
except Exception as e:
|
|
2078
|
+
# If backup fails (e.g. invalid password for old hotkey), just warn
|
|
2079
|
+
print_warning(f" • Backup failed: {e}")
|
|
2080
|
+
|
|
2081
|
+
# Save new wallet
|
|
2082
|
+
# Delete old file first
|
|
2083
|
+
wallet_manager.delete_wallet(
|
|
2084
|
+
name, is_hotkey=True, owner_coldkey_name=owner_name
|
|
2085
|
+
)
|
|
2086
|
+
wallet_manager.save_hotkey(
|
|
2087
|
+
name, new_keypair, owner_address, password, owner_coldkey_name=owner_name
|
|
2088
|
+
)
|
|
2089
|
+
|
|
2090
|
+
console.print(print_success(f"✅ Local hotkey '{name}' updated successfully!"))
|
|
2091
|
+
console.print(f" • New Address: {new_address}")
|
|
2092
|
+
|
|
2093
|
+
except Exception as e:
|
|
2094
|
+
from ...errors.handlers import handle_wallet_error
|
|
2095
|
+
from ...ui.display import print_error
|
|
2096
|
+
|
|
2097
|
+
error = handle_wallet_error(e, "rotate")
|
|
2098
|
+
print_error(error.message, suggestions=error.suggestions)
|
|
2099
|
+
|
|
2100
|
+
|
|
2101
|
+
def update_coldkey_handler(
|
|
2102
|
+
name: Optional[str] = None,
|
|
2103
|
+
new_name: Optional[str] = None,
|
|
2104
|
+
new_password: Optional[str] = None,
|
|
2105
|
+
current_password: Optional[str] = None,
|
|
2106
|
+
):
|
|
2107
|
+
"""Handle coldkey update command."""
|
|
2108
|
+
try:
|
|
2109
|
+
# STEP 1: Collect input parameters
|
|
2110
|
+
request = prompt_wallet_update(
|
|
2111
|
+
current_name=name,
|
|
2112
|
+
new_name=new_name,
|
|
2113
|
+
new_password=new_password,
|
|
2114
|
+
is_hotkey=False,
|
|
2115
|
+
)
|
|
2116
|
+
|
|
2117
|
+
# STEP 2: Execute via client layer
|
|
2118
|
+
wallet_manager = WalletManager()
|
|
2119
|
+
result = wallet_manager.update_coldkey(
|
|
2120
|
+
name=request.current_name,
|
|
2121
|
+
new_name=request.new_name,
|
|
2122
|
+
new_password=request.new_password,
|
|
2123
|
+
remove_password=request.remove_password,
|
|
2124
|
+
current_password=request.current_password,
|
|
2125
|
+
)
|
|
2126
|
+
update_data = result["data"]
|
|
2127
|
+
|
|
2128
|
+
# Create response model
|
|
2129
|
+
response = WalletUpdateResponse(
|
|
2130
|
+
old_name=update_data["old_name"],
|
|
2131
|
+
new_name=update_data["new_name"],
|
|
2132
|
+
address=update_data["ss58_address"],
|
|
2133
|
+
key_type=update_data["key_type"],
|
|
2134
|
+
name_updated=update_data["name_updated"],
|
|
2135
|
+
password_updated=update_data["password_updated"],
|
|
2136
|
+
)
|
|
2137
|
+
|
|
2138
|
+
# STEP 3: Display results
|
|
2139
|
+
display_wallet_update_result(response)
|
|
2140
|
+
|
|
2141
|
+
except Exception as e:
|
|
2142
|
+
from ...errors.handlers import handle_wallet_error
|
|
2143
|
+
from ...ui.display import print_error
|
|
2144
|
+
|
|
2145
|
+
# Print full traceback for debugging
|
|
2146
|
+
# console.print("\n[yellow]Full Traceback:[/yellow]")
|
|
2147
|
+
# console.console.print_exception(show_locals=True, width=120)
|
|
2148
|
+
# console.print()
|
|
2149
|
+
|
|
2150
|
+
error = handle_wallet_error(e, "update")
|
|
2151
|
+
print_error(error.message, suggestions=error.suggestions)
|
|
2152
|
+
|
|
2153
|
+
|
|
2154
|
+
def update_hotkey_handler(
|
|
2155
|
+
name: Optional[str] = None,
|
|
2156
|
+
new_password: Optional[str] = None,
|
|
2157
|
+
current_password: Optional[str] = None,
|
|
2158
|
+
new_owner: Optional[str] = None,
|
|
2159
|
+
owner_name: Optional[str] = None,
|
|
2160
|
+
new_name: Optional[str] = None,
|
|
2161
|
+
):
|
|
2162
|
+
"""Handle hotkey update command."""
|
|
2163
|
+
try:
|
|
2164
|
+
# STEP 1: Collect input parameters
|
|
2165
|
+
request = prompt_wallet_update(
|
|
2166
|
+
current_name=name,
|
|
2167
|
+
new_name=new_name,
|
|
2168
|
+
new_password=new_password,
|
|
2169
|
+
new_coldkey=new_owner,
|
|
2170
|
+
is_hotkey=True,
|
|
2171
|
+
coldkey_name=owner_name,
|
|
2172
|
+
)
|
|
2173
|
+
except AmbiguousWalletError as e:
|
|
2174
|
+
# FIX: If prompt didn't handle it, handle it here
|
|
2175
|
+
from ...ui.display import print_warning
|
|
2176
|
+
from ...ui.components import HTCLITable
|
|
2177
|
+
from ...ui.prompts import prompt_for_required
|
|
2178
|
+
|
|
2179
|
+
console.print()
|
|
2180
|
+
print_warning(
|
|
2181
|
+
f"Found {len(e.matches)} hotkeys named '{e.name}'. Please select which one to update:"
|
|
2182
|
+
)
|
|
2183
|
+
|
|
2184
|
+
table = HTCLITable(title="Select Hotkey")
|
|
2185
|
+
table.add_column("Index", style="bold white", width=6)
|
|
2186
|
+
table.add_column("Coldkey", style="bold yellow", width=25)
|
|
2187
|
+
table.add_column("Address", style="white", width=30)
|
|
2188
|
+
|
|
2189
|
+
for i, match in enumerate(e.matches):
|
|
2190
|
+
owner_ck = match.get("owner_coldkey_name", "Unknown")
|
|
2191
|
+
address = (
|
|
2192
|
+
match.get("evm_address")
|
|
2193
|
+
or match.get("ss58_address")
|
|
2194
|
+
or match.get("address", "N/A")
|
|
2195
|
+
)
|
|
2196
|
+
if address != "N/A":
|
|
2197
|
+
from ...ui.colors import format_address_display
|
|
2198
|
+
|
|
2199
|
+
address_display = format_address_display(address)
|
|
2200
|
+
else:
|
|
2201
|
+
address_display = address
|
|
2202
|
+
table.add_row(str(i + 1), owner_ck, address_display)
|
|
2203
|
+
|
|
2204
|
+
console.print(table.table)
|
|
2205
|
+
console.print()
|
|
2206
|
+
|
|
2207
|
+
choice = prompt_for_required(
|
|
2208
|
+
f"Select hotkey to update (1-{len(e.matches)})",
|
|
2209
|
+
int,
|
|
2210
|
+
f"Enter a number from 1 to {len(e.matches)}",
|
|
2211
|
+
)
|
|
2212
|
+
|
|
2213
|
+
if not (1 <= choice <= len(e.matches)):
|
|
2214
|
+
raise ValueError(
|
|
2215
|
+
f"Invalid selection. Please choose between 1 and {len(e.matches)}"
|
|
2216
|
+
)
|
|
2217
|
+
|
|
2218
|
+
selected_match = e.matches[choice - 1]
|
|
2219
|
+
|
|
2220
|
+
# Retry with selected owner
|
|
2221
|
+
owner_coldkey_name = selected_match.get("owner_coldkey_name")
|
|
2222
|
+
try:
|
|
2223
|
+
request = prompt_wallet_update(
|
|
2224
|
+
current_name=e.name,
|
|
2225
|
+
new_name=new_name,
|
|
2226
|
+
new_password=new_password,
|
|
2227
|
+
new_coldkey=new_owner,
|
|
2228
|
+
is_hotkey=True,
|
|
2229
|
+
coldkey_name=owner_coldkey_name,
|
|
2230
|
+
)
|
|
2231
|
+
except Exception as e:
|
|
2232
|
+
from ...errors.handlers import handle_wallet_error
|
|
2233
|
+
from ...ui.display import print_error
|
|
2234
|
+
|
|
2235
|
+
error = handle_wallet_error(e, "update")
|
|
2236
|
+
print_error(error.message, suggestions=error.suggestions)
|
|
2237
|
+
return
|
|
2238
|
+
|
|
2239
|
+
except Exception as e:
|
|
2240
|
+
from ...errors.handlers import handle_wallet_error
|
|
2241
|
+
from ...ui.display import print_error
|
|
2242
|
+
|
|
2243
|
+
error = handle_wallet_error(e, "update")
|
|
2244
|
+
print_error(error.message, suggestions=error.suggestions)
|
|
2245
|
+
return
|
|
2246
|
+
|
|
2247
|
+
try:
|
|
2248
|
+
# STEP 2: Execute via client layer
|
|
2249
|
+
wallet_manager = WalletManager()
|
|
2250
|
+
|
|
2251
|
+
# Get owner address for disambiguation if needed
|
|
2252
|
+
owner_address = None
|
|
2253
|
+
if request.owner_name:
|
|
2254
|
+
try:
|
|
2255
|
+
owner_info = wallet_manager.get_wallet_info(
|
|
2256
|
+
request.owner_name, is_hotkey=False
|
|
2257
|
+
)
|
|
2258
|
+
owner_address = (
|
|
2259
|
+
owner_info["data"].get("evm_address")
|
|
2260
|
+
or owner_info["data"].get("ss58_address")
|
|
2261
|
+
or owner_info["data"].get("address")
|
|
2262
|
+
)
|
|
2263
|
+
except Exception:
|
|
2264
|
+
pass
|
|
2265
|
+
|
|
2266
|
+
# STEP 2a: Handle on-chain owner update if owner is being changed
|
|
2267
|
+
on_chain_update_result = None
|
|
2268
|
+
if request.new_owner:
|
|
2269
|
+
# Owner is being changed - need to update on-chain if hotkey is registered
|
|
2270
|
+
from ...dependencies import get_client
|
|
2271
|
+
from ...ui.display import print_warning, print_info
|
|
2272
|
+
from ...ui.prompts import confirm_prompt
|
|
2273
|
+
|
|
2274
|
+
# Get hotkey address first (before updating off-chain)
|
|
2275
|
+
hotkey_info = wallet_manager.get_wallet_info(
|
|
2276
|
+
request.current_name,
|
|
2277
|
+
is_hotkey=True,
|
|
2278
|
+
owner_address=owner_address,
|
|
2279
|
+
owner_coldkey_name=request.owner_name,
|
|
2280
|
+
)
|
|
2281
|
+
hotkey_address = (
|
|
2282
|
+
hotkey_info["data"].get("evm_address")
|
|
2283
|
+
or hotkey_info["data"].get("ss58_address")
|
|
2284
|
+
or hotkey_info["data"].get("address")
|
|
2285
|
+
)
|
|
2286
|
+
|
|
2287
|
+
# Get new owner address
|
|
2288
|
+
new_owner_info = wallet_manager.get_wallet_info(
|
|
2289
|
+
request.new_owner, is_hotkey=False
|
|
2290
|
+
)
|
|
2291
|
+
new_owner_address = (
|
|
2292
|
+
new_owner_info["data"].get("evm_address")
|
|
2293
|
+
or new_owner_info["data"].get("ss58_address")
|
|
2294
|
+
or new_owner_info["data"].get("address")
|
|
2295
|
+
)
|
|
2296
|
+
|
|
2297
|
+
# Get old owner address and keypair for signing
|
|
2298
|
+
old_owner_address = hotkey_info["data"].get("owner_address")
|
|
2299
|
+
old_owner_name = hotkey_info["data"].get("owner_coldkey_name")
|
|
2300
|
+
|
|
2301
|
+
if not old_owner_name:
|
|
2302
|
+
# Try to find old owner name from address
|
|
2303
|
+
all_keys = list_keys()
|
|
2304
|
+
for key in all_keys:
|
|
2305
|
+
if not key.get("is_hotkey", False):
|
|
2306
|
+
key_addr = (
|
|
2307
|
+
key.get("evm_address")
|
|
2308
|
+
or key.get("ss58_address")
|
|
2309
|
+
or key.get("address")
|
|
2310
|
+
)
|
|
2311
|
+
if key_addr and key_addr.lower() == old_owner_address.lower():
|
|
2312
|
+
old_owner_name = key.get("name")
|
|
2313
|
+
break
|
|
2314
|
+
|
|
2315
|
+
# Check if hotkey is registered on-chain
|
|
2316
|
+
client = get_client()
|
|
2317
|
+
if not client.substrate:
|
|
2318
|
+
if not client.connect():
|
|
2319
|
+
console.print()
|
|
2320
|
+
print_warning(
|
|
2321
|
+
"Could not connect to blockchain to check on-chain owner"
|
|
2322
|
+
)
|
|
2323
|
+
console.print()
|
|
2324
|
+
if not confirm_prompt(
|
|
2325
|
+
"Continue with file-only update? (on-chain owner will NOT be updated)",
|
|
2326
|
+
default=False,
|
|
2327
|
+
):
|
|
2328
|
+
console.print()
|
|
2329
|
+
print_info("Owner change cancelled")
|
|
2330
|
+
return
|
|
2331
|
+
else:
|
|
2332
|
+
# Connection successful, continue
|
|
2333
|
+
pass
|
|
2334
|
+
|
|
2335
|
+
# Query on-chain owner
|
|
2336
|
+
on_chain_owner = None
|
|
2337
|
+
console.print() # Add blank line before spinner
|
|
2338
|
+
with HTCLISpinner("Checking if hotkey is registered on-chain..."):
|
|
2339
|
+
try:
|
|
2340
|
+
import time
|
|
2341
|
+
|
|
2342
|
+
time.sleep(0.1) # Small delay to ensure spinner is visible
|
|
2343
|
+
owner_result = client.substrate.query(
|
|
2344
|
+
"Network", "HotkeyOwner", [hotkey_address]
|
|
2345
|
+
)
|
|
2346
|
+
if owner_result and owner_result.value:
|
|
2347
|
+
owner_value = owner_result.value
|
|
2348
|
+
# Check if owner is the zero address (means no owner registered)
|
|
2349
|
+
if isinstance(owner_value, str):
|
|
2350
|
+
owner_hex = owner_value.lower().replace("0x", "")
|
|
2351
|
+
if owner_hex and not all(c == "0" for c in owner_hex):
|
|
2352
|
+
on_chain_owner = owner_value
|
|
2353
|
+
elif isinstance(owner_value, (bytes, list)):
|
|
2354
|
+
owner_bytes = (
|
|
2355
|
+
bytes(owner_value)
|
|
2356
|
+
if isinstance(owner_value, list)
|
|
2357
|
+
else owner_value
|
|
2358
|
+
)
|
|
2359
|
+
if owner_bytes and not all(b == 0 for b in owner_bytes):
|
|
2360
|
+
from ...utils.blockchain.formatting import (
|
|
2361
|
+
to_checksum_address,
|
|
2362
|
+
)
|
|
2363
|
+
|
|
2364
|
+
on_chain_owner = to_checksum_address(
|
|
2365
|
+
"0x" + owner_bytes.hex()
|
|
2366
|
+
)
|
|
2367
|
+
else:
|
|
2368
|
+
owner_str = str(owner_value)
|
|
2369
|
+
if (
|
|
2370
|
+
owner_str
|
|
2371
|
+
!= "0x0000000000000000000000000000000000000000"
|
|
2372
|
+
):
|
|
2373
|
+
on_chain_owner = owner_str
|
|
2374
|
+
except Exception as e:
|
|
2375
|
+
logger.warning(f"Failed to query on-chain owner: {e}")
|
|
2376
|
+
on_chain_owner = None
|
|
2377
|
+
|
|
2378
|
+
if on_chain_owner:
|
|
2379
|
+
# Hotkey is registered on-chain - need to update on-chain owner
|
|
2380
|
+
console.print()
|
|
2381
|
+
console.print(
|
|
2382
|
+
info("🔗 Hotkey is registered on-chain. Updating on-chain owner...")
|
|
2383
|
+
)
|
|
2384
|
+
console.print()
|
|
2385
|
+
|
|
2386
|
+
# Verify old owner matches on-chain owner
|
|
2387
|
+
if on_chain_owner.lower() != old_owner_address.lower():
|
|
2388
|
+
console.print()
|
|
2389
|
+
print_warning(
|
|
2390
|
+
f"On-chain owner ({on_chain_owner}) does not match local owner ({old_owner_address})"
|
|
2391
|
+
)
|
|
2392
|
+
console.print()
|
|
2393
|
+
if not confirm_prompt(
|
|
2394
|
+
"Continue anyway? This may fail if you don't own the hotkey on-chain.",
|
|
2395
|
+
default=False,
|
|
2396
|
+
):
|
|
2397
|
+
console.print()
|
|
2398
|
+
print_info("Owner change cancelled")
|
|
2399
|
+
return
|
|
2400
|
+
|
|
2401
|
+
# Get old owner keypair for signing
|
|
2402
|
+
if not old_owner_name:
|
|
2403
|
+
console.print()
|
|
2404
|
+
print_warning(
|
|
2405
|
+
"Could not determine old owner name. Cannot update on-chain owner."
|
|
2406
|
+
)
|
|
2407
|
+
console.print()
|
|
2408
|
+
if not confirm_prompt(
|
|
2409
|
+
"Continue with file-only update? (on-chain owner will NOT be updated)",
|
|
2410
|
+
default=False,
|
|
2411
|
+
):
|
|
2412
|
+
console.print()
|
|
2413
|
+
print_info("Owner change cancelled")
|
|
2414
|
+
return
|
|
2415
|
+
old_owner_keypair = None
|
|
2416
|
+
else:
|
|
2417
|
+
# Load old owner keypair
|
|
2418
|
+
try:
|
|
2419
|
+
old_owner_wallet_info = wallet_manager.get_wallet_info(
|
|
2420
|
+
old_owner_name, is_hotkey=False
|
|
2421
|
+
)
|
|
2422
|
+
# Get keypair - this will prompt for password if needed
|
|
2423
|
+
from ...utils.wallet.core import (
|
|
2424
|
+
_get_keypair_with_password_retry,
|
|
2425
|
+
)
|
|
2426
|
+
|
|
2427
|
+
old_owner_name_resolved, old_owner_keypair = (
|
|
2428
|
+
_get_keypair_with_password_retry(
|
|
2429
|
+
client, old_owner_name, old_owner_wallet_info["data"], 3
|
|
2430
|
+
)
|
|
2431
|
+
)
|
|
2432
|
+
# Verify the keypair matches the old owner address
|
|
2433
|
+
old_keypair_address = old_owner_keypair.ss58_address
|
|
2434
|
+
if old_keypair_address.lower() != old_owner_address.lower():
|
|
2435
|
+
console.print()
|
|
2436
|
+
print_warning(
|
|
2437
|
+
f"⚠️ Loaded keypair address ({old_keypair_address}) does not match expected owner ({old_owner_address})"
|
|
2438
|
+
)
|
|
2439
|
+
console.print()
|
|
2440
|
+
if not confirm_prompt(
|
|
2441
|
+
"Continue anyway? This may fail if you don't own the hotkey on-chain.",
|
|
2442
|
+
default=False,
|
|
2443
|
+
):
|
|
2444
|
+
console.print()
|
|
2445
|
+
print_info("Owner change cancelled")
|
|
2446
|
+
return
|
|
2447
|
+
except Exception as e:
|
|
2448
|
+
console.print()
|
|
2449
|
+
print_warning(f"Could not load old owner keypair: {str(e)}")
|
|
2450
|
+
console.print()
|
|
2451
|
+
if not confirm_prompt(
|
|
2452
|
+
"Continue with file-only update? (on-chain owner will NOT be updated)",
|
|
2453
|
+
default=False,
|
|
2454
|
+
):
|
|
2455
|
+
console.print()
|
|
2456
|
+
print_info("Owner change cancelled")
|
|
2457
|
+
return
|
|
2458
|
+
old_owner_keypair = None
|
|
2459
|
+
|
|
2460
|
+
if old_owner_keypair:
|
|
2461
|
+
# Update on-chain owner
|
|
2462
|
+
try:
|
|
2463
|
+
with HTCLISpinner("Updating on-chain owner..."):
|
|
2464
|
+
on_chain_update_result = (
|
|
2465
|
+
client.extrinsics.wallet.update_coldkey(
|
|
2466
|
+
hotkey=hotkey_address,
|
|
2467
|
+
new_coldkey=new_owner_address,
|
|
2468
|
+
keypair=old_owner_keypair,
|
|
2469
|
+
)
|
|
2470
|
+
)
|
|
2471
|
+
|
|
2472
|
+
# Check for errors in result
|
|
2473
|
+
if isinstance(
|
|
2474
|
+
on_chain_update_result, dict
|
|
2475
|
+
) and not on_chain_update_result.get("success", False):
|
|
2476
|
+
error_msg = on_chain_update_result.get(
|
|
2477
|
+
"message"
|
|
2478
|
+
) or on_chain_update_result.get(
|
|
2479
|
+
"error", "Update failed"
|
|
2480
|
+
)
|
|
2481
|
+
if handle_wallet_extrinsic_error(
|
|
2482
|
+
error_msg, "update coldkey", client
|
|
2483
|
+
):
|
|
2484
|
+
# Error was handled, ask if user wants to continue with file-only update
|
|
2485
|
+
console.print()
|
|
2486
|
+
if not confirm_prompt(
|
|
2487
|
+
"Continue with file-only update? (on-chain owner will remain unchanged)",
|
|
2488
|
+
default=False,
|
|
2489
|
+
):
|
|
2490
|
+
console.print()
|
|
2491
|
+
print_info("Owner change cancelled")
|
|
2492
|
+
return
|
|
2493
|
+
on_chain_update_result = None # Mark as failed
|
|
2494
|
+
else:
|
|
2495
|
+
# Error not handled, show warning and ask
|
|
2496
|
+
console.print()
|
|
2497
|
+
print_warning(
|
|
2498
|
+
f"On-chain update failed: {error_msg}"
|
|
2499
|
+
)
|
|
2500
|
+
console.print()
|
|
2501
|
+
if not confirm_prompt(
|
|
2502
|
+
"Continue with file-only update? (on-chain owner will NOT be updated)",
|
|
2503
|
+
default=False,
|
|
2504
|
+
):
|
|
2505
|
+
console.print()
|
|
2506
|
+
print_info("Owner change cancelled")
|
|
2507
|
+
return
|
|
2508
|
+
on_chain_update_result = None # Mark as failed
|
|
2509
|
+
else:
|
|
2510
|
+
# Success
|
|
2511
|
+
console.print()
|
|
2512
|
+
console.print(
|
|
2513
|
+
success(f"✅ On-chain owner updated successfully!")
|
|
2514
|
+
)
|
|
2515
|
+
console.print(
|
|
2516
|
+
info(
|
|
2517
|
+
f" Transaction: {on_chain_update_result.get('transaction_hash', 'N/A')}"
|
|
2518
|
+
)
|
|
2519
|
+
)
|
|
2520
|
+
if on_chain_update_result.get("block_hash"):
|
|
2521
|
+
console.print(
|
|
2522
|
+
info(
|
|
2523
|
+
f" Block Hash: {on_chain_update_result.get('block_hash')}"
|
|
2524
|
+
)
|
|
2525
|
+
)
|
|
2526
|
+
if on_chain_update_result.get("block_number"):
|
|
2527
|
+
console.print(
|
|
2528
|
+
info(
|
|
2529
|
+
f" Block Number: {on_chain_update_result.get('block_number')}"
|
|
2530
|
+
)
|
|
2531
|
+
)
|
|
2532
|
+
console.print()
|
|
2533
|
+
except Exception as e:
|
|
2534
|
+
error_str = str(e)
|
|
2535
|
+
if not handle_wallet_extrinsic_error(
|
|
2536
|
+
error_str, "update coldkey", client
|
|
2537
|
+
):
|
|
2538
|
+
# Error not handled by our handler, show generic warning
|
|
2539
|
+
console.print()
|
|
2540
|
+
print_warning(f"On-chain update failed: {error_str}")
|
|
2541
|
+
console.print()
|
|
2542
|
+
|
|
2543
|
+
# Ask if user wants to continue with file-only update
|
|
2544
|
+
if not confirm_prompt(
|
|
2545
|
+
"Continue with file-only update? (on-chain owner will remain unchanged)",
|
|
2546
|
+
default=False,
|
|
2547
|
+
):
|
|
2548
|
+
console.print()
|
|
2549
|
+
print_info("Owner change cancelled")
|
|
2550
|
+
return
|
|
2551
|
+
on_chain_update_result = None # Mark as failed
|
|
2552
|
+
else:
|
|
2553
|
+
# Hotkey is not registered on-chain - cannot change owner
|
|
2554
|
+
|
|
2555
|
+
print_warning("Hotkey is not registered on-chain. Cannot change owner.")
|
|
2556
|
+
console.print()
|
|
2557
|
+
print_info(
|
|
2558
|
+
"💡 To change the owner, the hotkey must first be registered on-chain (e.g., by registering a node)."
|
|
2559
|
+
)
|
|
2560
|
+
console.print()
|
|
2561
|
+
print_info("Owner change cancelled - no changes were made.")
|
|
2562
|
+
return
|
|
2563
|
+
|
|
2564
|
+
# STEP 2b: Update off-chain wallet file (only if on-chain update succeeded or owner not changed)
|
|
2565
|
+
result = wallet_manager.update_hotkey(
|
|
2566
|
+
name=request.current_name,
|
|
2567
|
+
new_name=request.new_name,
|
|
2568
|
+
new_password=request.new_password,
|
|
2569
|
+
remove_password=request.remove_password,
|
|
2570
|
+
new_owner=request.new_owner,
|
|
2571
|
+
current_password=request.current_password,
|
|
2572
|
+
owner_address=owner_address,
|
|
2573
|
+
)
|
|
2574
|
+
update_data = result["data"]
|
|
2575
|
+
|
|
2576
|
+
# Create response model
|
|
2577
|
+
response = WalletUpdateResponse(
|
|
2578
|
+
old_name=update_data["old_name"],
|
|
2579
|
+
new_name=update_data["new_name"],
|
|
2580
|
+
address=update_data.get("ss58_address")
|
|
2581
|
+
or update_data.get("evm_address")
|
|
2582
|
+
or update_data.get("address"),
|
|
2583
|
+
key_type=update_data["key_type"],
|
|
2584
|
+
name_updated=update_data["name_updated"],
|
|
2585
|
+
password_updated=update_data["password_updated"],
|
|
2586
|
+
owner_updated=update_data.get("owner_updated"),
|
|
2587
|
+
old_owner_address=update_data.get("old_owner_address"),
|
|
2588
|
+
new_owner_address=update_data.get("new_owner_address"),
|
|
2589
|
+
transaction_hash=(
|
|
2590
|
+
on_chain_update_result.get("transaction_hash")
|
|
2591
|
+
if on_chain_update_result
|
|
2592
|
+
else None
|
|
2593
|
+
),
|
|
2594
|
+
block_hash=(
|
|
2595
|
+
on_chain_update_result.get("block_hash")
|
|
2596
|
+
if on_chain_update_result
|
|
2597
|
+
else None
|
|
2598
|
+
),
|
|
2599
|
+
block_number=(
|
|
2600
|
+
on_chain_update_result.get("block_number")
|
|
2601
|
+
if on_chain_update_result
|
|
2602
|
+
else None
|
|
2603
|
+
),
|
|
2604
|
+
)
|
|
2605
|
+
|
|
2606
|
+
# STEP 3: Display results
|
|
2607
|
+
display_wallet_update_result(response)
|
|
2608
|
+
|
|
2609
|
+
except Exception as e:
|
|
2610
|
+
from ...errors.handlers import handle_wallet_error
|
|
2611
|
+
from ...ui.display import print_error
|
|
2612
|
+
|
|
2613
|
+
error = handle_wallet_error(e, "update")
|
|
2614
|
+
print_error(error.message, suggestions=error.suggestions)
|
|
2615
|
+
|
|
2616
|
+
|
|
2617
|
+
def describe_handler(name: Optional[str] = None):
|
|
2618
|
+
"""Handle wallet describe command."""
|
|
2619
|
+
try:
|
|
2620
|
+
# STEP 1: Collect input parameters
|
|
2621
|
+
if not name:
|
|
2622
|
+
from .prompts import prompt_wallet_selection
|
|
2623
|
+
|
|
2624
|
+
name = prompt_wallet_selection("Select wallet to describe")
|
|
2625
|
+
|
|
2626
|
+
# STEP 2: Get wallet information
|
|
2627
|
+
wallet_info = get_wallet_info_by_name(name)
|
|
2628
|
+
|
|
2629
|
+
# STEP 3: Get balance if it's a coldkey
|
|
2630
|
+
balance_info = None
|
|
2631
|
+
if not wallet_info.get("is_hotkey", False):
|
|
2632
|
+
try:
|
|
2633
|
+
from ...dependencies import get_client
|
|
2634
|
+
|
|
2635
|
+
client = get_client()
|
|
2636
|
+
# Ensure connection is established
|
|
2637
|
+
if not client.substrate:
|
|
2638
|
+
if not client.connect():
|
|
2639
|
+
balance_info = {"error": "Failed to connect to blockchain"}
|
|
2640
|
+
else:
|
|
2641
|
+
response = client.rpc.wallet.get_balance(wallet_info["address"])
|
|
2642
|
+
else:
|
|
2643
|
+
response = client.rpc.wallet.get_balance(wallet_info["address"])
|
|
2644
|
+
if response.success:
|
|
2645
|
+
# BalanceResponse has fields directly, not in data object
|
|
2646
|
+
balance_info = {
|
|
2647
|
+
"address": response.address,
|
|
2648
|
+
"balance": response.balance or 0,
|
|
2649
|
+
"available_balance": response.available_balance or 0,
|
|
2650
|
+
"locked_balance": response.locked_balance or 0,
|
|
2651
|
+
"reserved_balance": response.reserved_balance or 0,
|
|
2652
|
+
}
|
|
2653
|
+
else:
|
|
2654
|
+
raise Exception(response.message)
|
|
2655
|
+
except Exception as e:
|
|
2656
|
+
balance_info = {"error": f"Unable to fetch balance: {str(e)}"}
|
|
2657
|
+
|
|
2658
|
+
# STEP 4: Display results
|
|
2659
|
+
display_wallet_describe_result(wallet_info, balance_info)
|
|
2660
|
+
|
|
2661
|
+
except FileNotFoundError:
|
|
2662
|
+
from ...ui.colors import error
|
|
2663
|
+
from ...ui.display import HTCLIConsole
|
|
2664
|
+
|
|
2665
|
+
console = HTCLIConsole()
|
|
2666
|
+
console.print(error(f"Wallet '{name}' does not exist."))
|
|
2667
|
+
except Exception as e:
|
|
2668
|
+
from ...errors.handlers import handle_wallet_error
|
|
2669
|
+
from ...ui.display import print_error
|
|
2670
|
+
|
|
2671
|
+
error = handle_wallet_error(e, "update")
|
|
2672
|
+
print_error(error.message, suggestions=error.suggestions)
|
|
2673
|
+
|
|
2674
|
+
|
|
2675
|
+
def check_hotkey_owner_handler(
|
|
2676
|
+
hotkey_name: Optional[str] = None,
|
|
2677
|
+
hotkey_address: Optional[str] = None,
|
|
2678
|
+
coldkey_name: Optional[str] = None,
|
|
2679
|
+
):
|
|
2680
|
+
"""Check the on-chain owner of a hotkey."""
|
|
2681
|
+
try:
|
|
2682
|
+
from ...dependencies import get_client
|
|
2683
|
+
from ...ui.display import print_error
|
|
2684
|
+
|
|
2685
|
+
# STEP 1: Get hotkey address
|
|
2686
|
+
if not hotkey_address:
|
|
2687
|
+
if not hotkey_name:
|
|
2688
|
+
# Show list of hotkeys for user to select
|
|
2689
|
+
all_keys = list_keys()
|
|
2690
|
+
hotkeys = [k for k in all_keys if k.get("is_hotkey", False)]
|
|
2691
|
+
if not hotkeys:
|
|
2692
|
+
console.print(warning("No hotkeys found."))
|
|
2693
|
+
return
|
|
2694
|
+
|
|
2695
|
+
from ...ui.components import HTCLITable
|
|
2696
|
+
from ...ui.prompts import prompt_for_required
|
|
2697
|
+
|
|
2698
|
+
table = HTCLITable(
|
|
2699
|
+
title="Available Hotkeys",
|
|
2700
|
+
border_style="blue",
|
|
2701
|
+
header_style="bold cyan",
|
|
2702
|
+
)
|
|
2703
|
+
table.add_column("Index", style="bold white", width=6)
|
|
2704
|
+
table.add_column("Name", style="bold cyan", width=25)
|
|
2705
|
+
table.add_column("Coldkey", style="bold yellow", width=25)
|
|
2706
|
+
table.add_column("Address", style="white", width=30)
|
|
2707
|
+
|
|
2708
|
+
for i, hotkey in enumerate(hotkeys):
|
|
2709
|
+
coldkey_name_display = "Unknown"
|
|
2710
|
+
coldkey_address = hotkey.get("owner_address")
|
|
2711
|
+
if coldkey_address:
|
|
2712
|
+
for key in all_keys:
|
|
2713
|
+
if key.get(
|
|
2714
|
+
"ss58_address"
|
|
2715
|
+
) == coldkey_address and not key.get("is_hotkey", False):
|
|
2716
|
+
coldkey_name_display = key.get(
|
|
2717
|
+
"name", coldkey_address[:20] + "..."
|
|
2718
|
+
)
|
|
2719
|
+
break
|
|
2720
|
+
else:
|
|
2721
|
+
coldkey_name_display = coldkey_address[:20] + "..."
|
|
2722
|
+
|
|
2723
|
+
address_display = (
|
|
2724
|
+
hotkey.get("evm_address")
|
|
2725
|
+
or hotkey.get("ss58_address")
|
|
2726
|
+
or hotkey.get("address", "N/A")
|
|
2727
|
+
)
|
|
2728
|
+
if address_display != "N/A":
|
|
2729
|
+
from ...ui.colors import format_address_display
|
|
2730
|
+
|
|
2731
|
+
address_display = format_address_display(address_display)
|
|
2732
|
+
|
|
2733
|
+
table.add_row(
|
|
2734
|
+
str(i + 1),
|
|
2735
|
+
hotkey.get("name", "N/A"),
|
|
2736
|
+
coldkey_name_display,
|
|
2737
|
+
address_display,
|
|
2738
|
+
)
|
|
2739
|
+
|
|
2740
|
+
console.print(table.table)
|
|
2741
|
+
console.print()
|
|
2742
|
+
|
|
2743
|
+
choice = prompt_for_required(
|
|
2744
|
+
f"Select hotkey to check owner (1-{len(hotkeys)})",
|
|
2745
|
+
int,
|
|
2746
|
+
f"Enter a number from 1 to {len(hotkeys)}",
|
|
2747
|
+
)
|
|
2748
|
+
|
|
2749
|
+
if not (1 <= choice <= len(hotkeys)):
|
|
2750
|
+
raise ValueError(
|
|
2751
|
+
f"Invalid selection. Please choose between 1 and {len(hotkeys)}"
|
|
2752
|
+
)
|
|
2753
|
+
|
|
2754
|
+
selected_hotkey = hotkeys[choice - 1]
|
|
2755
|
+
hotkey_name = selected_hotkey["name"]
|
|
2756
|
+
coldkey_name = None
|
|
2757
|
+
# Try to find coldkey name
|
|
2758
|
+
coldkey_address = selected_hotkey.get("owner_address")
|
|
2759
|
+
if coldkey_address:
|
|
2760
|
+
for key in all_keys:
|
|
2761
|
+
if key.get("ss58_address") == coldkey_address and not key.get(
|
|
2762
|
+
"is_hotkey", False
|
|
2763
|
+
):
|
|
2764
|
+
coldkey_name = key.get("name")
|
|
2765
|
+
break
|
|
2766
|
+
|
|
2767
|
+
# Get hotkey info
|
|
2768
|
+
if coldkey_name:
|
|
2769
|
+
# Use coldkey to disambiguate
|
|
2770
|
+
wallet_manager = WalletManager()
|
|
2771
|
+
try:
|
|
2772
|
+
coldkey_info_result = wallet_manager.get_wallet_info(
|
|
2773
|
+
coldkey_name, is_hotkey=False
|
|
2774
|
+
)
|
|
2775
|
+
coldkey_info = coldkey_info_result["data"]
|
|
2776
|
+
owner_address = coldkey_info.get(
|
|
2777
|
+
"ss58_address"
|
|
2778
|
+
) or coldkey_info.get("address")
|
|
2779
|
+
except (ValueError, FileNotFoundError) as e:
|
|
2780
|
+
console.print(
|
|
2781
|
+
warning(f"❌ Coldkey '{coldkey_name}' does not exist.")
|
|
2782
|
+
)
|
|
2783
|
+
return
|
|
2784
|
+
|
|
2785
|
+
# Get hotkey info
|
|
2786
|
+
try:
|
|
2787
|
+
hotkey_info_result = wallet_manager.get_wallet_info(
|
|
2788
|
+
hotkey_name,
|
|
2789
|
+
is_hotkey=True,
|
|
2790
|
+
owner_address=owner_address,
|
|
2791
|
+
owner_coldkey_name=coldkey_name,
|
|
2792
|
+
)
|
|
2793
|
+
hotkey_info = hotkey_info_result["data"]
|
|
2794
|
+
hotkey_address = (
|
|
2795
|
+
hotkey_info.get("evm_address")
|
|
2796
|
+
or hotkey_info.get("ss58_address")
|
|
2797
|
+
or hotkey_info.get("address")
|
|
2798
|
+
)
|
|
2799
|
+
except (ValueError, FileNotFoundError) as e:
|
|
2800
|
+
console.print(
|
|
2801
|
+
warning(
|
|
2802
|
+
f"❌ Hotkey '{hotkey_name}' owned by '{coldkey_name}' does not exist."
|
|
2803
|
+
)
|
|
2804
|
+
)
|
|
2805
|
+
return
|
|
2806
|
+
else:
|
|
2807
|
+
# Get hotkey info without owner context
|
|
2808
|
+
wallet_info = get_wallet_info_by_name(hotkey_name, is_hotkey=True)
|
|
2809
|
+
if not wallet_info.get("is_hotkey", False):
|
|
2810
|
+
console.print(warning(f"❌ '{hotkey_name}' is not a hotkey."))
|
|
2811
|
+
return
|
|
2812
|
+
hotkey_address = (
|
|
2813
|
+
wallet_info.get("evm_address")
|
|
2814
|
+
or wallet_info.get("ss58_address")
|
|
2815
|
+
or wallet_info.get("address")
|
|
2816
|
+
)
|
|
2817
|
+
|
|
2818
|
+
# STEP 2: Connect to blockchain and query
|
|
2819
|
+
client = get_client()
|
|
2820
|
+
if not client.substrate:
|
|
2821
|
+
if not client.connect():
|
|
2822
|
+
print_error(
|
|
2823
|
+
"Failed to connect to blockchain. Cannot check hotkey owner."
|
|
2824
|
+
)
|
|
2825
|
+
return
|
|
2826
|
+
|
|
2827
|
+
# STEP 3: Query on-chain owner
|
|
2828
|
+
console.print()
|
|
2829
|
+
console.print(
|
|
2830
|
+
info(f"🔍 Checking on-chain owner for hotkey: {address(hotkey_address)}")
|
|
2831
|
+
)
|
|
2832
|
+
console.print()
|
|
2833
|
+
|
|
2834
|
+
on_chain_owner = None
|
|
2835
|
+
try:
|
|
2836
|
+
owner_result = client.substrate.query(
|
|
2837
|
+
"Network", "HotkeyOwner", [hotkey_address]
|
|
2838
|
+
)
|
|
2839
|
+
if owner_result and owner_result.value:
|
|
2840
|
+
owner_value = owner_result.value
|
|
2841
|
+
# Check if owner is the zero address (means no owner registered)
|
|
2842
|
+
if isinstance(owner_value, str):
|
|
2843
|
+
owner_hex = owner_value.lower().replace("0x", "")
|
|
2844
|
+
if owner_hex and not all(c == "0" for c in owner_hex):
|
|
2845
|
+
on_chain_owner = owner_value
|
|
2846
|
+
elif isinstance(owner_value, (bytes, list)):
|
|
2847
|
+
owner_bytes = (
|
|
2848
|
+
bytes(owner_value)
|
|
2849
|
+
if isinstance(owner_value, list)
|
|
2850
|
+
else owner_value
|
|
2851
|
+
)
|
|
2852
|
+
if owner_bytes and not all(b == 0 for b in owner_bytes):
|
|
2853
|
+
from ...utils.blockchain.formatting import to_checksum_address
|
|
2854
|
+
|
|
2855
|
+
on_chain_owner = to_checksum_address("0x" + owner_bytes.hex())
|
|
2856
|
+
else:
|
|
2857
|
+
owner_str = str(owner_value)
|
|
2858
|
+
if owner_str != "0x0000000000000000000000000000000000000000":
|
|
2859
|
+
on_chain_owner = owner_str
|
|
2860
|
+
except Exception as e:
|
|
2861
|
+
logger.error(f"Failed to query on-chain owner: {e}")
|
|
2862
|
+
console.print(warning(f"Could not query on-chain owner: {str(e)}"))
|
|
2863
|
+
return
|
|
2864
|
+
|
|
2865
|
+
# STEP 4: Display results
|
|
2866
|
+
console.print()
|
|
2867
|
+
if on_chain_owner:
|
|
2868
|
+
console.print(success("✅ Hotkey has an on-chain owner"))
|
|
2869
|
+
console.print()
|
|
2870
|
+
console.print(f"[bold]Hotkey Address:[/bold] {address(hotkey_address)}")
|
|
2871
|
+
console.print(f"[bold]On-Chain Owner:[/bold] {address(on_chain_owner)}")
|
|
2872
|
+
|
|
2873
|
+
# Try to find if this owner is a known coldkey
|
|
2874
|
+
all_keys = list_keys()
|
|
2875
|
+
owner_name = None
|
|
2876
|
+
for key in all_keys:
|
|
2877
|
+
key_address = (
|
|
2878
|
+
key.get("evm_address")
|
|
2879
|
+
or key.get("ss58_address")
|
|
2880
|
+
or key.get("address")
|
|
2881
|
+
)
|
|
2882
|
+
if (
|
|
2883
|
+
key_address
|
|
2884
|
+
and key_address.lower() == on_chain_owner.lower()
|
|
2885
|
+
and not key.get("is_hotkey", False)
|
|
2886
|
+
):
|
|
2887
|
+
owner_name = key.get("name")
|
|
2888
|
+
break
|
|
2889
|
+
|
|
2890
|
+
if owner_name:
|
|
2891
|
+
console.print(f"[bold]Owner Name:[/bold] {owner_name}")
|
|
2892
|
+
else:
|
|
2893
|
+
console.print(warning("Hotkey is not registered on-chain"))
|
|
2894
|
+
console.print()
|
|
2895
|
+
console.print(f"[bold]Hotkey Address:[/bold] {address(hotkey_address)}")
|
|
2896
|
+
console.print("[bold]On-Chain Owner:[/bold] None (not registered)")
|
|
2897
|
+
console.print()
|
|
2898
|
+
console.print(
|
|
2899
|
+
info(
|
|
2900
|
+
"💡 This hotkey can be registered on-chain when registering a node."
|
|
2901
|
+
)
|
|
2902
|
+
)
|
|
2903
|
+
|
|
2904
|
+
console.print()
|
|
2905
|
+
|
|
2906
|
+
except Exception as e:
|
|
2907
|
+
from ...errors.handlers import handle_wallet_error
|
|
2908
|
+
from ...ui.display import print_error
|
|
2909
|
+
|
|
2910
|
+
error = handle_wallet_error(e, "check-hotkey-owner")
|
|
2911
|
+
print_error(error.message, suggestions=error.suggestions)
|
|
2912
|
+
|
|
2913
|
+
|
|
2914
|
+
def upgrade_wallet_layout_handler(
|
|
2915
|
+
*,
|
|
2916
|
+
dry_run: bool = False,
|
|
2917
|
+
force: bool = False,
|
|
2918
|
+
backup: bool = True,
|
|
2919
|
+
wallet_names: Optional[list[str]] = None,
|
|
2920
|
+
):
|
|
2921
|
+
"""Handle wallet layout upgrade command."""
|
|
2922
|
+
try:
|
|
2923
|
+
wallet_manager = WalletManager()
|
|
2924
|
+
result = wallet_manager.upgrade_wallet_layout(
|
|
2925
|
+
dry_run=dry_run,
|
|
2926
|
+
force=force,
|
|
2927
|
+
backup=backup,
|
|
2928
|
+
wallets=wallet_names,
|
|
2929
|
+
log=console.print,
|
|
2930
|
+
)
|
|
2931
|
+
summary = result.get("data", {})
|
|
2932
|
+
migrated = len(summary.get("migrated", []))
|
|
2933
|
+
skipped = len(summary.get("skipped", []))
|
|
2934
|
+
warnings_list = summary.get("warnings", [])
|
|
2935
|
+
|
|
2936
|
+
console.print(
|
|
2937
|
+
"[success]Wallet layout upgrade complete[/success] "
|
|
2938
|
+
f"(migrated={migrated}, skipped={skipped}, warnings={len(warnings_list)})"
|
|
2939
|
+
)
|
|
2940
|
+
if warnings_list:
|
|
2941
|
+
console.print()
|
|
2942
|
+
console.print("[warning]Warnings:[/warning]")
|
|
2943
|
+
for warning_msg in warnings_list:
|
|
2944
|
+
console.print(f"[warning]- {warning_msg}[/warning]")
|
|
2945
|
+
except Exception as e:
|
|
2946
|
+
from ...errors.handlers import handle_wallet_error
|
|
2947
|
+
|
|
2948
|
+
error = handle_wallet_error(e, "update")
|
|
2949
|
+
console.print(f"[error]{error.message}[/error]")
|
|
2950
|
+
if error.suggestions:
|
|
2951
|
+
for suggestion in error.suggestions:
|
|
2952
|
+
console.print(f"[info]- {suggestion}[/info]")
|
|
2953
|
+
|
|
2954
|
+
|
|
2955
|
+
def update_identity_handler(
|
|
2956
|
+
hotkey_name: Optional[str] = None,
|
|
2957
|
+
name: Optional[str] = None,
|
|
2958
|
+
url: Optional[str] = None,
|
|
2959
|
+
image: Optional[str] = None,
|
|
2960
|
+
discord: Optional[str] = None,
|
|
2961
|
+
x: Optional[str] = None,
|
|
2962
|
+
telegram: Optional[str] = None,
|
|
2963
|
+
github: Optional[str] = None,
|
|
2964
|
+
hugging_face: Optional[str] = None,
|
|
2965
|
+
description: Optional[str] = None,
|
|
2966
|
+
misc: Optional[str] = None,
|
|
2967
|
+
password: Optional[str] = None,
|
|
2968
|
+
):
|
|
2969
|
+
"""Handle identity update/registration command."""
|
|
2970
|
+
try:
|
|
2971
|
+
from ...dependencies import get_client
|
|
2972
|
+
from ...ui.display import print_error, print_success
|
|
2973
|
+
from ...utils.wallet.core import _get_keypair_with_password_retry
|
|
2974
|
+
|
|
2975
|
+
# STEP 1: Collect input parameters
|
|
2976
|
+
hotkey_name, password, request = prompt_identity_update(
|
|
2977
|
+
hotkey_name=hotkey_name,
|
|
2978
|
+
name=name,
|
|
2979
|
+
url=url,
|
|
2980
|
+
image=image,
|
|
2981
|
+
discord=discord,
|
|
2982
|
+
x=x,
|
|
2983
|
+
telegram=telegram,
|
|
2984
|
+
github=github,
|
|
2985
|
+
hugging_face=hugging_face,
|
|
2986
|
+
description=description,
|
|
2987
|
+
misc=misc,
|
|
2988
|
+
password=password,
|
|
2989
|
+
)
|
|
2990
|
+
|
|
2991
|
+
# STEP 2: Connect to blockchain
|
|
2992
|
+
client = get_client()
|
|
2993
|
+
if not client.substrate:
|
|
2994
|
+
if not client.connect():
|
|
2995
|
+
print_error("Failed to connect to blockchain. Cannot update identity.")
|
|
2996
|
+
return
|
|
2997
|
+
|
|
2998
|
+
# STEP 3: Load wallet
|
|
2999
|
+
wallet_manager = WalletManager()
|
|
3000
|
+
console.print(info(f"🔐 Unlocking hotkey '{hotkey_name}'..."))
|
|
3001
|
+
|
|
3002
|
+
try:
|
|
3003
|
+
wallet_info = wallet_manager.get_wallet_info(hotkey_name, is_hotkey=True)
|
|
3004
|
+
except Exception:
|
|
3005
|
+
print_error(f"Wallet '{hotkey_name}' not found.")
|
|
3006
|
+
return
|
|
3007
|
+
|
|
3008
|
+
# Update request with address
|
|
3009
|
+
address = wallet_info["data"].get("ss58_address") or wallet_info["data"].get(
|
|
3010
|
+
"address"
|
|
3011
|
+
)
|
|
3012
|
+
request.hotkey = address
|
|
3013
|
+
|
|
3014
|
+
# Get keypair
|
|
3015
|
+
_, keypair = _get_keypair_with_password_retry(
|
|
3016
|
+
client, hotkey_name, wallet_info["data"], 3, password
|
|
3017
|
+
)
|
|
3018
|
+
|
|
3019
|
+
# STEP 4: Submit extrinsic
|
|
3020
|
+
console.print()
|
|
3021
|
+
with HTCLISpinner("Updating identity on-chain..."):
|
|
3022
|
+
response = client.extrinsics.identity.register_or_update_identity(
|
|
3023
|
+
request, keypair
|
|
3024
|
+
)
|
|
3025
|
+
|
|
3026
|
+
# STEP 5: Display result
|
|
3027
|
+
if response["success"]:
|
|
3028
|
+
display_identity_update_result(response, address)
|
|
3029
|
+
else:
|
|
3030
|
+
error_msg = response.get("message") or response.get(
|
|
3031
|
+
"error", "Unknown error"
|
|
3032
|
+
)
|
|
3033
|
+
print_error(f"Identity update failed: {error_msg}")
|
|
3034
|
+
|
|
3035
|
+
except Exception as e:
|
|
3036
|
+
from ...errors.handlers import handle_wallet_error
|
|
3037
|
+
from ...ui.display import print_error
|
|
3038
|
+
|
|
3039
|
+
error = handle_wallet_error(e, "identity")
|
|
3040
|
+
print_error(error.message, suggestions=error.suggestions)
|