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.
Files changed (140) hide show
  1. htcli-1.1.0.dist-info/METADATA +509 -0
  2. htcli-1.1.0.dist-info/RECORD +140 -0
  3. htcli-1.1.0.dist-info/WHEEL +4 -0
  4. htcli-1.1.0.dist-info/entry_points.txt +2 -0
  5. htcli-1.1.0.dist-info/licenses/LICENSE +21 -0
  6. src/__init__.py +0 -0
  7. src/htcli/__init__.py +5 -0
  8. src/htcli/client/__init__.py +338 -0
  9. src/htcli/client/extrinsics/__init__.py +26 -0
  10. src/htcli/client/extrinsics/base.py +487 -0
  11. src/htcli/client/extrinsics/consensus.py +79 -0
  12. src/htcli/client/extrinsics/governance.py +714 -0
  13. src/htcli/client/extrinsics/identity.py +490 -0
  14. src/htcli/client/extrinsics/node.py +1054 -0
  15. src/htcli/client/extrinsics/overwatch.py +401 -0
  16. src/htcli/client/extrinsics/staking.py +1504 -0
  17. src/htcli/client/extrinsics/subnet.py +2218 -0
  18. src/htcli/client/extrinsics/validator.py +203 -0
  19. src/htcli/client/extrinsics/wallet.py +323 -0
  20. src/htcli/client/offchain/__init__.py +10 -0
  21. src/htcli/client/offchain/backup.py +385 -0
  22. src/htcli/client/offchain/config.py +541 -0
  23. src/htcli/client/offchain/wallet.py +839 -0
  24. src/htcli/client/rpc/__init__.py +20 -0
  25. src/htcli/client/rpc/chain.py +568 -0
  26. src/htcli/client/rpc/node.py +783 -0
  27. src/htcli/client/rpc/overwatch.py +680 -0
  28. src/htcli/client/rpc/staking.py +216 -0
  29. src/htcli/client/rpc/subnet.py +2104 -0
  30. src/htcli/client/rpc/wallet.py +912 -0
  31. src/htcli/commands/__init__.py +31 -0
  32. src/htcli/commands/chain/__init__.py +66 -0
  33. src/htcli/commands/chain/display.py +204 -0
  34. src/htcli/commands/chain/handlers.py +260 -0
  35. src/htcli/commands/config/__init__.py +158 -0
  36. src/htcli/commands/config/display.py +353 -0
  37. src/htcli/commands/config/handlers.py +347 -0
  38. src/htcli/commands/config/prompts.py +357 -0
  39. src/htcli/commands/consensus/__init__.py +61 -0
  40. src/htcli/commands/consensus/handlers.py +100 -0
  41. src/htcli/commands/governance/__init__.py +49 -0
  42. src/htcli/commands/governance/handlers.py +81 -0
  43. src/htcli/commands/node/__init__.py +304 -0
  44. src/htcli/commands/node/display.py +749 -0
  45. src/htcli/commands/node/error_handling.py +470 -0
  46. src/htcli/commands/node/handlers.py +844 -0
  47. src/htcli/commands/node/prompts.py +346 -0
  48. src/htcli/commands/overwatch/__init__.py +219 -0
  49. src/htcli/commands/overwatch/display.py +396 -0
  50. src/htcli/commands/overwatch/error_handling.py +276 -0
  51. src/htcli/commands/overwatch/handlers.py +443 -0
  52. src/htcli/commands/overwatch/prompts.py +359 -0
  53. src/htcli/commands/stake/__init__.py +736 -0
  54. src/htcli/commands/stake/display.py +1103 -0
  55. src/htcli/commands/stake/error_handling.py +425 -0
  56. src/htcli/commands/stake/handlers.py +1902 -0
  57. src/htcli/commands/stake/prompts.py +1080 -0
  58. src/htcli/commands/subnet/__init__.py +639 -0
  59. src/htcli/commands/subnet/display.py +801 -0
  60. src/htcli/commands/subnet/error_handling.py +524 -0
  61. src/htcli/commands/subnet/handlers.py +2855 -0
  62. src/htcli/commands/subnet/prompts.py +1225 -0
  63. src/htcli/commands/validator/__init__.py +192 -0
  64. src/htcli/commands/validator/display.py +54 -0
  65. src/htcli/commands/validator/handlers.py +340 -0
  66. src/htcli/commands/wallet/__init__.py +546 -0
  67. src/htcli/commands/wallet/display.py +806 -0
  68. src/htcli/commands/wallet/error_handling.py +210 -0
  69. src/htcli/commands/wallet/handlers.py +3040 -0
  70. src/htcli/commands/wallet/prompts.py +1518 -0
  71. src/htcli/config.py +184 -0
  72. src/htcli/dependencies.py +186 -0
  73. src/htcli/errors/__init__.py +63 -0
  74. src/htcli/errors/base.py +141 -0
  75. src/htcli/errors/display.py +20 -0
  76. src/htcli/errors/handlers.py +710 -0
  77. src/htcli/main.py +343 -0
  78. src/htcli/models/__init__.py +21 -0
  79. src/htcli/models/enums/enum_types.py +35 -0
  80. src/htcli/models/errors.py +103 -0
  81. src/htcli/models/requests/__init__.py +197 -0
  82. src/htcli/models/requests/config.py +70 -0
  83. src/htcli/models/requests/consensus.py +19 -0
  84. src/htcli/models/requests/governance.py +38 -0
  85. src/htcli/models/requests/identity.py +51 -0
  86. src/htcli/models/requests/key.py +22 -0
  87. src/htcli/models/requests/node.py +91 -0
  88. src/htcli/models/requests/overwatch.py +64 -0
  89. src/htcli/models/requests/staking.py +580 -0
  90. src/htcli/models/requests/subnet.py +195 -0
  91. src/htcli/models/requests/validator.py +139 -0
  92. src/htcli/models/requests/wallet.py +118 -0
  93. src/htcli/models/responses/__init__.py +147 -0
  94. src/htcli/models/responses/base.py +18 -0
  95. src/htcli/models/responses/chain.py +39 -0
  96. src/htcli/models/responses/config.py +58 -0
  97. src/htcli/models/responses/identity.py +102 -0
  98. src/htcli/models/responses/overwatch.py +51 -0
  99. src/htcli/models/responses/staking.py +502 -0
  100. src/htcli/models/responses/subnet.py +856 -0
  101. src/htcli/models/responses/wallet.py +185 -0
  102. src/htcli/ui/__init__.py +87 -0
  103. src/htcli/ui/colors.py +309 -0
  104. src/htcli/ui/components/__init__.py +60 -0
  105. src/htcli/ui/components/panels.py +174 -0
  106. src/htcli/ui/components/progress.py +166 -0
  107. src/htcli/ui/components/spinners.py +92 -0
  108. src/htcli/ui/components/tables.py +809 -0
  109. src/htcli/ui/components/trees.py +721 -0
  110. src/htcli/ui/display.py +336 -0
  111. src/htcli/ui/prompts.py +870 -0
  112. src/htcli/utils/__init__.py +76 -0
  113. src/htcli/utils/blockchain/__init__.py +75 -0
  114. src/htcli/utils/blockchain/formatting.py +368 -0
  115. src/htcli/utils/blockchain/patches.py +286 -0
  116. src/htcli/utils/blockchain/peer_id.py +186 -0
  117. src/htcli/utils/blockchain/staking.py +448 -0
  118. src/htcli/utils/blockchain/type_registry.py +1373 -0
  119. src/htcli/utils/blockchain/validation.py +179 -0
  120. src/htcli/utils/cache.py +613 -0
  121. src/htcli/utils/constants.py +38 -0
  122. src/htcli/utils/legacy/__init__.py +12 -0
  123. src/htcli/utils/legacy/colors.py +311 -0
  124. src/htcli/utils/legacy/crypto.py +1176 -0
  125. src/htcli/utils/legacy/formatting.py +452 -0
  126. src/htcli/utils/legacy/interactive.py +306 -0
  127. src/htcli/utils/legacy/subnet_manifest.py +265 -0
  128. src/htcli/utils/legacy/validation.py +488 -0
  129. src/htcli/utils/logging.py +183 -0
  130. src/htcli/utils/network/__init__.py +20 -0
  131. src/htcli/utils/network/subnet.py +344 -0
  132. src/htcli/utils/prompts.py +27 -0
  133. src/htcli/utils/scale_codec.py +155 -0
  134. src/htcli/utils/validation/__init__.py +57 -0
  135. src/htcli/utils/validation/prompt_validators.py +267 -0
  136. src/htcli/utils/wallet/__init__.py +65 -0
  137. src/htcli/utils/wallet/auth.py +151 -0
  138. src/htcli/utils/wallet/core.py +1069 -0
  139. src/htcli/utils/wallet/crypto.py +1615 -0
  140. 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)