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,306 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive prompting utilities for CLI commands.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any, Callable, Optional
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.prompt import Confirm, Prompt
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def prompt_for_required(
|
|
15
|
+
param_name: str,
|
|
16
|
+
param_type: type,
|
|
17
|
+
help_text: str,
|
|
18
|
+
validator: Optional[Callable[[Any], bool]] = None,
|
|
19
|
+
default: Optional[Any] = None,
|
|
20
|
+
allow_empty: bool = False,
|
|
21
|
+
) -> Any:
|
|
22
|
+
"""
|
|
23
|
+
Prompt user for a required parameter value.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
param_name: Name of the parameter
|
|
27
|
+
param_type: Expected type (str, int, float, bool)
|
|
28
|
+
help_text: Help text to display
|
|
29
|
+
validator: Optional validation function
|
|
30
|
+
default: Default value to suggest
|
|
31
|
+
allow_empty: Whether empty values are allowed
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The user input converted to the specified type
|
|
35
|
+
"""
|
|
36
|
+
while True:
|
|
37
|
+
# Build the prompt text
|
|
38
|
+
prompt_text = f"{param_name}"
|
|
39
|
+
if help_text:
|
|
40
|
+
prompt_text += f" ({help_text})"
|
|
41
|
+
if default is not None:
|
|
42
|
+
prompt_text += f" (default: {default})"
|
|
43
|
+
prompt_text += ": "
|
|
44
|
+
|
|
45
|
+
# Get user input
|
|
46
|
+
# Convert default to string for Prompt.ask
|
|
47
|
+
default_str = str(default) if default is not None else ""
|
|
48
|
+
user_input = Prompt.ask(prompt_text, default=default_str)
|
|
49
|
+
|
|
50
|
+
# Handle empty input
|
|
51
|
+
if not user_input.strip() and not allow_empty:
|
|
52
|
+
console.print(
|
|
53
|
+
"[red]❌ This field is required. Please provide a value.[/red]"
|
|
54
|
+
)
|
|
55
|
+
continue
|
|
56
|
+
|
|
57
|
+
if not user_input.strip() and allow_empty:
|
|
58
|
+
return None if param_type != str else ""
|
|
59
|
+
|
|
60
|
+
# Convert to the correct type
|
|
61
|
+
try:
|
|
62
|
+
if param_type == str:
|
|
63
|
+
converted_value = user_input.strip()
|
|
64
|
+
elif param_type == int:
|
|
65
|
+
converted_value = int(user_input.strip())
|
|
66
|
+
elif param_type == float:
|
|
67
|
+
converted_value = float(user_input.strip())
|
|
68
|
+
elif param_type == bool:
|
|
69
|
+
converted_value = user_input.strip().lower() in (
|
|
70
|
+
"true",
|
|
71
|
+
"yes",
|
|
72
|
+
"y",
|
|
73
|
+
"1",
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
converted_value = user_input.strip()
|
|
77
|
+
except ValueError:
|
|
78
|
+
console.print(
|
|
79
|
+
f"[red]❌ Invalid {param_type.__name__} value. Please try again.[/red]"
|
|
80
|
+
)
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
# Run validation if provided
|
|
84
|
+
if validator and not validator(converted_value):
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
return converted_value
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def prompt_for_optional(
|
|
91
|
+
param_name: str,
|
|
92
|
+
param_type: type,
|
|
93
|
+
help_text: str,
|
|
94
|
+
validator: Optional[Callable[[Any], bool]] = None,
|
|
95
|
+
default: Optional[Any] = None,
|
|
96
|
+
) -> Optional[Any]:
|
|
97
|
+
"""
|
|
98
|
+
Prompt user for an optional parameter value.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
param_name: Name of the parameter
|
|
102
|
+
param_type: Expected type (str, int, float, bool)
|
|
103
|
+
help_text: Help text to display
|
|
104
|
+
validator: Optional validation function
|
|
105
|
+
default: Default value to suggest
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
The user input converted to the specified type, or None if skipped
|
|
109
|
+
"""
|
|
110
|
+
# Build the prompt text
|
|
111
|
+
prompt_text = f"{param_name} (optional)"
|
|
112
|
+
if help_text:
|
|
113
|
+
prompt_text += f" ({help_text})"
|
|
114
|
+
if default is not None:
|
|
115
|
+
prompt_text += f" (default: {default})"
|
|
116
|
+
prompt_text += ": "
|
|
117
|
+
|
|
118
|
+
# Get user input
|
|
119
|
+
# Convert default to string for Prompt.ask
|
|
120
|
+
default_str = str(default) if default is not None else ""
|
|
121
|
+
user_input = Prompt.ask(prompt_text, default=default_str)
|
|
122
|
+
|
|
123
|
+
# Handle empty input (skip optional parameter)
|
|
124
|
+
if not user_input.strip():
|
|
125
|
+
return None
|
|
126
|
+
|
|
127
|
+
# Convert to the correct type
|
|
128
|
+
try:
|
|
129
|
+
if param_type == str:
|
|
130
|
+
converted_value = user_input.strip()
|
|
131
|
+
elif param_type == int:
|
|
132
|
+
converted_value = int(user_input.strip())
|
|
133
|
+
elif param_type == float:
|
|
134
|
+
converted_value = float(user_input.strip())
|
|
135
|
+
elif param_type == bool:
|
|
136
|
+
converted_value = user_input.strip().lower() in ("true", "yes", "y", "1")
|
|
137
|
+
else:
|
|
138
|
+
converted_value = user_input.strip()
|
|
139
|
+
except ValueError:
|
|
140
|
+
console.print(
|
|
141
|
+
f"[red]❌ Invalid {param_type.__name__} value. Please try again.[/red]"
|
|
142
|
+
)
|
|
143
|
+
return prompt_for_optional(
|
|
144
|
+
param_name, param_type, help_text, validator, default
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Run validation if provided
|
|
148
|
+
if validator and not validator(converted_value):
|
|
149
|
+
return prompt_for_optional(
|
|
150
|
+
param_name, param_type, help_text, validator, default
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return converted_value
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def prompt_for_list(
|
|
157
|
+
param_name: str,
|
|
158
|
+
help_text: str,
|
|
159
|
+
separator: str = ",",
|
|
160
|
+
validator: Optional[Callable[[Any], bool]] = None,
|
|
161
|
+
default: Optional[str] = None,
|
|
162
|
+
) -> list[str]:
|
|
163
|
+
"""
|
|
164
|
+
Prompt user for a list of values.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
param_name: Name of the parameter
|
|
168
|
+
help_text: Help text to display
|
|
169
|
+
separator: Character to separate list items
|
|
170
|
+
validator: Optional validation function for individual items
|
|
171
|
+
default: Default value to suggest
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
List of user input values
|
|
175
|
+
"""
|
|
176
|
+
while True:
|
|
177
|
+
# Build the prompt text
|
|
178
|
+
prompt_text = f"{param_name}"
|
|
179
|
+
if help_text:
|
|
180
|
+
prompt_text += f" ({help_text})"
|
|
181
|
+
if default is not None:
|
|
182
|
+
prompt_text += f" (default: {default})"
|
|
183
|
+
prompt_text += ": "
|
|
184
|
+
|
|
185
|
+
# Get user input
|
|
186
|
+
user_input = Prompt.ask(
|
|
187
|
+
prompt_text, default=default if default is not None else ""
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Handle empty input
|
|
191
|
+
if not user_input.strip():
|
|
192
|
+
console.print(
|
|
193
|
+
"[red]❌ This field is required. Please provide at least one value.[/red]"
|
|
194
|
+
)
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
# Split the input
|
|
198
|
+
items = [item.strip() for item in user_input.split(separator) if item.strip()]
|
|
199
|
+
|
|
200
|
+
if not items:
|
|
201
|
+
console.print(
|
|
202
|
+
"[red]❌ No valid items found. Please provide at least one value.[/red]"
|
|
203
|
+
)
|
|
204
|
+
continue
|
|
205
|
+
|
|
206
|
+
# Validate individual items if validator provided
|
|
207
|
+
if validator:
|
|
208
|
+
invalid_items = [item for item in items if not validator(item)]
|
|
209
|
+
if invalid_items:
|
|
210
|
+
console.print(
|
|
211
|
+
f"[red]❌ Invalid items: {', '.join(invalid_items)}[/red]"
|
|
212
|
+
)
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
return items
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def prompt_for_confirmation(
|
|
219
|
+
message: str,
|
|
220
|
+
default: bool = False,
|
|
221
|
+
) -> bool:
|
|
222
|
+
"""
|
|
223
|
+
Prompt user for confirmation.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
message: Message to display
|
|
227
|
+
default: Default value
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
True if user confirms, False otherwise
|
|
231
|
+
"""
|
|
232
|
+
return Confirm.ask(message, default=default)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def validate_url(url: str) -> bool:
|
|
236
|
+
"""Validate URL format."""
|
|
237
|
+
url_pattern = re.compile(
|
|
238
|
+
r"^https?://" # http:// or https://
|
|
239
|
+
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|" # domain...
|
|
240
|
+
r"localhost|" # localhost...
|
|
241
|
+
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # ...or ip
|
|
242
|
+
r"(?::\d+)?" # optional port
|
|
243
|
+
r"(?:/?|[/?]\S+)$",
|
|
244
|
+
re.IGNORECASE,
|
|
245
|
+
)
|
|
246
|
+
return bool(url_pattern.match(url))
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def validate_positive_int(value: int) -> bool:
|
|
250
|
+
"""Validate that value is a positive integer."""
|
|
251
|
+
return isinstance(value, int) and value > 0
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def validate_percentage(value: int) -> bool:
|
|
255
|
+
"""Validate that value is a percentage (0-100)."""
|
|
256
|
+
return isinstance(value, int) and 0 <= value <= 100
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def validate_stake_amount(value: int) -> bool:
|
|
260
|
+
"""Validate stake amount."""
|
|
261
|
+
return isinstance(value, int) and value > 0
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def validate_epoch_value(value: int) -> bool:
|
|
265
|
+
"""Validate epoch value."""
|
|
266
|
+
return isinstance(value, int) and 0 <= value <= 1_000_000
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def validate_churn_limit(value: int) -> bool:
|
|
270
|
+
"""Validate churn limit."""
|
|
271
|
+
return isinstance(value, int) and 1 <= value <= 1000
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def validate_max_penalties(value: int) -> bool:
|
|
275
|
+
"""Validate max penalties."""
|
|
276
|
+
return isinstance(value, int) and 1 <= value <= 100
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def validate_max_nodes(value: int) -> bool:
|
|
280
|
+
"""Validate max nodes."""
|
|
281
|
+
return isinstance(value, int) and 1 <= value <= 10_000
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def validate_subnet_name(name: str) -> bool:
|
|
285
|
+
"""Validate subnet name."""
|
|
286
|
+
if not name or len(name) > 50:
|
|
287
|
+
return False
|
|
288
|
+
# Allow alphanumeric, hyphens, underscores, and spaces
|
|
289
|
+
return bool(re.match(r"^[a-zA-Z0-9\-\_\s]+$", name))
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def validate_subnet_description(description: str) -> bool:
|
|
293
|
+
"""Validate subnet description."""
|
|
294
|
+
return 10 <= len(description) <= 1000
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def validate_key_types(key_types: list[str]) -> bool:
|
|
298
|
+
"""Validate key types."""
|
|
299
|
+
valid_types = {"RSA", "Ed25519", "Secp256k1", "ECDSA"}
|
|
300
|
+
return all(kt.strip() in valid_types for kt in key_types)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def validate_coldkey_address(address: str) -> bool:
|
|
304
|
+
"""Validate coldkey address format."""
|
|
305
|
+
# Basic SS58 format validation
|
|
306
|
+
return bool(re.match(r"^[1-9A-HJ-NP-Za-km-z]{32,}$", address))
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Subnet manifest management for tracking locally registered subnets.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SubnetManifestEntry(BaseModel):
|
|
14
|
+
"""Model for a subnet entry in the manifest."""
|
|
15
|
+
|
|
16
|
+
subnet_id: Optional[int] = Field(
|
|
17
|
+
None, description="Subnet ID from blockchain (set after confirmation)"
|
|
18
|
+
)
|
|
19
|
+
name: str = Field(..., description="Subnet name")
|
|
20
|
+
repo: str = Field(..., description="Repository URL")
|
|
21
|
+
description: str = Field(..., description="Subnet description")
|
|
22
|
+
owner_wallet: str = Field(..., description="Wallet used to register the subnet")
|
|
23
|
+
owner_address: str = Field(..., description="Owner's blockchain address")
|
|
24
|
+
transaction_hash: str = Field(..., description="Registration transaction hash")
|
|
25
|
+
block_number: Optional[int] = Field(
|
|
26
|
+
None, description="Block number of registration"
|
|
27
|
+
)
|
|
28
|
+
registration_time: str = Field(
|
|
29
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Subnet parameters
|
|
33
|
+
min_stake: int = Field(..., description="Minimum stake amount")
|
|
34
|
+
max_stake: int = Field(..., description="Maximum stake amount")
|
|
35
|
+
delegate_percentage: int = Field(..., description="Delegate stake percentage")
|
|
36
|
+
churn_limit: int = Field(..., description="Churn limit")
|
|
37
|
+
queue_epochs: int = Field(..., description="Registration queue epochs")
|
|
38
|
+
grace_epochs: int = Field(..., description="Activation grace epochs")
|
|
39
|
+
queue_classification_epochs: int = Field(
|
|
40
|
+
..., description="Queue classification epochs"
|
|
41
|
+
)
|
|
42
|
+
included_classification_epochs: int = Field(
|
|
43
|
+
..., description="Included classification epochs"
|
|
44
|
+
)
|
|
45
|
+
max_penalties: int = Field(..., description="Maximum node penalties")
|
|
46
|
+
max_nodes: int = Field(..., description="Maximum registered nodes")
|
|
47
|
+
hotkey: str = Field(..., description="Hotkey address")
|
|
48
|
+
initial_coldkeys: Optional[str] = Field(
|
|
49
|
+
None, description="Comma-separated initial coldkeys"
|
|
50
|
+
)
|
|
51
|
+
misc: Optional[str] = Field("", description="Miscellaneous information")
|
|
52
|
+
|
|
53
|
+
# Status tracking
|
|
54
|
+
status: str = Field(
|
|
55
|
+
"pending", description="Status: pending, registered, activated, paused, removed"
|
|
56
|
+
)
|
|
57
|
+
confirmed: bool = Field(
|
|
58
|
+
False, description="Whether registration is confirmed on chain"
|
|
59
|
+
)
|
|
60
|
+
last_updated: str = Field(
|
|
61
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SubnetManifest(BaseModel):
|
|
66
|
+
"""Model for the complete subnet manifest."""
|
|
67
|
+
|
|
68
|
+
version: str = Field("1.0", description="Manifest format version")
|
|
69
|
+
created: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
70
|
+
last_updated: str = Field(
|
|
71
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
72
|
+
)
|
|
73
|
+
subnets: dict[str, SubnetManifestEntry] = Field(
|
|
74
|
+
default_factory=dict, description="Subnets by registration tx hash"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_subnet_registry_path() -> Path:
|
|
79
|
+
"""Get the path to the subnet registry file."""
|
|
80
|
+
subnet_dir = Path.home() / ".hypertensor" / "subnets"
|
|
81
|
+
subnet_dir.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
return subnet_dir / "subnets.json"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def load_subnet_registry() -> SubnetManifest:
|
|
86
|
+
"""Load the subnet registry from disk."""
|
|
87
|
+
registry_path = get_subnet_registry_path()
|
|
88
|
+
|
|
89
|
+
if registry_path.exists():
|
|
90
|
+
try:
|
|
91
|
+
with open(registry_path, encoding="utf-8") as f:
|
|
92
|
+
data = json.load(f)
|
|
93
|
+
return SubnetManifest(**data)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
print(f"⚠️ Warning: Failed to load subnet registry: {e}")
|
|
96
|
+
print("Creating new registry...")
|
|
97
|
+
|
|
98
|
+
return SubnetManifest()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def save_subnet_registry(registry: SubnetManifest) -> bool:
|
|
102
|
+
"""Save the subnet registry to disk."""
|
|
103
|
+
try:
|
|
104
|
+
registry_path = get_subnet_registry_path()
|
|
105
|
+
registry.last_updated = datetime.now(timezone.utc).isoformat()
|
|
106
|
+
|
|
107
|
+
with open(registry_path, "w", encoding="utf-8") as f:
|
|
108
|
+
json.dump(registry.dict(), f, indent=2, ensure_ascii=False)
|
|
109
|
+
|
|
110
|
+
return True
|
|
111
|
+
except Exception as e:
|
|
112
|
+
print(f"❌ Failed to save subnet registry: {e}")
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def add_subnet_to_registry(
|
|
117
|
+
name: str,
|
|
118
|
+
repo: str,
|
|
119
|
+
description: str,
|
|
120
|
+
owner_wallet: str,
|
|
121
|
+
owner_address: str,
|
|
122
|
+
transaction_hash: str,
|
|
123
|
+
block_number: Optional[int],
|
|
124
|
+
subnet_params: dict,
|
|
125
|
+
) -> bool:
|
|
126
|
+
"""Add a new subnet registration to the registry."""
|
|
127
|
+
try:
|
|
128
|
+
registry = load_subnet_registry()
|
|
129
|
+
|
|
130
|
+
# Create subnet entry
|
|
131
|
+
entry = SubnetManifestEntry(
|
|
132
|
+
name=name,
|
|
133
|
+
repo=repo,
|
|
134
|
+
description=description,
|
|
135
|
+
owner_wallet=owner_wallet,
|
|
136
|
+
owner_address=owner_address,
|
|
137
|
+
transaction_hash=transaction_hash,
|
|
138
|
+
block_number=block_number,
|
|
139
|
+
**subnet_params,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Add to registry using transaction hash as key
|
|
143
|
+
registry.subnets[transaction_hash] = entry
|
|
144
|
+
|
|
145
|
+
return save_subnet_registry(registry)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
print(f"❌ Failed to add subnet to registry: {e}")
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def update_subnet_status(
|
|
152
|
+
transaction_hash: str, status: str, subnet_id: Optional[int] = None
|
|
153
|
+
) -> bool:
|
|
154
|
+
"""Update the status of a subnet in the registry."""
|
|
155
|
+
try:
|
|
156
|
+
registry = load_subnet_registry()
|
|
157
|
+
|
|
158
|
+
if transaction_hash in registry.subnets:
|
|
159
|
+
registry.subnets[transaction_hash].status = status
|
|
160
|
+
registry.subnets[transaction_hash].last_updated = datetime.now(
|
|
161
|
+
timezone.utc
|
|
162
|
+
).isoformat()
|
|
163
|
+
|
|
164
|
+
if subnet_id is not None:
|
|
165
|
+
registry.subnets[transaction_hash].subnet_id = subnet_id
|
|
166
|
+
registry.subnets[transaction_hash].confirmed = True
|
|
167
|
+
|
|
168
|
+
return save_subnet_registry(registry)
|
|
169
|
+
else:
|
|
170
|
+
print(
|
|
171
|
+
f"⚠️ Subnet with transaction hash {transaction_hash} not found in registry"
|
|
172
|
+
)
|
|
173
|
+
return False
|
|
174
|
+
except Exception as e:
|
|
175
|
+
print(f"❌ Failed to update subnet status: {e}")
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def get_user_subnets(owner_address: Optional[str] = None) -> list[SubnetManifestEntry]:
|
|
180
|
+
"""Get subnets owned by a specific address or all subnets."""
|
|
181
|
+
try:
|
|
182
|
+
registry = load_subnet_registry()
|
|
183
|
+
subnets = list(registry.subnets.values())
|
|
184
|
+
|
|
185
|
+
if owner_address:
|
|
186
|
+
subnets = [s for s in subnets if s.owner_address == owner_address]
|
|
187
|
+
|
|
188
|
+
# Sort by registration time (newest first)
|
|
189
|
+
subnets.sort(key=lambda x: x.registration_time, reverse=True)
|
|
190
|
+
return subnets
|
|
191
|
+
except Exception as e:
|
|
192
|
+
print(f"❌ Failed to get user subnets: {e}")
|
|
193
|
+
return []
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def find_subnet_by_name(name: str) -> Optional[SubnetManifestEntry]:
|
|
197
|
+
"""Find a subnet by name."""
|
|
198
|
+
try:
|
|
199
|
+
registry = load_subnet_registry()
|
|
200
|
+
for subnet in registry.subnets.values():
|
|
201
|
+
if subnet.name == name:
|
|
202
|
+
return subnet
|
|
203
|
+
return None
|
|
204
|
+
except Exception as e:
|
|
205
|
+
print(f"❌ Failed to find subnet by name: {e}")
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def find_subnet_by_id(subnet_id: int) -> Optional[SubnetManifestEntry]:
|
|
210
|
+
"""Find a subnet by its blockchain ID."""
|
|
211
|
+
try:
|
|
212
|
+
registry = load_subnet_registry()
|
|
213
|
+
for subnet in registry.subnets.values():
|
|
214
|
+
if subnet.subnet_id == subnet_id:
|
|
215
|
+
return subnet
|
|
216
|
+
return None
|
|
217
|
+
except Exception as e:
|
|
218
|
+
print(f"❌ Failed to find subnet by ID: {e}")
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def remove_subnet_from_registry(transaction_hash: str) -> bool:
|
|
223
|
+
"""Remove a subnet from the registry."""
|
|
224
|
+
try:
|
|
225
|
+
registry = load_subnet_registry()
|
|
226
|
+
|
|
227
|
+
if transaction_hash in registry.subnets:
|
|
228
|
+
del registry.subnets[transaction_hash]
|
|
229
|
+
return save_subnet_registry(registry)
|
|
230
|
+
else:
|
|
231
|
+
print(
|
|
232
|
+
f"⚠️ Subnet with transaction hash {transaction_hash} not found in registry"
|
|
233
|
+
)
|
|
234
|
+
return False
|
|
235
|
+
except Exception as e:
|
|
236
|
+
print(f"❌ Failed to remove subnet from registry: {e}")
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def get_registry_stats() -> dict:
|
|
241
|
+
"""Get statistics about the subnets in the registry."""
|
|
242
|
+
try:
|
|
243
|
+
registry = load_subnet_registry()
|
|
244
|
+
subnets = list(registry.subnets.values())
|
|
245
|
+
|
|
246
|
+
total = len(subnets)
|
|
247
|
+
pending = len([s for s in subnets if s.status == "pending"])
|
|
248
|
+
registered = len([s for s in subnets if s.status == "registered"])
|
|
249
|
+
activated = len([s for s in subnets if s.status == "activated"])
|
|
250
|
+
paused = len([s for s in subnets if s.status == "paused"])
|
|
251
|
+
removed = len([s for s in subnets if s.status == "removed"])
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
"total": total,
|
|
255
|
+
"pending": pending,
|
|
256
|
+
"registered": registered,
|
|
257
|
+
"activated": activated,
|
|
258
|
+
"paused": paused,
|
|
259
|
+
"removed": removed,
|
|
260
|
+
"confirmed": len([s for s in subnets if s.confirmed]),
|
|
261
|
+
"unconfirmed": len([s for s in subnets if not s.confirmed]),
|
|
262
|
+
}
|
|
263
|
+
except Exception as e:
|
|
264
|
+
print(f"❌ Failed to get registry stats: {e}")
|
|
265
|
+
return {}
|