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,385 @@
1
+ """
2
+ Off-chain backup management operations.
3
+ Handles backup and restoration of wallets, configurations, and local state.
4
+ """
5
+
6
+ import json
7
+ import shutil
8
+ import tarfile
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import Optional
12
+
13
+ from ...utils.logging import get_logger
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ class BackupManager:
19
+ """Manager for off-chain backup operations."""
20
+
21
+ def __init__(self, backup_dir: Optional[str] = None):
22
+ self.backup_dir = (
23
+ Path(backup_dir) if backup_dir else Path.home() / ".htcli" / "backups"
24
+ )
25
+ self.backup_dir.mkdir(parents=True, exist_ok=True)
26
+ self.htcli_dir = Path.home() / ".htcli"
27
+
28
+ def _validate_member_path(self, destination: Path, member_name: str) -> Path:
29
+ """Resolve a tar member path and ensure it stays under destination."""
30
+ target_path = (destination / member_name).resolve()
31
+ try:
32
+ target_path.relative_to(destination)
33
+ except ValueError as exc:
34
+ raise ValueError(f"Unsafe backup member path: {member_name}") from exc
35
+ return target_path
36
+
37
+ def _safe_extract_tar(self, tar: tarfile.TarFile, destination: Path) -> None:
38
+ """Extract regular files and directories without trusting archive paths."""
39
+ destination = destination.resolve()
40
+ destination.mkdir(parents=True, exist_ok=True)
41
+
42
+ for member in tar.getmembers():
43
+ target_path = self._validate_member_path(destination, member.name)
44
+
45
+ if member.isdir():
46
+ target_path.mkdir(parents=True, exist_ok=True)
47
+ continue
48
+
49
+ if not member.isfile():
50
+ raise ValueError(f"Unsupported backup member type: {member.name}")
51
+
52
+ source = tar.extractfile(member)
53
+ if source is None:
54
+ raise ValueError(f"Unable to read backup member: {member.name}")
55
+
56
+ target_path.parent.mkdir(parents=True, exist_ok=True)
57
+ with source, open(target_path, "wb") as target:
58
+ shutil.copyfileobj(source, target)
59
+
60
+ def create_full_backup(self, backup_name: Optional[str] = None) -> dict:
61
+ """Create a full backup of all HTCLI data."""
62
+ try:
63
+ if backup_name is None:
64
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
65
+ backup_name = f"htcli_backup_{timestamp}"
66
+
67
+ backup_file = self.backup_dir / f"{backup_name}.tar.gz"
68
+
69
+ # Create backup archive
70
+ with tarfile.open(backup_file, "w:gz") as tar:
71
+ if self.htcli_dir.exists():
72
+ # Add all files except existing backups to avoid recursion
73
+ for item in self.htcli_dir.iterdir():
74
+ if item.name != "backups":
75
+ tar.add(item, arcname=item.name)
76
+
77
+ # Create backup manifest
78
+ manifest = self._create_backup_manifest(backup_name)
79
+ manifest_file = self.backup_dir / f"{backup_name}_manifest.json"
80
+
81
+ with open(manifest_file, "w") as f:
82
+ json.dump(manifest, f, indent=2)
83
+
84
+ return {
85
+ "success": True,
86
+ "message": "Full backup created successfully",
87
+ "data": {
88
+ "backup_name": backup_name,
89
+ "backup_file": str(backup_file),
90
+ "manifest_file": str(manifest_file),
91
+ "size": backup_file.stat().st_size,
92
+ "manifest": manifest,
93
+ },
94
+ }
95
+ except Exception as e:
96
+ logger.error(f"Failed to create full backup: {str(e)}")
97
+ raise
98
+
99
+ def restore_full_backup(self, backup_name: str, confirm: bool = False) -> dict:
100
+ """Restore a full backup (replaces current data)."""
101
+ try:
102
+ if not confirm:
103
+ raise ValueError("Full restore requires explicit confirmation")
104
+
105
+ backup_file = self.backup_dir / f"{backup_name}.tar.gz"
106
+ manifest_file = self.backup_dir / f"{backup_name}_manifest.json"
107
+
108
+ if not backup_file.exists():
109
+ raise ValueError(f"Backup file '{backup_name}.tar.gz' not found")
110
+
111
+ # Load manifest
112
+ manifest = {}
113
+ if manifest_file.exists():
114
+ with open(manifest_file) as f:
115
+ manifest = json.load(f)
116
+
117
+ # Create backup of current state before restore
118
+ current_backup_result = self.create_full_backup(
119
+ f"pre_restore_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
120
+ )
121
+
122
+ # Clear current HTCLI directory (except backups)
123
+ if self.htcli_dir.exists():
124
+ for item in self.htcli_dir.iterdir():
125
+ if item.name != "backups":
126
+ if item.is_dir():
127
+ shutil.rmtree(item)
128
+ else:
129
+ item.unlink()
130
+
131
+ # Extract backup
132
+ with tarfile.open(backup_file, "r:gz") as tar:
133
+ self._safe_extract_tar(tar, self.htcli_dir)
134
+
135
+ return {
136
+ "success": True,
137
+ "message": f"Full backup '{backup_name}' restored successfully",
138
+ "data": {
139
+ "backup_name": backup_name,
140
+ "manifest": manifest,
141
+ "pre_restore_backup": current_backup_result["data"]["backup_name"],
142
+ },
143
+ }
144
+ except Exception as e:
145
+ logger.error(f"Failed to restore full backup: {str(e)}")
146
+ raise
147
+
148
+ def backup_wallets(self, backup_name: Optional[str] = None) -> dict:
149
+ """Create a backup of all wallets."""
150
+ try:
151
+ if backup_name is None:
152
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
153
+ backup_name = f"wallets_backup_{timestamp}"
154
+
155
+ wallets_dir = self.htcli_dir / "wallets"
156
+ backup_file = self.backup_dir / f"{backup_name}.tar.gz"
157
+
158
+ if not wallets_dir.exists():
159
+ return {
160
+ "success": False,
161
+ "message": "No wallets directory found",
162
+ "data": None,
163
+ }
164
+
165
+ # Create wallets backup
166
+ with tarfile.open(backup_file, "w:gz") as tar:
167
+ tar.add(wallets_dir, arcname="wallets")
168
+
169
+ # Count wallet files
170
+ wallet_count = len(list(wallets_dir.glob("*.json")))
171
+
172
+ return {
173
+ "success": True,
174
+ "message": "Wallets backup created successfully",
175
+ "data": {
176
+ "backup_name": backup_name,
177
+ "backup_file": str(backup_file),
178
+ "wallet_count": wallet_count,
179
+ "size": backup_file.stat().st_size,
180
+ },
181
+ }
182
+ except Exception as e:
183
+ logger.error(f"Failed to backup wallets: {str(e)}")
184
+ raise
185
+
186
+ def restore_wallets(self, backup_name: str, confirm: bool = False) -> dict:
187
+ """Restore wallets from backup."""
188
+ try:
189
+ if not confirm:
190
+ raise ValueError("Wallet restore requires explicit confirmation")
191
+
192
+ backup_file = self.backup_dir / f"{backup_name}.tar.gz"
193
+
194
+ if not backup_file.exists():
195
+ raise ValueError(f"Backup file '{backup_name}.tar.gz' not found")
196
+
197
+ wallets_dir = self.htcli_dir / "wallets"
198
+
199
+ # Create backup of current wallets if they exist
200
+ current_backup = None
201
+ if wallets_dir.exists() and any(wallets_dir.glob("*.json")):
202
+ current_backup_result = self.backup_wallets(
203
+ f"pre_restore_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
204
+ )
205
+ current_backup = current_backup_result["data"]["backup_name"]
206
+
207
+ # Clear current wallets
208
+ if wallets_dir.exists():
209
+ shutil.rmtree(wallets_dir)
210
+
211
+ # Extract backup
212
+ with tarfile.open(backup_file, "r:gz") as tar:
213
+ self._safe_extract_tar(tar, self.htcli_dir)
214
+
215
+ # Count restored wallets
216
+ wallet_count = (
217
+ len(list(wallets_dir.glob("*.json"))) if wallets_dir.exists() else 0
218
+ )
219
+
220
+ return {
221
+ "success": True,
222
+ "message": f"Wallets restored from backup '{backup_name}' successfully",
223
+ "data": {
224
+ "backup_name": backup_name,
225
+ "wallet_count": wallet_count,
226
+ "pre_restore_backup": current_backup,
227
+ },
228
+ }
229
+ except Exception as e:
230
+ logger.error(f"Failed to restore wallets: {str(e)}")
231
+ raise
232
+
233
+ def list_backups(self) -> dict:
234
+ """List all available backups."""
235
+ try:
236
+ backup_files = list(self.backup_dir.glob("*.tar.gz"))
237
+ backups = []
238
+
239
+ for backup_file in backup_files:
240
+ try:
241
+ backup_name = backup_file.stem
242
+ manifest_file = self.backup_dir / f"{backup_name}_manifest.json"
243
+
244
+ backup_info = {
245
+ "name": backup_name,
246
+ "file": str(backup_file),
247
+ "size": backup_file.stat().st_size,
248
+ "created_at": datetime.fromtimestamp(
249
+ backup_file.stat().st_ctime
250
+ ).isoformat(),
251
+ "manifest": None,
252
+ }
253
+
254
+ # Load manifest if exists
255
+ if manifest_file.exists():
256
+ with open(manifest_file) as f:
257
+ backup_info["manifest"] = json.load(f)
258
+
259
+ backups.append(backup_info)
260
+ except Exception as e:
261
+ logger.warning(f"Failed to read backup info for {backup_file}: {e}")
262
+ continue
263
+
264
+ # Sort by creation time (newest first)
265
+ backups.sort(key=lambda x: x["created_at"], reverse=True)
266
+
267
+ return {
268
+ "success": True,
269
+ "message": f"Found {len(backups)} backup(s)",
270
+ "data": backups,
271
+ }
272
+ except Exception as e:
273
+ logger.error(f"Failed to list backups: {str(e)}")
274
+ raise
275
+
276
+ def delete_backup(self, backup_name: str, confirm: bool = False) -> dict:
277
+ """Delete a backup."""
278
+ try:
279
+ if not confirm:
280
+ raise ValueError("Backup deletion requires explicit confirmation")
281
+
282
+ backup_file = self.backup_dir / f"{backup_name}.tar.gz"
283
+ manifest_file = self.backup_dir / f"{backup_name}_manifest.json"
284
+
285
+ if not backup_file.exists():
286
+ raise ValueError(f"Backup '{backup_name}' not found")
287
+
288
+ # Delete files
289
+ backup_file.unlink()
290
+ if manifest_file.exists():
291
+ manifest_file.unlink()
292
+
293
+ return {
294
+ "success": True,
295
+ "message": f"Backup '{backup_name}' deleted successfully",
296
+ "data": {"backup_name": backup_name},
297
+ }
298
+ except Exception as e:
299
+ logger.error(f"Failed to delete backup: {str(e)}")
300
+ raise
301
+
302
+ def cleanup_old_backups(self, keep_count: int = 10, confirm: bool = False) -> dict:
303
+ """Clean up old backups, keeping only the most recent ones."""
304
+ try:
305
+ if not confirm:
306
+ raise ValueError("Backup cleanup requires explicit confirmation")
307
+
308
+ backups_result = self.list_backups()
309
+ backups = backups_result["data"]
310
+
311
+ if len(backups) <= keep_count:
312
+ return {
313
+ "success": True,
314
+ "message": f"No cleanup needed. Current backup count: {len(backups)}",
315
+ "data": {
316
+ "deleted_count": 0,
317
+ "remaining_count": len(backups),
318
+ },
319
+ }
320
+
321
+ # Delete oldest backups
322
+ backups_to_delete = backups[keep_count:]
323
+ deleted_count = 0
324
+
325
+ for backup in backups_to_delete:
326
+ try:
327
+ self.delete_backup(backup["name"], confirm=True)
328
+ deleted_count += 1
329
+ except Exception as e:
330
+ logger.warning(f"Failed to delete backup {backup['name']}: {e}")
331
+
332
+ return {
333
+ "success": True,
334
+ "message": f"Cleanup completed. Deleted {deleted_count} old backup(s)",
335
+ "data": {
336
+ "deleted_count": deleted_count,
337
+ "remaining_count": len(backups) - deleted_count,
338
+ },
339
+ }
340
+ except Exception as e:
341
+ logger.error(f"Failed to cleanup backups: {str(e)}")
342
+ raise
343
+
344
+ def _create_backup_manifest(self, backup_name: str) -> dict:
345
+ """Create a manifest describing the backup contents."""
346
+ try:
347
+ manifest = {
348
+ "backup_name": backup_name,
349
+ "created_at": datetime.now().isoformat(),
350
+ "version": "1.0",
351
+ "contents": {"wallets": [], "config": None, "other_files": []},
352
+ }
353
+
354
+ # List wallets
355
+ wallets_dir = self.htcli_dir / "wallets"
356
+ if wallets_dir.exists():
357
+ for wallet_file in wallets_dir.glob("*.json"):
358
+ try:
359
+ with open(wallet_file) as f:
360
+ wallet_data = json.load(f)
361
+ manifest["contents"]["wallets"].append(
362
+ {
363
+ "name": wallet_data.get("name", wallet_file.stem),
364
+ "address": wallet_data.get("address"),
365
+ "file": wallet_file.name,
366
+ }
367
+ )
368
+ except Exception as e:
369
+ logger.warning(f"Failed to read wallet {wallet_file}: {e}")
370
+
371
+ # Check config
372
+ config_file = self.htcli_dir / "config.yaml"
373
+ if config_file.exists():
374
+ manifest["contents"]["config"] = "config.yaml"
375
+
376
+ # List other files
377
+ if self.htcli_dir.exists():
378
+ for item in self.htcli_dir.iterdir():
379
+ if item.name not in ["wallets", "config.yaml", "backups"]:
380
+ manifest["contents"]["other_files"].append(item.name)
381
+
382
+ return manifest
383
+ except Exception as e:
384
+ logger.error(f"Failed to create backup manifest: {str(e)}")
385
+ return {"error": str(e)}