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,839 @@
1
+ """
2
+ Off-chain wallet management operations.
3
+ Thin client layer that delegates to utility functions.
4
+ """
5
+
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Callable, Iterable, Optional
9
+
10
+ from substrateinterface.keypair import Keypair
11
+
12
+ from src.htcli.utils.wallet.core import decrypt_data, encrypt_data
13
+ from src.htcli.utils.wallet.crypto import (
14
+ build_wallet_file_path,
15
+ public_key_to_evm_address,
16
+ resolve_wallet_file_path,
17
+ )
18
+
19
+ from ...utils.logging import get_logger
20
+
21
+ logger = get_logger(__name__)
22
+
23
+
24
+ class WalletManager:
25
+ """Manager for off-chain wallet operations - thin client layer."""
26
+
27
+ def __init__(self, wallet_dir: Optional[str] = None):
28
+ """Initialize WalletManager with wallet directory.
29
+
30
+ Args:
31
+ wallet_dir: Optional wallet directory path. If None, will read from config.
32
+ """
33
+ if wallet_dir:
34
+ self.wallet_dir = Path(wallet_dir).expanduser()
35
+ else:
36
+ # Fall back to config or default
37
+ try:
38
+ from ...utils.wallet.crypto import get_wallet_directory
39
+
40
+ self.wallet_dir = get_wallet_directory()
41
+ except Exception:
42
+ # Final fallback to default
43
+ self.wallet_dir = Path.home() / ".htcli" / "wallets"
44
+
45
+ def create_coldkey_wallet(
46
+ self, name: str, password: Optional[str] = None, mnemonic: Optional[str] = None
47
+ ) -> dict:
48
+ """Create a new wallet - delegates to utility functions."""
49
+ from ...utils.wallet.crypto import generate_coldkey_pair
50
+
51
+ try:
52
+ # Delegate to utility function
53
+ result = generate_coldkey_pair(
54
+ name=name, key_type="ecdsa", password=password
55
+ )
56
+
57
+ return {
58
+ "success": True,
59
+ "message": f"Wallet '{name}' created successfully",
60
+ "data": {
61
+ "name": result.name,
62
+ "ss58_address": result.ss58_address,
63
+ "evm_address": result.ss58_address, # Same for ECDSA
64
+ "public_key": result.public_key,
65
+ "mnemonic": result.mnemonic, # Return for user to backup
66
+ "is_encrypted": bool(password),
67
+ "key_type": result.key_type,
68
+ },
69
+ }
70
+ except Exception as e:
71
+ logger.error(f"Failed to create wallet: {str(e)}")
72
+ raise
73
+
74
+ def create_hotkey_wallet(
75
+ self,
76
+ name: str,
77
+ owner_address: str,
78
+ password: Optional[str] = None,
79
+ mnemonic: Optional[str] = None,
80
+ owner_coldkey_name: Optional[str] = None,
81
+ ) -> dict:
82
+ """Create a new hotkey wallet - delegates to utility functions."""
83
+ from ...utils.wallet.crypto import generate_hotkey_pair
84
+
85
+ try:
86
+ # Delegate to utility function
87
+ result = generate_hotkey_pair(
88
+ name=name,
89
+ owner_address=owner_address,
90
+ key_type="ecdsa",
91
+ password=password,
92
+ owner_coldkey_name=owner_coldkey_name,
93
+ )
94
+
95
+ return {
96
+ "success": True,
97
+ "message": f"Hotkey wallet '{name}' created successfully",
98
+ "data": {
99
+ "name": result.name,
100
+ "ss58_address": result.ss58_address,
101
+ "evm_address": result.ss58_address, # Same for ECDSA
102
+ "public_key": result.public_key,
103
+ "mnemonic": result.mnemonic, # Return for user to backup
104
+ "is_encrypted": bool(password),
105
+ "key_type": result.key_type,
106
+ "owner_address": result.owner_address,
107
+ "owner_coldkey_name": result.owner_coldkey_name
108
+ or owner_coldkey_name,
109
+ },
110
+ }
111
+ except Exception as e:
112
+ logger.error(f"Failed to create hotkey wallet: {str(e)}")
113
+ raise
114
+
115
+ def load_wallet(
116
+ self,
117
+ name: str,
118
+ password: str,
119
+ is_hotkey: Optional[bool] = None,
120
+ owner_address: Optional[str] = None,
121
+ ) -> dict:
122
+ """Load an existing wallet - delegates to utility functions.
123
+
124
+ Args:
125
+ name: Wallet name
126
+ password: Password for encrypted wallets
127
+ is_hotkey: Optional hint to specify if this is a hotkey (True) or coldkey (False)
128
+ owner_address: Optional coldkey address for hotkey disambiguation
129
+ """
130
+ from ...utils.wallet.crypto import load_keypair
131
+
132
+ try:
133
+ # Delegate to utility function with disambiguation parameters
134
+ keypair = load_keypair(
135
+ name, password, is_hotkey=is_hotkey, owner_address=owner_address
136
+ )
137
+
138
+ return {
139
+ "success": True,
140
+ "message": f"Wallet '{name}' loaded successfully",
141
+ "data": {
142
+ "name": name,
143
+ "address": keypair.ss58_address,
144
+ "public_key": keypair.public_key.hex(),
145
+ "keypair": keypair,
146
+ },
147
+ }
148
+ except Exception as e:
149
+ logger.error(f"Failed to load wallet: {str(e)}")
150
+ raise
151
+
152
+ def list_wallets(self) -> dict:
153
+ """List all available wallets - delegates to utility functions."""
154
+ from ...utils.wallet.crypto import list_keys
155
+
156
+ try:
157
+ # Delegate to utility function
158
+ wallets = list_keys()
159
+
160
+ return {
161
+ "success": True,
162
+ "message": f"Found {len(wallets)} wallet(s)",
163
+ "data": wallets,
164
+ }
165
+ except Exception as e:
166
+ logger.error(f"Failed to list wallets: {str(e)}")
167
+ raise
168
+
169
+ def delete_wallet(
170
+ self,
171
+ name: str,
172
+ is_hotkey: Optional[bool] = None,
173
+ owner_address: Optional[str] = None,
174
+ owner_coldkey_name: Optional[str] = None,
175
+ ) -> dict:
176
+ """Delete a wallet - delegates to utility functions.
177
+
178
+ Args:
179
+ name: Wallet name to delete
180
+ is_hotkey: Optional hint to specify if this is a hotkey (True) or coldkey (False)
181
+ owner_address: Optional coldkey address for hotkey disambiguation
182
+ """
183
+ from ...utils.wallet.crypto import delete_keypair
184
+
185
+ try:
186
+ # Delegate to utility function (confirmation handled by handler)
187
+ deleted = delete_keypair(
188
+ name,
189
+ is_hotkey=is_hotkey,
190
+ owner_address=owner_address,
191
+ owner_coldkey_name=owner_coldkey_name,
192
+ )
193
+
194
+ if not deleted:
195
+ raise FileNotFoundError(f"Wallet '{name}' not found")
196
+
197
+ return {
198
+ "success": True,
199
+ "message": f"Wallet '{name}' deleted successfully",
200
+ "data": {"name": name},
201
+ }
202
+ except Exception as e:
203
+ logger.error(f"Failed to delete wallet: {str(e)}")
204
+ raise
205
+
206
+ def get_wallet_info(
207
+ self,
208
+ name: str,
209
+ is_hotkey: Optional[bool] = None,
210
+ owner_address: Optional[str] = None,
211
+ owner_coldkey_name: Optional[str] = None,
212
+ ) -> dict:
213
+ """Get wallet information - delegates to utility functions.
214
+
215
+ Args:
216
+ name: Wallet name
217
+ is_hotkey: Optional hint to specify if this is a hotkey (True) or coldkey (False)
218
+ owner_address: Optional coldkey address for hotkey disambiguation
219
+ """
220
+ from ...utils.wallet.crypto import get_wallet_info_by_name
221
+
222
+ try:
223
+ # Delegate to utility function
224
+ wallet_info = get_wallet_info_by_name(
225
+ name,
226
+ is_hotkey=is_hotkey,
227
+ owner_address=owner_address,
228
+ owner_coldkey_name=owner_coldkey_name,
229
+ )
230
+
231
+ if not wallet_info:
232
+ raise ValueError(f"Wallet '{name}' not found")
233
+
234
+ return {
235
+ "success": True,
236
+ "message": f"Wallet '{name}' information retrieved",
237
+ "data": wallet_info,
238
+ }
239
+ except Exception as e:
240
+ logger.error(f"Failed to get wallet info: {str(e)}")
241
+ raise
242
+
243
+ def backup_wallet(self, name: str, backup_path: str, password: str) -> dict:
244
+ """Create a backup of a wallet."""
245
+ from ...utils.wallet.crypto import get_wallet_info_by_name
246
+
247
+ try:
248
+ # First get wallet info to handle ambiguity
249
+ wallet_info = get_wallet_info_by_name(name)
250
+ is_hotkey = wallet_info.get("is_hotkey", False)
251
+ owner_address = wallet_info.get("owner_address")
252
+
253
+ # Use wallet_dir from config
254
+ wallet_path = resolve_wallet_file_path(
255
+ name, is_hotkey=is_hotkey, owner_address=owner_address
256
+ )
257
+
258
+ # Load wallet to verify password
259
+ wallet_result = self.load_wallet(
260
+ name, password, is_hotkey=is_hotkey, owner_address=owner_address
261
+ )
262
+ if not wallet_result["success"]:
263
+ raise ValueError("Invalid password")
264
+
265
+ # Copy wallet file to backup location
266
+ backup_file = Path(backup_path)
267
+ backup_file.parent.mkdir(parents=True, exist_ok=True)
268
+
269
+ with open(wallet_path) as src, open(backup_file, "w") as dst:
270
+ dst.write(src.read())
271
+
272
+ return {
273
+ "success": True,
274
+ "message": f"Wallet '{name}' backed up successfully",
275
+ "data": {
276
+ "name": name,
277
+ "backup_path": str(backup_file),
278
+ "original_path": str(wallet_path),
279
+ },
280
+ }
281
+ except Exception as e:
282
+ logger.error(f"Failed to backup wallet: {str(e)}")
283
+ raise
284
+
285
+ def restore_wallet(self, backup_path: str, name: Optional[str] = None) -> dict:
286
+ """Restore a wallet from backup."""
287
+ try:
288
+ backup_file = Path(backup_path)
289
+
290
+ if not backup_file.exists():
291
+ raise ValueError(f"Backup file '{backup_path}' not found")
292
+
293
+ with open(backup_file) as f:
294
+ wallet_data = json.load(f)
295
+
296
+ # Use provided name or original name
297
+ wallet_name = name or wallet_data.get("name", backup_file.stem)
298
+ # Use wallet_dir from config
299
+ wallet_path = build_wallet_file_path(
300
+ wallet_name,
301
+ wallet_data.get("is_hotkey", False),
302
+ wallet_dir=self.wallet_dir,
303
+ owner_address=wallet_data.get("owner_address"),
304
+ )
305
+
306
+ if wallet_path.exists():
307
+ raise ValueError(f"Wallet '{wallet_name}' already exists")
308
+
309
+ # Update wallet data with new name if different
310
+ if name and name != wallet_data.get("name"):
311
+ wallet_data["name"] = name
312
+
313
+ # Save restored wallet
314
+ with open(wallet_path, "w") as f:
315
+ json.dump(wallet_data, f, indent=2)
316
+
317
+ return {
318
+ "success": True,
319
+ "message": f"Wallet '{wallet_name}' restored successfully",
320
+ "data": {
321
+ "name": wallet_name,
322
+ "address": wallet_data.get("address"),
323
+ "path": str(wallet_path),
324
+ "backup_path": backup_path,
325
+ },
326
+ }
327
+ except Exception as e:
328
+ logger.error(f"Failed to restore wallet: {str(e)}")
329
+ raise
330
+
331
+ def change_wallet_password(
332
+ self, name: str, old_password: str, new_password: str
333
+ ) -> dict:
334
+ """Change wallet password."""
335
+ from ...utils.wallet.crypto import get_wallet_info_by_name
336
+
337
+ try:
338
+ # First get wallet info to handle ambiguity
339
+ wallet_info = get_wallet_info_by_name(name)
340
+ is_hotkey = wallet_info.get("is_hotkey", False)
341
+ owner_address = wallet_info.get("owner_address")
342
+
343
+ # Load wallet with old password
344
+ wallet_result = self.load_wallet(
345
+ name, old_password, is_hotkey=is_hotkey, owner_address=owner_address
346
+ )
347
+ if not wallet_result["success"]:
348
+ raise ValueError("Invalid old password")
349
+
350
+ wallet_data = wallet_result["data"]["wallet_data"]
351
+
352
+ # Decrypt with old password and re-encrypt with new password
353
+ encrypted_mnemonic = wallet_data.get("encrypted_mnemonic")
354
+ mnemonic = decrypt_data(encrypted_mnemonic, old_password)
355
+ new_encrypted_mnemonic = encrypt_data(mnemonic, new_password)
356
+
357
+ # Update wallet data
358
+ wallet_data["encrypted_mnemonic"] = new_encrypted_mnemonic
359
+
360
+ # Save updated wallet
361
+ wallet_path = resolve_wallet_file_path(
362
+ name, is_hotkey=is_hotkey, owner_address=owner_address
363
+ )
364
+ with open(wallet_path, "w") as f:
365
+ json.dump(wallet_data, f, indent=2)
366
+
367
+ return {
368
+ "success": True,
369
+ "message": f"Password for wallet '{name}' changed successfully",
370
+ "data": {"name": name},
371
+ }
372
+ except Exception as e:
373
+ logger.error(f"Failed to change wallet password: {str(e)}")
374
+ raise
375
+
376
+ def export_mnemonic(self, name: str, password: str) -> dict:
377
+ """Export wallet mnemonic (use with caution)."""
378
+ from ...utils.wallet.crypto import get_wallet_info_by_name
379
+
380
+ try:
381
+ # First get wallet info to handle ambiguity
382
+ wallet_info = get_wallet_info_by_name(name)
383
+ is_hotkey = wallet_info.get("is_hotkey", False)
384
+ owner_address = wallet_info.get("owner_address")
385
+
386
+ wallet_result = self.load_wallet(
387
+ name, password, is_hotkey=is_hotkey, owner_address=owner_address
388
+ )
389
+ if not wallet_result["success"]:
390
+ raise ValueError("Invalid password")
391
+
392
+ wallet_data = wallet_result["data"]["wallet_data"]
393
+ encrypted_mnemonic = wallet_data.get("encrypted_mnemonic")
394
+ mnemonic = decrypt_data(encrypted_mnemonic, password)
395
+
396
+ return {
397
+ "success": True,
398
+ "message": f"Mnemonic for wallet '{name}' exported",
399
+ "data": {
400
+ "name": name,
401
+ "mnemonic": mnemonic,
402
+ "warning": "Keep this mnemonic secure and private!",
403
+ },
404
+ }
405
+ except Exception as e:
406
+ logger.error(f"Failed to export mnemonic: {str(e)}")
407
+ raise
408
+
409
+ def upgrade_wallet_layout(
410
+ self,
411
+ *,
412
+ dry_run: bool = False,
413
+ force: bool = False,
414
+ backup: bool = True,
415
+ wallets: Optional[Iterable[str]] = None,
416
+ log: Optional[Callable[[str], None]] = None,
417
+ ) -> dict:
418
+ """Upgrade the wallet directory to the hierarchical layout."""
419
+ from ...utils.wallet.migration import migrate_wallet_layout
420
+
421
+ try:
422
+ report = migrate_wallet_layout(
423
+ wallet_dir=self.wallet_dir,
424
+ dry_run=dry_run,
425
+ force=force,
426
+ backup=backup,
427
+ only_wallets=wallets,
428
+ log=log,
429
+ )
430
+ return {
431
+ "success": True,
432
+ "message": "Wallet layout upgrade completed",
433
+ "data": report.as_dict(),
434
+ }
435
+ except Exception as e:
436
+ logger.error(f"Failed to upgrade wallet layout: {str(e)}")
437
+ raise
438
+
439
+ def get_keypair(
440
+ self,
441
+ name: str,
442
+ password: str,
443
+ is_hotkey: Optional[bool] = None,
444
+ owner_address: Optional[str] = None,
445
+ ) -> Keypair:
446
+ """Get keypair for a wallet (for transaction signing).
447
+
448
+ Args:
449
+ name: Wallet name
450
+ password: Password for encrypted wallets (None for unencrypted)
451
+ is_hotkey: Optional hint to specify if this is a hotkey (True) or coldkey (False)
452
+ owner_address: Optional coldkey address for hotkey disambiguation
453
+ """
454
+ try:
455
+ wallet_result = self.load_wallet(
456
+ name, password, is_hotkey=is_hotkey, owner_address=owner_address
457
+ )
458
+ if not wallet_result["success"]:
459
+ raise ValueError("Invalid password")
460
+
461
+ return wallet_result["data"]["keypair"]
462
+ except Exception as e:
463
+ logger.error(f"Failed to get keypair: {str(e)}")
464
+ raise
465
+
466
+ def resolve_hotkey_address(
467
+ self,
468
+ hotkey_input: str,
469
+ owner_coldkey_name: Optional[str] = None,
470
+ owner_address: Optional[str] = None,
471
+ ) -> str:
472
+ """
473
+ Resolve hotkey input to a valid Bytes20 address.
474
+ Handles both direct addresses and wallet names.
475
+
476
+ Args:
477
+ hotkey_input: Either a Bytes20 address (0x...) or wallet name
478
+ owner_coldkey_name: Optional coldkey name for disambiguating hotkeys with the same name
479
+ owner_address: Optional coldkey address for disambiguating hotkeys with the same name
480
+
481
+ Returns:
482
+ str: Valid Bytes20 address
483
+
484
+ Raises:
485
+ ValueError: If hotkey cannot be resolved or is invalid format
486
+ """
487
+ hotkey_input = hotkey_input.strip()
488
+
489
+ # Check if it's already a valid Ethereum address
490
+ if self.validate_ethereum_address(hotkey_input):
491
+ return hotkey_input
492
+
493
+ # Check if it's a wallet name
494
+ try:
495
+ # Use get_wallet_info_by_name with owner context for proper disambiguation
496
+ from ...utils.wallet.crypto import get_wallet_info_by_name
497
+
498
+ wallet_info = get_wallet_info_by_name(
499
+ hotkey_input,
500
+ is_hotkey=True,
501
+ owner_address=owner_address,
502
+ owner_coldkey_name=owner_coldkey_name,
503
+ )
504
+
505
+ if wallet_info:
506
+ resolved_address = (
507
+ wallet_info.get("evm_address")
508
+ or wallet_info.get("address")
509
+ or wallet_info.get("ss58_address")
510
+ )
511
+ if resolved_address and self.validate_ethereum_address(
512
+ resolved_address
513
+ ):
514
+ return resolved_address
515
+
516
+ public_key_hex = wallet_info.get("public_key")
517
+ if public_key_hex:
518
+ try:
519
+ derived_address = public_key_to_evm_address(
520
+ bytes.fromhex(public_key_hex)
521
+ )
522
+ if self.validate_ethereum_address(derived_address):
523
+ return derived_address
524
+ except Exception:
525
+ pass
526
+
527
+ raise ValueError(
528
+ f"Wallet '{hotkey_input}' does not have a valid EVM address on disk. "
529
+ "Please regenerate or restore this wallet to continue."
530
+ )
531
+ else:
532
+ raise ValueError(f"Wallet '{hotkey_input}' not found")
533
+ except FileNotFoundError:
534
+ raise ValueError(f"Wallet '{hotkey_input}' not found")
535
+ except Exception as e:
536
+ raise ValueError(
537
+ f"Failed to resolve hotkey '{hotkey_input}': {str(e)}"
538
+ ) from e
539
+
540
+ def validate_ethereum_address(self, address: str) -> bool:
541
+ """Validate if address is a valid Ethereum/Bytes20 format."""
542
+ return (
543
+ address.startswith("0x")
544
+ and len(address) == 42
545
+ and all(c in "0123456789abcdefABCDEF" for c in address[2:])
546
+ )
547
+
548
+ # ============================================================================
549
+ # MIGRATION METHODS: Methods that wrap utils functions for proper architecture
550
+ # ============================================================================
551
+
552
+ def import_coldkey_from_private_key(
553
+ self,
554
+ name: str,
555
+ private_key: str,
556
+ key_type: str = "ecdsa",
557
+ password: Optional[str] = None,
558
+ ) -> dict:
559
+ """Import coldkey from private key - delegates to utility functions."""
560
+ from ...utils.wallet.crypto import import_keypair
561
+
562
+ try:
563
+ # Delegate to utility function
564
+ keypair_info = import_keypair(
565
+ name=name,
566
+ private_key=private_key,
567
+ key_type=key_type,
568
+ password=password,
569
+ )
570
+
571
+ return {
572
+ "success": True,
573
+ "message": f"Coldkey '{name}' imported from private key successfully",
574
+ "data": {
575
+ "name": keypair_info.name,
576
+ "ss58_address": keypair_info.ss58_address,
577
+ "public_key": keypair_info.public_key,
578
+ "key_type": keypair_info.key_type,
579
+ "mnemonic": None, # Private key import doesn't have mnemonic
580
+ "import_method": "private key",
581
+ },
582
+ }
583
+ except Exception as e:
584
+ logger.error(f"Failed to import coldkey from private key: {str(e)}")
585
+ raise
586
+
587
+ def import_coldkey_from_mnemonic(
588
+ self,
589
+ name: str,
590
+ mnemonic: str,
591
+ key_type: str = "ecdsa",
592
+ password: Optional[str] = None,
593
+ ) -> dict:
594
+ """Import coldkey from mnemonic - delegates to utility functions."""
595
+ from ...utils.wallet.crypto import import_keypair_from_mnemonic
596
+
597
+ try:
598
+ # Delegate to utility function
599
+ keypair_info = import_keypair_from_mnemonic(
600
+ name=name,
601
+ mnemonic=mnemonic,
602
+ key_type=key_type,
603
+ password=password,
604
+ )
605
+
606
+ return {
607
+ "success": True,
608
+ "message": f"Coldkey '{name}' imported from mnemonic successfully",
609
+ "data": {
610
+ "name": keypair_info.name,
611
+ "ss58_address": keypair_info.ss58_address,
612
+ "public_key": keypair_info.public_key,
613
+ "key_type": keypair_info.key_type,
614
+ "mnemonic": mnemonic, # Include the mnemonic used for restoration
615
+ "import_method": "mnemonic",
616
+ },
617
+ }
618
+ except Exception as e:
619
+ logger.error(f"Failed to import coldkey from mnemonic: {str(e)}")
620
+ raise
621
+
622
+ def import_hotkey_from_private_key(
623
+ self,
624
+ name: str,
625
+ private_key: str,
626
+ owner_address: str,
627
+ key_type: str = "ecdsa",
628
+ password: Optional[str] = None,
629
+ ) -> dict:
630
+ """Import hotkey from private key - delegates to utility functions."""
631
+ from ...utils.wallet.crypto import import_hotkey_from_private_key
632
+
633
+ try:
634
+ # Delegate to utility function
635
+ keypair_info = import_hotkey_from_private_key(
636
+ name=name,
637
+ private_key=private_key,
638
+ owner_address=owner_address,
639
+ key_type=key_type,
640
+ password=password,
641
+ )
642
+
643
+ return {
644
+ "success": True,
645
+ "message": f"Hotkey '{name}' imported from private key successfully",
646
+ "data": {
647
+ "name": keypair_info.name,
648
+ "ss58_address": keypair_info.ss58_address,
649
+ "public_key": keypair_info.public_key,
650
+ "key_type": keypair_info.key_type,
651
+ "owner_address": keypair_info.owner_address,
652
+ "mnemonic": None, # Private key import doesn't have mnemonic
653
+ "import_method": "private key",
654
+ },
655
+ }
656
+ except Exception as e:
657
+ logger.error(f"Failed to import hotkey from private key: {str(e)}")
658
+ raise
659
+
660
+ def import_hotkey_from_mnemonic(
661
+ self,
662
+ name: str,
663
+ mnemonic: str,
664
+ owner_address: str,
665
+ key_type: str = "ecdsa",
666
+ password: Optional[str] = None,
667
+ ) -> dict:
668
+ """Import hotkey from mnemonic - delegates to utility functions."""
669
+ from ...utils.wallet.crypto import import_hotkey_from_mnemonic
670
+
671
+ try:
672
+ # Delegate to utility function
673
+ keypair_info = import_hotkey_from_mnemonic(
674
+ name=name,
675
+ mnemonic=mnemonic,
676
+ owner_address=owner_address,
677
+ key_type=key_type,
678
+ password=password,
679
+ )
680
+
681
+ return {
682
+ "success": True,
683
+ "message": f"Hotkey '{name}' imported from mnemonic successfully",
684
+ "data": {
685
+ "name": keypair_info.name,
686
+ "ss58_address": keypair_info.ss58_address,
687
+ "public_key": keypair_info.public_key,
688
+ "key_type": keypair_info.key_type,
689
+ "owner_address": keypair_info.owner_address,
690
+ "mnemonic": mnemonic, # Include the mnemonic used for restoration
691
+ "import_method": "mnemonic",
692
+ },
693
+ }
694
+ except Exception as e:
695
+ logger.error(f"Failed to import hotkey from mnemonic: {str(e)}")
696
+ raise
697
+
698
+ def import_preseeded_wallet(
699
+ self, preseeded_name: str, wallet_name: str, password: Optional[str] = None
700
+ ) -> dict:
701
+ """Import preseeded wallet - delegates to utility functions."""
702
+ from ...utils.wallet.crypto import import_preseeded_wallet
703
+
704
+ try:
705
+ # Delegate to utility function
706
+ keypair_info = import_preseeded_wallet(
707
+ preseeded_name=preseeded_name,
708
+ wallet_name=wallet_name,
709
+ password=password,
710
+ )
711
+
712
+ return {
713
+ "success": True,
714
+ "message": f"Preseeded wallet '{preseeded_name}' imported as '{wallet_name}' successfully",
715
+ "data": {
716
+ "name": keypair_info.name,
717
+ "ss58_address": keypair_info.ss58_address,
718
+ "public_key": keypair_info.public_key,
719
+ "key_type": keypair_info.key_type,
720
+ "mnemonic": None, # Preseeded wallets use private keys, no mnemonic
721
+ "preseeded_name": preseeded_name,
722
+ },
723
+ }
724
+ except Exception as e:
725
+ logger.error(f"Failed to import preseeded wallet: {str(e)}")
726
+ raise
727
+
728
+ def delete_coldkey_and_hotkeys(self, name: str) -> dict:
729
+ """Delete coldkey and associated hotkeys - delegates to utility functions."""
730
+ from ...utils.wallet.crypto import delete_coldkey_and_hotkeys
731
+
732
+ try:
733
+ # Delegate to utility function (confirmation handled by handler)
734
+ result = delete_coldkey_and_hotkeys(name)
735
+
736
+ return {
737
+ "success": True,
738
+ "message": f"Coldkey '{name}' and associated hotkeys deleted successfully",
739
+ "data": result,
740
+ }
741
+ except Exception as e:
742
+ logger.error(f"Failed to delete coldkey and hotkeys: {str(e)}")
743
+ raise
744
+
745
+ def update_coldkey(
746
+ self,
747
+ name: str,
748
+ new_name: Optional[str] = None,
749
+ new_password: Optional[str] = None,
750
+ remove_password: bool = False,
751
+ current_password: Optional[str] = None,
752
+ ) -> dict:
753
+ """Update coldkey - delegates to utility functions."""
754
+ from ...utils.wallet.crypto import update_coldkey
755
+
756
+ try:
757
+ # Delegate to utility function
758
+ update_data = update_coldkey(
759
+ current_name=name,
760
+ new_name=new_name,
761
+ new_password=new_password,
762
+ remove_password=remove_password,
763
+ current_password=current_password,
764
+ )
765
+
766
+ return {
767
+ "success": True,
768
+ "message": f"Coldkey '{name}' updated successfully",
769
+ "data": update_data,
770
+ }
771
+ except Exception as e:
772
+ # Let the error bubble up to handler for beautiful error display
773
+ raise
774
+
775
+ def update_hotkey(
776
+ self,
777
+ name: str,
778
+ new_name: Optional[str] = None,
779
+ new_password: Optional[str] = None,
780
+ remove_password: bool = False,
781
+ new_owner: Optional[str] = None,
782
+ current_password: Optional[str] = None,
783
+ owner_address: Optional[str] = None,
784
+ ) -> dict:
785
+ """Update hotkey - delegates to utility functions."""
786
+ from ...utils.wallet.crypto import update_hotkey
787
+
788
+ try:
789
+ # Delegate to utility function
790
+ # update_hotkey expects new_owner_name as a wallet name, not address
791
+ update_data = update_hotkey(
792
+ current_name=name,
793
+ new_name=new_name,
794
+ new_password=new_password,
795
+ remove_password=remove_password,
796
+ new_owner_name=new_owner, # This will be the wallet name
797
+ current_password=current_password,
798
+ owner_address=owner_address, # Pass owner address for disambiguation
799
+ )
800
+
801
+ return {
802
+ "success": True,
803
+ "message": f"Hotkey '{name}' updated successfully",
804
+ "data": update_data,
805
+ }
806
+ except Exception as e:
807
+ # Let the error bubble up to handler for beautiful error display
808
+ raise
809
+
810
+ def save_coldkey(
811
+ self, name: str, keypair: Keypair, password: Optional[str] = None
812
+ ) -> dict:
813
+ """Save a coldkey to disk (used for rotation/restoration)."""
814
+ from ...utils.wallet.crypto import save_coldkey
815
+
816
+ try:
817
+ save_coldkey(name, keypair, password)
818
+ return {"success": True, "message": f"Coldkey '{name}' saved successfully"}
819
+ except Exception as e:
820
+ logger.error(f"Failed to save coldkey: {str(e)}")
821
+ raise
822
+
823
+ def save_hotkey(
824
+ self,
825
+ name: str,
826
+ keypair: Keypair,
827
+ owner_address: str,
828
+ password: Optional[str] = None,
829
+ owner_coldkey_name: Optional[str] = None,
830
+ ) -> dict:
831
+ """Save a hotkey to disk (used for rotation/restoration)."""
832
+ from ...utils.wallet.crypto import save_hotkey
833
+
834
+ try:
835
+ save_hotkey(name, keypair, owner_address, password, owner_coldkey_name)
836
+ return {"success": True, "message": f"Hotkey '{name}' saved successfully"}
837
+ except Exception as e:
838
+ logger.error(f"Failed to save hotkey: {str(e)}")
839
+ raise