htcli 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- htcli-1.1.0.dist-info/METADATA +509 -0
- htcli-1.1.0.dist-info/RECORD +140 -0
- htcli-1.1.0.dist-info/WHEEL +4 -0
- htcli-1.1.0.dist-info/entry_points.txt +2 -0
- htcli-1.1.0.dist-info/licenses/LICENSE +21 -0
- src/__init__.py +0 -0
- src/htcli/__init__.py +5 -0
- src/htcli/client/__init__.py +338 -0
- src/htcli/client/extrinsics/__init__.py +26 -0
- src/htcli/client/extrinsics/base.py +487 -0
- src/htcli/client/extrinsics/consensus.py +79 -0
- src/htcli/client/extrinsics/governance.py +714 -0
- src/htcli/client/extrinsics/identity.py +490 -0
- src/htcli/client/extrinsics/node.py +1054 -0
- src/htcli/client/extrinsics/overwatch.py +401 -0
- src/htcli/client/extrinsics/staking.py +1504 -0
- src/htcli/client/extrinsics/subnet.py +2218 -0
- src/htcli/client/extrinsics/validator.py +203 -0
- src/htcli/client/extrinsics/wallet.py +323 -0
- src/htcli/client/offchain/__init__.py +10 -0
- src/htcli/client/offchain/backup.py +385 -0
- src/htcli/client/offchain/config.py +541 -0
- src/htcli/client/offchain/wallet.py +839 -0
- src/htcli/client/rpc/__init__.py +20 -0
- src/htcli/client/rpc/chain.py +568 -0
- src/htcli/client/rpc/node.py +783 -0
- src/htcli/client/rpc/overwatch.py +680 -0
- src/htcli/client/rpc/staking.py +216 -0
- src/htcli/client/rpc/subnet.py +2104 -0
- src/htcli/client/rpc/wallet.py +912 -0
- src/htcli/commands/__init__.py +31 -0
- src/htcli/commands/chain/__init__.py +66 -0
- src/htcli/commands/chain/display.py +204 -0
- src/htcli/commands/chain/handlers.py +260 -0
- src/htcli/commands/config/__init__.py +158 -0
- src/htcli/commands/config/display.py +353 -0
- src/htcli/commands/config/handlers.py +347 -0
- src/htcli/commands/config/prompts.py +357 -0
- src/htcli/commands/consensus/__init__.py +61 -0
- src/htcli/commands/consensus/handlers.py +100 -0
- src/htcli/commands/governance/__init__.py +49 -0
- src/htcli/commands/governance/handlers.py +81 -0
- src/htcli/commands/node/__init__.py +304 -0
- src/htcli/commands/node/display.py +749 -0
- src/htcli/commands/node/error_handling.py +470 -0
- src/htcli/commands/node/handlers.py +844 -0
- src/htcli/commands/node/prompts.py +346 -0
- src/htcli/commands/overwatch/__init__.py +219 -0
- src/htcli/commands/overwatch/display.py +396 -0
- src/htcli/commands/overwatch/error_handling.py +276 -0
- src/htcli/commands/overwatch/handlers.py +443 -0
- src/htcli/commands/overwatch/prompts.py +359 -0
- src/htcli/commands/stake/__init__.py +736 -0
- src/htcli/commands/stake/display.py +1103 -0
- src/htcli/commands/stake/error_handling.py +425 -0
- src/htcli/commands/stake/handlers.py +1902 -0
- src/htcli/commands/stake/prompts.py +1080 -0
- src/htcli/commands/subnet/__init__.py +639 -0
- src/htcli/commands/subnet/display.py +801 -0
- src/htcli/commands/subnet/error_handling.py +524 -0
- src/htcli/commands/subnet/handlers.py +2855 -0
- src/htcli/commands/subnet/prompts.py +1225 -0
- src/htcli/commands/validator/__init__.py +192 -0
- src/htcli/commands/validator/display.py +54 -0
- src/htcli/commands/validator/handlers.py +340 -0
- src/htcli/commands/wallet/__init__.py +546 -0
- src/htcli/commands/wallet/display.py +806 -0
- src/htcli/commands/wallet/error_handling.py +210 -0
- src/htcli/commands/wallet/handlers.py +3040 -0
- src/htcli/commands/wallet/prompts.py +1518 -0
- src/htcli/config.py +184 -0
- src/htcli/dependencies.py +186 -0
- src/htcli/errors/__init__.py +63 -0
- src/htcli/errors/base.py +141 -0
- src/htcli/errors/display.py +20 -0
- src/htcli/errors/handlers.py +710 -0
- src/htcli/main.py +343 -0
- src/htcli/models/__init__.py +21 -0
- src/htcli/models/enums/enum_types.py +35 -0
- src/htcli/models/errors.py +103 -0
- src/htcli/models/requests/__init__.py +197 -0
- src/htcli/models/requests/config.py +70 -0
- src/htcli/models/requests/consensus.py +19 -0
- src/htcli/models/requests/governance.py +38 -0
- src/htcli/models/requests/identity.py +51 -0
- src/htcli/models/requests/key.py +22 -0
- src/htcli/models/requests/node.py +91 -0
- src/htcli/models/requests/overwatch.py +64 -0
- src/htcli/models/requests/staking.py +580 -0
- src/htcli/models/requests/subnet.py +195 -0
- src/htcli/models/requests/validator.py +139 -0
- src/htcli/models/requests/wallet.py +118 -0
- src/htcli/models/responses/__init__.py +147 -0
- src/htcli/models/responses/base.py +18 -0
- src/htcli/models/responses/chain.py +39 -0
- src/htcli/models/responses/config.py +58 -0
- src/htcli/models/responses/identity.py +102 -0
- src/htcli/models/responses/overwatch.py +51 -0
- src/htcli/models/responses/staking.py +502 -0
- src/htcli/models/responses/subnet.py +856 -0
- src/htcli/models/responses/wallet.py +185 -0
- src/htcli/ui/__init__.py +87 -0
- src/htcli/ui/colors.py +309 -0
- src/htcli/ui/components/__init__.py +60 -0
- src/htcli/ui/components/panels.py +174 -0
- src/htcli/ui/components/progress.py +166 -0
- src/htcli/ui/components/spinners.py +92 -0
- src/htcli/ui/components/tables.py +809 -0
- src/htcli/ui/components/trees.py +721 -0
- src/htcli/ui/display.py +336 -0
- src/htcli/ui/prompts.py +870 -0
- src/htcli/utils/__init__.py +76 -0
- src/htcli/utils/blockchain/__init__.py +75 -0
- src/htcli/utils/blockchain/formatting.py +368 -0
- src/htcli/utils/blockchain/patches.py +286 -0
- src/htcli/utils/blockchain/peer_id.py +186 -0
- src/htcli/utils/blockchain/staking.py +448 -0
- src/htcli/utils/blockchain/type_registry.py +1373 -0
- src/htcli/utils/blockchain/validation.py +179 -0
- src/htcli/utils/cache.py +613 -0
- src/htcli/utils/constants.py +38 -0
- src/htcli/utils/legacy/__init__.py +12 -0
- src/htcli/utils/legacy/colors.py +311 -0
- src/htcli/utils/legacy/crypto.py +1176 -0
- src/htcli/utils/legacy/formatting.py +452 -0
- src/htcli/utils/legacy/interactive.py +306 -0
- src/htcli/utils/legacy/subnet_manifest.py +265 -0
- src/htcli/utils/legacy/validation.py +488 -0
- src/htcli/utils/logging.py +183 -0
- src/htcli/utils/network/__init__.py +20 -0
- src/htcli/utils/network/subnet.py +344 -0
- src/htcli/utils/prompts.py +27 -0
- src/htcli/utils/scale_codec.py +155 -0
- src/htcli/utils/validation/__init__.py +57 -0
- src/htcli/utils/validation/prompt_validators.py +267 -0
- src/htcli/utils/wallet/__init__.py +65 -0
- src/htcli/utils/wallet/auth.py +151 -0
- src/htcli/utils/wallet/core.py +1069 -0
- src/htcli/utils/wallet/crypto.py +1615 -0
- src/htcli/utils/wallet/migration.py +159 -0
|
@@ -0,0 +1,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
|