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,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
+