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,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 {}