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,344 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Subnet management and registry utilities.
|
|
3
|
+
Handles subnet manifest operations and network-specific functionality.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from time import time
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from ...utils.logging import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_subnet_registry_path() -> Path:
|
|
17
|
+
"""Get the path to the subnet registry file."""
|
|
18
|
+
home = Path.home()
|
|
19
|
+
registry_dir = home / ".htcli" / "registry"
|
|
20
|
+
registry_dir.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
return registry_dir / "subnets.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_subnet_registry() -> dict[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Load the subnet registry from file.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Dictionary containing subnet registry data
|
|
30
|
+
"""
|
|
31
|
+
registry_path = get_subnet_registry_path()
|
|
32
|
+
|
|
33
|
+
if not registry_path.exists():
|
|
34
|
+
return {"subnets": {}, "metadata": {"version": "1.0"}}
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
with open(registry_path) as f:
|
|
38
|
+
return json.load(f)
|
|
39
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
40
|
+
logger.error(f"Failed to load subnet registry: {e}")
|
|
41
|
+
return {"subnets": {}, "metadata": {"version": "1.0"}}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def save_subnet_registry(registry_data: dict[str, Any]) -> bool:
|
|
45
|
+
"""
|
|
46
|
+
Save the subnet registry to file.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
registry_data: Registry data to save
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
True if saved successfully, False otherwise
|
|
53
|
+
"""
|
|
54
|
+
registry_path = get_subnet_registry_path()
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
with open(registry_path, "w") as f:
|
|
58
|
+
json.dump(registry_data, f, indent=2)
|
|
59
|
+
return True
|
|
60
|
+
except OSError as e:
|
|
61
|
+
logger.error(f"Failed to save subnet registry: {e}")
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def add_subnet_to_registry(
|
|
66
|
+
subnet_id: int, subnet_data: dict[str, Any], owner_address: Optional[str] = None
|
|
67
|
+
) -> bool:
|
|
68
|
+
"""
|
|
69
|
+
Add a subnet to the local registry.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
subnet_id: Subnet ID
|
|
73
|
+
subnet_data: Subnet information
|
|
74
|
+
owner_address: Optional owner address
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
True if added successfully, False otherwise
|
|
78
|
+
"""
|
|
79
|
+
try:
|
|
80
|
+
registry = load_subnet_registry()
|
|
81
|
+
|
|
82
|
+
subnet_entry = {
|
|
83
|
+
"id": subnet_id,
|
|
84
|
+
"name": subnet_data.get("name", f"Subnet {subnet_id}"),
|
|
85
|
+
"description": subnet_data.get("description", ""),
|
|
86
|
+
"owner": owner_address or subnet_data.get("owner", ""),
|
|
87
|
+
"status": subnet_data.get("status", "unknown"),
|
|
88
|
+
"created_at": subnet_data.get("created_at"),
|
|
89
|
+
"updated_at": subnet_data.get("updated_at"),
|
|
90
|
+
"metadata": subnet_data.get("metadata", {}),
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
registry["subnets"][str(subnet_id)] = subnet_entry
|
|
94
|
+
|
|
95
|
+
return save_subnet_registry(registry)
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.error(f"Failed to add subnet {subnet_id} to registry: {e}")
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_subnet_from_registry(subnet_id: int) -> Optional[dict[str, Any]]:
|
|
103
|
+
"""
|
|
104
|
+
Get subnet information from the local registry.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
subnet_id: Subnet ID to retrieve
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Subnet information or None if not found
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
registry = load_subnet_registry()
|
|
114
|
+
return registry["subnets"].get(str(subnet_id))
|
|
115
|
+
except Exception as e:
|
|
116
|
+
logger.error(f"Failed to get subnet {subnet_id} from registry: {e}")
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def remove_subnet_from_registry(subnet_id: int) -> bool:
|
|
121
|
+
"""
|
|
122
|
+
Remove a subnet from the local registry.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
subnet_id: Subnet ID to remove
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
True if removed successfully, False otherwise
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
registry = load_subnet_registry()
|
|
132
|
+
|
|
133
|
+
if str(subnet_id) in registry["subnets"]:
|
|
134
|
+
del registry["subnets"][str(subnet_id)]
|
|
135
|
+
return save_subnet_registry(registry)
|
|
136
|
+
|
|
137
|
+
return False # Subnet not found
|
|
138
|
+
|
|
139
|
+
except Exception as e:
|
|
140
|
+
logger.error(f"Failed to remove subnet {subnet_id} from registry: {e}")
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def list_registered_subnets(owner_filter: Optional[str] = None) -> list[dict[str, Any]]:
|
|
145
|
+
"""
|
|
146
|
+
List all registered subnets.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
owner_filter: Optional owner address to filter by
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
List of subnet information dictionaries
|
|
153
|
+
"""
|
|
154
|
+
try:
|
|
155
|
+
registry = load_subnet_registry()
|
|
156
|
+
subnets = []
|
|
157
|
+
|
|
158
|
+
for subnet_data in registry["subnets"].values():
|
|
159
|
+
if owner_filter and subnet_data.get("owner") != owner_filter:
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
subnets.append(subnet_data)
|
|
163
|
+
|
|
164
|
+
# Sort by subnet ID
|
|
165
|
+
return sorted(subnets, key=lambda x: x.get("id", 0))
|
|
166
|
+
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.error(f"Failed to list registered subnets: {e}")
|
|
169
|
+
return []
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def validate_subnet_manifest(manifest: dict[str, Any]) -> tuple[bool, list[str]]:
|
|
173
|
+
"""
|
|
174
|
+
Validate a subnet manifest structure.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
manifest: Subnet manifest to validate
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Tuple of (is_valid, error_messages)
|
|
181
|
+
"""
|
|
182
|
+
errors = []
|
|
183
|
+
|
|
184
|
+
# Required fields
|
|
185
|
+
required_fields = ["name", "description", "version"]
|
|
186
|
+
for field in required_fields:
|
|
187
|
+
if field not in manifest:
|
|
188
|
+
errors.append(f"Missing required field: {field}")
|
|
189
|
+
|
|
190
|
+
# Validate name
|
|
191
|
+
name = manifest.get("name", "")
|
|
192
|
+
if not name or not isinstance(name, str) or len(name) > 100:
|
|
193
|
+
errors.append("Name must be a non-empty string with max 100 characters")
|
|
194
|
+
|
|
195
|
+
# Validate description
|
|
196
|
+
description = manifest.get("description", "")
|
|
197
|
+
if not description or not isinstance(description, str) or len(description) > 500:
|
|
198
|
+
errors.append("Description must be a non-empty string with max 500 characters")
|
|
199
|
+
|
|
200
|
+
# Validate version
|
|
201
|
+
version = manifest.get("version", "")
|
|
202
|
+
if not version or not isinstance(version, str):
|
|
203
|
+
errors.append("Version must be a non-empty string")
|
|
204
|
+
|
|
205
|
+
# Validate optional fields
|
|
206
|
+
if "tags" in manifest:
|
|
207
|
+
tags = manifest["tags"]
|
|
208
|
+
if not isinstance(tags, list) or any(not isinstance(tag, str) for tag in tags):
|
|
209
|
+
errors.append("Tags must be a list of strings")
|
|
210
|
+
|
|
211
|
+
if "repository" in manifest:
|
|
212
|
+
repo = manifest["repository"]
|
|
213
|
+
if not isinstance(repo, str) or not repo.startswith(
|
|
214
|
+
("http://", "https://", "git://")
|
|
215
|
+
):
|
|
216
|
+
errors.append("Repository must be a valid URL")
|
|
217
|
+
|
|
218
|
+
# Validate configuration if present
|
|
219
|
+
if "config" in manifest:
|
|
220
|
+
config = manifest["config"]
|
|
221
|
+
if not isinstance(config, dict):
|
|
222
|
+
errors.append("Config must be a dictionary")
|
|
223
|
+
else:
|
|
224
|
+
# Validate specific config fields
|
|
225
|
+
if "min_stake" in config:
|
|
226
|
+
min_stake = config["min_stake"]
|
|
227
|
+
if not isinstance(min_stake, (int, float)) or min_stake < 0:
|
|
228
|
+
errors.append("min_stake must be a non-negative number")
|
|
229
|
+
|
|
230
|
+
if "max_nodes" in config:
|
|
231
|
+
max_nodes = config["max_nodes"]
|
|
232
|
+
if not isinstance(max_nodes, int) or max_nodes < 1:
|
|
233
|
+
errors.append("max_nodes must be a positive integer")
|
|
234
|
+
|
|
235
|
+
return len(errors) == 0, errors
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def create_subnet_manifest(
|
|
239
|
+
name: str,
|
|
240
|
+
description: str,
|
|
241
|
+
version: str = "1.0.0",
|
|
242
|
+
tags: Optional[list[str]] = None,
|
|
243
|
+
repository: Optional[str] = None,
|
|
244
|
+
config: Optional[dict[str, Any]] = None,
|
|
245
|
+
) -> dict[str, Any]:
|
|
246
|
+
"""
|
|
247
|
+
Create a subnet manifest with the provided information.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
name: Subnet name
|
|
251
|
+
description: Subnet description
|
|
252
|
+
version: Subnet version
|
|
253
|
+
tags: Optional tags
|
|
254
|
+
repository: Optional repository URL
|
|
255
|
+
config: Optional configuration
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Subnet manifest dictionary
|
|
259
|
+
"""
|
|
260
|
+
manifest = {
|
|
261
|
+
"name": name,
|
|
262
|
+
"description": description,
|
|
263
|
+
"version": version,
|
|
264
|
+
"created_at": str(int(time.time()) if "time" in globals() else 0),
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if tags:
|
|
268
|
+
manifest["tags"] = tags
|
|
269
|
+
|
|
270
|
+
if repository:
|
|
271
|
+
manifest["repository"] = repository
|
|
272
|
+
|
|
273
|
+
if config:
|
|
274
|
+
manifest["config"] = config
|
|
275
|
+
|
|
276
|
+
return manifest
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def update_subnet_registry_entry(subnet_id: int, updates: dict[str, Any]) -> bool:
|
|
280
|
+
"""
|
|
281
|
+
Update a subnet registry entry with new information.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
subnet_id: Subnet ID to update
|
|
285
|
+
updates: Dictionary of fields to update
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
True if updated successfully, False otherwise
|
|
289
|
+
"""
|
|
290
|
+
try:
|
|
291
|
+
registry = load_subnet_registry()
|
|
292
|
+
|
|
293
|
+
if str(subnet_id) not in registry["subnets"]:
|
|
294
|
+
return False # Subnet not found
|
|
295
|
+
|
|
296
|
+
subnet_entry = registry["subnets"][str(subnet_id)]
|
|
297
|
+
|
|
298
|
+
# Update allowed fields
|
|
299
|
+
allowed_fields = ["name", "description", "status", "metadata", "updated_at"]
|
|
300
|
+
for field, value in updates.items():
|
|
301
|
+
if field in allowed_fields:
|
|
302
|
+
subnet_entry[field] = value
|
|
303
|
+
|
|
304
|
+
return save_subnet_registry(registry)
|
|
305
|
+
|
|
306
|
+
except Exception as e:
|
|
307
|
+
logger.error(f"Failed to update subnet {subnet_id} in registry: {e}")
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def get_subnet_statistics() -> dict[str, Any]:
|
|
312
|
+
"""
|
|
313
|
+
Get statistics about registered subnets.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Statistics dictionary
|
|
317
|
+
"""
|
|
318
|
+
try:
|
|
319
|
+
registry = load_subnet_registry()
|
|
320
|
+
subnets = registry["subnets"]
|
|
321
|
+
|
|
322
|
+
# Count by status
|
|
323
|
+
status_counts = {}
|
|
324
|
+
owner_counts = {}
|
|
325
|
+
|
|
326
|
+
for subnet in subnets.values():
|
|
327
|
+
status = subnet.get("status", "unknown")
|
|
328
|
+
status_counts[status] = status_counts.get(status, 0) + 1
|
|
329
|
+
|
|
330
|
+
owner = subnet.get("owner", "unknown")
|
|
331
|
+
if owner != "unknown":
|
|
332
|
+
owner_counts[owner] = owner_counts.get(owner, 0) + 1
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
"total_subnets": len(subnets),
|
|
336
|
+
"status_breakdown": status_counts,
|
|
337
|
+
"top_owners": dict(
|
|
338
|
+
sorted(owner_counts.items(), key=lambda x: x[1], reverse=True)[:10]
|
|
339
|
+
),
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
except Exception as e:
|
|
343
|
+
logger.error(f"Failed to get subnet statistics: {e}")
|
|
344
|
+
return {"total_subnets": 0, "status_breakdown": {}, "top_owners": {}}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for prompting user input.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from rich.prompt import Confirm, IntPrompt, Prompt
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def string_prompt(message: str, default: Optional[str] = None) -> str:
|
|
11
|
+
"""Prompt for a string input."""
|
|
12
|
+
return Prompt.ask(message, default=default)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def integer_prompt(message: str, default: Optional[int] = None) -> int:
|
|
16
|
+
"""Prompt for an integer input."""
|
|
17
|
+
return IntPrompt.ask(message, default=default)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def confirm_prompt(message: str, default: bool = False) -> bool:
|
|
21
|
+
"""Prompt for a yes/no confirmation."""
|
|
22
|
+
return Confirm.ask(message, default=default)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def choice_prompt(message: str, choices: list, default: Optional[str] = None) -> str:
|
|
26
|
+
"""Prompt for a choice from a list of options."""
|
|
27
|
+
return Prompt.ask(message, choices=choices, default=default)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SCALE codec utilities for HTCLI.
|
|
3
|
+
|
|
4
|
+
This module provides SCALE encoding/decoding utilities following the mesh.substrate patterns.
|
|
5
|
+
It uses the centralized type registry from blockchain.type_registry for consistency.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Optional, Union
|
|
10
|
+
|
|
11
|
+
from scalecodec import ScaleBytes
|
|
12
|
+
|
|
13
|
+
from .blockchain.type_registry import get_rpc_runtime_config
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChainDataType(Enum):
|
|
17
|
+
"""
|
|
18
|
+
Enum for chain data types.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
SubnetData = 1
|
|
22
|
+
SubnetInfo = 2
|
|
23
|
+
SubnetNode = 3
|
|
24
|
+
RewardsData = 4
|
|
25
|
+
SubnetNodeInfo = 5
|
|
26
|
+
ConsensusSubmissionData = 6
|
|
27
|
+
SubnetNodeConsensusData = 7
|
|
28
|
+
ConsensusData = 8
|
|
29
|
+
AllSubnetBootnodes = 9
|
|
30
|
+
SubnetNodeStakeInfo = 10
|
|
31
|
+
DelegateStakeInfo = 11
|
|
32
|
+
NodeDelegateStakeInfo = 12
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def from_scale_encoding(
|
|
36
|
+
input_data: Union[list[int], bytes, ScaleBytes],
|
|
37
|
+
type_name: ChainDataType,
|
|
38
|
+
is_vec: bool = False,
|
|
39
|
+
is_option: bool = False,
|
|
40
|
+
use_legacy: bool = False,
|
|
41
|
+
) -> Optional[dict]:
|
|
42
|
+
"""
|
|
43
|
+
Returns the decoded data from the SCALE encoded input.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
input_data: The SCALE encoded input (List[int], bytes, or ScaleBytes)
|
|
47
|
+
type_name: The ChainDataType enum
|
|
48
|
+
is_vec: Whether the input is a Vec
|
|
49
|
+
is_option: Whether the input is an Option
|
|
50
|
+
use_legacy: Whether to use the legacy type registry (for backward compatibility)
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Optional[Dict]: The decoded data
|
|
54
|
+
"""
|
|
55
|
+
type_string = type_name.name
|
|
56
|
+
if is_option:
|
|
57
|
+
type_string = f"Option<{type_string}>"
|
|
58
|
+
if is_vec:
|
|
59
|
+
type_string = f"Vec<{type_string}>"
|
|
60
|
+
|
|
61
|
+
return from_scale_encoding_using_type_string(
|
|
62
|
+
input_data, type_string, use_legacy=use_legacy
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def from_scale_encoding_using_type_string(
|
|
67
|
+
input_data: Union[list[int], bytes, ScaleBytes],
|
|
68
|
+
type_string: str,
|
|
69
|
+
use_legacy: bool = False,
|
|
70
|
+
) -> Optional[dict]:
|
|
71
|
+
"""
|
|
72
|
+
Returns the decoded data from the SCALE encoded input using the type string.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
input_data: The SCALE encoded input (List[int], bytes, or ScaleBytes)
|
|
76
|
+
type_string: The type string (e.g., "SubnetInfo", "Vec<SubnetNodeInfo>")
|
|
77
|
+
use_legacy: Whether to use the legacy type registry (for backward compatibility)
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Optional[Dict]: The decoded data
|
|
81
|
+
"""
|
|
82
|
+
if isinstance(input_data, ScaleBytes):
|
|
83
|
+
as_scale_bytes = input_data
|
|
84
|
+
else:
|
|
85
|
+
if isinstance(input_data, list) and all(isinstance(i, int) for i in input_data):
|
|
86
|
+
vec_u8 = input_data
|
|
87
|
+
as_bytes = bytes(vec_u8)
|
|
88
|
+
elif isinstance(input_data, bytes):
|
|
89
|
+
as_bytes = input_data
|
|
90
|
+
else:
|
|
91
|
+
raise TypeError("input_data must be a List[int], bytes, or ScaleBytes")
|
|
92
|
+
|
|
93
|
+
as_scale_bytes = ScaleBytes(as_bytes)
|
|
94
|
+
|
|
95
|
+
# Use the centralized type registry configuration
|
|
96
|
+
rpc_runtime_config = get_rpc_runtime_config(use_legacy=use_legacy)
|
|
97
|
+
|
|
98
|
+
obj = rpc_runtime_config.create_scale_object(type_string, data=as_scale_bytes)
|
|
99
|
+
|
|
100
|
+
return obj.decode()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def decode_hex_string(hex_str: Union[str, list[int], bytes]) -> str:
|
|
104
|
+
"""
|
|
105
|
+
Decodes a hex string (e.g., '0x...') or list of ints to a UTF-8 string.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
hex_str: Hex string, list of ints, or bytes to decode
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Decoded UTF-8 string
|
|
112
|
+
"""
|
|
113
|
+
if not hex_str:
|
|
114
|
+
return ""
|
|
115
|
+
|
|
116
|
+
# Handle different input types
|
|
117
|
+
if isinstance(hex_str, (list, tuple)):
|
|
118
|
+
try:
|
|
119
|
+
return bytes(hex_str).decode("utf-8")
|
|
120
|
+
except (UnicodeDecodeError, ValueError):
|
|
121
|
+
return str(hex_str)
|
|
122
|
+
elif isinstance(hex_str, bytes):
|
|
123
|
+
try:
|
|
124
|
+
return hex_str.decode("utf-8")
|
|
125
|
+
except (UnicodeDecodeError, ValueError):
|
|
126
|
+
return str(hex_str)
|
|
127
|
+
elif isinstance(hex_str, str):
|
|
128
|
+
# Handle hex string
|
|
129
|
+
if hex_str.startswith("0x"):
|
|
130
|
+
try:
|
|
131
|
+
return bytes.fromhex(hex_str[2:]).decode("utf-8")
|
|
132
|
+
except (UnicodeDecodeError, ValueError):
|
|
133
|
+
return hex_str
|
|
134
|
+
else:
|
|
135
|
+
return hex_str
|
|
136
|
+
|
|
137
|
+
return str(hex_str)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def format_balance(balance: int) -> str:
|
|
141
|
+
"""
|
|
142
|
+
Format balance from planck units to TENSOR with proper formatting.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
balance: Balance in planck units
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Formatted balance string
|
|
149
|
+
"""
|
|
150
|
+
if balance is None:
|
|
151
|
+
return "0.000000 TENSOR"
|
|
152
|
+
|
|
153
|
+
# Convert from planck (1e18) to TENSOR
|
|
154
|
+
tensor_amount = balance / (10**18)
|
|
155
|
+
return f"{tensor_amount:.6f} TENSOR"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validation utilities for prompt functions.
|
|
3
|
+
|
|
4
|
+
This module provides reusable validation functions extracted from Pydantic models
|
|
5
|
+
for use in interactive prompting scenarios.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .prompt_validators import (
|
|
9
|
+
validate_hotkey_address,
|
|
10
|
+
validate_node_id,
|
|
11
|
+
validate_retry_attempts,
|
|
12
|
+
validate_rpc_url,
|
|
13
|
+
validate_stake_amount,
|
|
14
|
+
validate_subnet_bootnodes,
|
|
15
|
+
validate_subnet_delegate_stake_percentage,
|
|
16
|
+
validate_subnet_initial_coldkeys,
|
|
17
|
+
validate_subnet_max_registered_nodes,
|
|
18
|
+
validate_subnet_max_stake,
|
|
19
|
+
validate_subnet_min_stake,
|
|
20
|
+
validate_subnet_name,
|
|
21
|
+
validate_subnet_repo,
|
|
22
|
+
validate_subnet_id,
|
|
23
|
+
validate_timeout,
|
|
24
|
+
validate_wallet_name,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Export with aliases for backward compatibility
|
|
28
|
+
validate_node_id_prompt = validate_node_id
|
|
29
|
+
validate_rpc_url_prompt = validate_rpc_url
|
|
30
|
+
validate_stake_amount_prompt = validate_stake_amount
|
|
31
|
+
validate_subnet_id_prompt = validate_subnet_id
|
|
32
|
+
validate_wallet_name_prompt = validate_wallet_name
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"validate_subnet_name",
|
|
36
|
+
"validate_subnet_repo",
|
|
37
|
+
"validate_subnet_min_stake",
|
|
38
|
+
"validate_subnet_max_stake",
|
|
39
|
+
"validate_subnet_delegate_stake_percentage",
|
|
40
|
+
"validate_subnet_max_registered_nodes",
|
|
41
|
+
"validate_subnet_bootnodes",
|
|
42
|
+
"validate_subnet_initial_coldkeys",
|
|
43
|
+
"validate_hotkey_address",
|
|
44
|
+
"validate_wallet_name",
|
|
45
|
+
"validate_wallet_name_prompt",
|
|
46
|
+
"validate_subnet_id",
|
|
47
|
+
"validate_subnet_id_prompt",
|
|
48
|
+
"validate_node_id",
|
|
49
|
+
"validate_node_id_prompt",
|
|
50
|
+
"validate_stake_amount",
|
|
51
|
+
"validate_stake_amount_prompt",
|
|
52
|
+
"validate_rpc_url",
|
|
53
|
+
"validate_rpc_url_prompt",
|
|
54
|
+
"validate_timeout",
|
|
55
|
+
"validate_retry_attempts",
|
|
56
|
+
]
|
|
57
|
+
|