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,488 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validation helpers for user input in the Hypertensor CLI.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, Union
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def validate_address(address: str) -> bool:
|
|
11
|
+
"""Validate SS58 address format."""
|
|
12
|
+
# Basic SS58 validation - should be 42-48 characters and start with a number
|
|
13
|
+
if not address or len(address) < 42 or len(address) > 48:
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
# Should start with a number (5 for mainnet, 0 for testnet)
|
|
17
|
+
if not address[0].isdigit():
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
# Should contain only alphanumeric characters
|
|
21
|
+
if not re.match(r"^[1-9A-HJ-NP-Za-km-z]+$", address):
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
return True
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def validate_ethereum_address(address: str) -> bool:
|
|
28
|
+
"""Validate Ethereum-style address format (Bytes20: 0x + 40 hex chars = 20 bytes)."""
|
|
29
|
+
if not address:
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
# Must start with 0x
|
|
33
|
+
if not address.startswith("0x"):
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
# Must be exactly 42 characters (0x + 40 hex chars = 20 bytes)
|
|
37
|
+
if len(address) != 42:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
# Must contain only valid hex characters after 0x
|
|
41
|
+
hex_part = address[2:]
|
|
42
|
+
if not re.match(r"^[a-fA-F0-9]{40}$", hex_part):
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def validate_hotkey_address(address: str) -> bool:
|
|
49
|
+
"""Validate hotkey address - should be Bytes20 format for EVM compatibility."""
|
|
50
|
+
return validate_ethereum_address(address)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def validate_amount(amount: Union[str, float, int]) -> bool:
|
|
54
|
+
"""Validate amount format for TENSOR token (18 decimals)."""
|
|
55
|
+
try:
|
|
56
|
+
if isinstance(amount, str):
|
|
57
|
+
amount = float(amount)
|
|
58
|
+
elif isinstance(amount, int):
|
|
59
|
+
amount = float(amount)
|
|
60
|
+
|
|
61
|
+
if amount <= 0:
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
# Check for TENSOR precision (18 decimals)
|
|
65
|
+
str_amount = f"{amount:.18f}"
|
|
66
|
+
if len(str_amount.split(".")[-1]) > 18:
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
return True
|
|
70
|
+
except (ValueError, TypeError):
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def validate_subnet_id(subnet_id: Union[str, int]) -> bool:
|
|
75
|
+
"""Validate subnet ID format."""
|
|
76
|
+
try:
|
|
77
|
+
if isinstance(subnet_id, str):
|
|
78
|
+
subnet_id = int(subnet_id)
|
|
79
|
+
|
|
80
|
+
if subnet_id <= 0:
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
return True
|
|
84
|
+
except (ValueError, TypeError):
|
|
85
|
+
return False
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def validate_node_id(node_id: Union[str, int]) -> bool:
|
|
89
|
+
"""Validate node ID format."""
|
|
90
|
+
try:
|
|
91
|
+
if isinstance(node_id, str):
|
|
92
|
+
node_id = int(node_id)
|
|
93
|
+
|
|
94
|
+
if node_id <= 0:
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
return True
|
|
98
|
+
except (ValueError, TypeError):
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def validate_peer_id(peer_id: str) -> bool:
|
|
103
|
+
"""Validate peer ID format (MultiHash)."""
|
|
104
|
+
# Basic MultiHash validation
|
|
105
|
+
if not peer_id or len(peer_id) < 10:
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
# Should start with Qm (base58btc multihash)
|
|
109
|
+
if not peer_id.startswith("Qm"):
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
# Should contain only base58 characters
|
|
113
|
+
if not re.match(r"^[1-9A-HJ-NP-Za-km-z]+$", peer_id):
|
|
114
|
+
return False
|
|
115
|
+
|
|
116
|
+
return True
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def validate_key_type(key_type: str) -> bool:
|
|
120
|
+
"""Validate key type."""
|
|
121
|
+
valid_types = ["sr25519", "ed25519", "ecdsa"]
|
|
122
|
+
return key_type.lower() in valid_types
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def validate_password(password: Optional[str]) -> bool:
|
|
126
|
+
"""Validate password strength."""
|
|
127
|
+
if password is None:
|
|
128
|
+
return True # Allow empty passwords
|
|
129
|
+
|
|
130
|
+
if len(password) < 8:
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
# Should contain at least one letter and one number
|
|
134
|
+
if not re.search(r"[a-zA-Z]", password) or not re.search(r"\d", password):
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def validate_file_path(file_path: str) -> bool:
|
|
141
|
+
"""Validate file path."""
|
|
142
|
+
try:
|
|
143
|
+
Path(file_path)
|
|
144
|
+
# Check if parent directory exists or can be created
|
|
145
|
+
return True
|
|
146
|
+
except Exception:
|
|
147
|
+
return False
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def validate_rpc_url(url: str) -> bool:
|
|
151
|
+
"""Validate RPC URL format."""
|
|
152
|
+
# Basic URL validation
|
|
153
|
+
if not url:
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
# Should start with ws:// or wss:// or http:// or https://
|
|
157
|
+
if not re.match(r"^(ws|wss|http|https)://", url):
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
return True
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def validate_vote_type(vote: str) -> bool:
|
|
164
|
+
"""Validate vote type."""
|
|
165
|
+
valid_votes = ["yay", "nay"]
|
|
166
|
+
return vote.lower() in valid_votes
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def validate_proposal_data(data: str) -> bool:
|
|
170
|
+
"""Validate proposal data."""
|
|
171
|
+
if not data or len(data.strip()) == 0:
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
# Basic length validation
|
|
175
|
+
if len(data) > 10000: # 10KB limit
|
|
176
|
+
return False
|
|
177
|
+
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def validate_validation_data(data: str) -> bool:
|
|
182
|
+
"""Validate validation data."""
|
|
183
|
+
if not data or len(data.strip()) == 0:
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
# Basic length validation
|
|
187
|
+
if len(data) > 100000: # 100KB limit
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def validate_memory_mb(memory_mb: int) -> bool:
|
|
194
|
+
"""Validate memory requirement in MB."""
|
|
195
|
+
if memory_mb <= 0 or memory_mb > 100000: # 100GB limit
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def validate_registration_blocks(blocks: int) -> bool:
|
|
202
|
+
"""Validate registration period in blocks."""
|
|
203
|
+
if blocks <= 0 or blocks > 1000000: # 1M blocks limit
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
return True
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def validate_entry_interval(interval: int) -> bool:
|
|
210
|
+
"""Validate entry interval in blocks."""
|
|
211
|
+
if interval <= 0 or interval > 100000: # 100K blocks limit
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
return True
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def validate_subnet_path(path: str) -> bool:
|
|
218
|
+
"""Validate subnet path/name."""
|
|
219
|
+
if not path or len(path.strip()) == 0:
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
# Should be alphanumeric with hyphens and underscores
|
|
223
|
+
if not re.match(r"^[a-zA-Z0-9_-]+$", path):
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
# Length validation
|
|
227
|
+
if len(path) > 100:
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def validate_wallet_name(name: str) -> bool:
|
|
234
|
+
"""Validate wallet name."""
|
|
235
|
+
if not name or len(name.strip()) == 0:
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
# Should be alphanumeric with hyphens and underscores
|
|
239
|
+
if not re.match(r"^[a-zA-Z0-9_-]+$", name):
|
|
240
|
+
return False
|
|
241
|
+
|
|
242
|
+
# Length validation
|
|
243
|
+
if len(name) > 50:
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
return True
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def validate_private_key(private_key: str) -> bool:
|
|
250
|
+
"""Validate private key format."""
|
|
251
|
+
if not private_key or len(private_key.strip()) == 0:
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
# Should be hex string
|
|
255
|
+
if not re.match(r"^[0-9a-fA-F]+$", private_key):
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
# Length validation (32 bytes = 64 hex characters)
|
|
259
|
+
if len(private_key) != 64:
|
|
260
|
+
return False
|
|
261
|
+
|
|
262
|
+
return True
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def validate_mnemonic(mnemonic: str) -> bool:
|
|
266
|
+
"""Validate mnemonic phrase."""
|
|
267
|
+
if not mnemonic or len(mnemonic.strip()) == 0:
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
# Split into words
|
|
271
|
+
words = mnemonic.strip().split()
|
|
272
|
+
|
|
273
|
+
# Should have 12, 15, 18, 21, or 24 words
|
|
274
|
+
if len(words) not in [12, 15, 18, 21, 24]:
|
|
275
|
+
return False
|
|
276
|
+
|
|
277
|
+
# All words should be lowercase
|
|
278
|
+
for word in words:
|
|
279
|
+
if not word.islower() or not word.isalpha():
|
|
280
|
+
return False
|
|
281
|
+
|
|
282
|
+
return True
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def validate_block_number(block_number: Optional[Union[str, int]]) -> bool:
|
|
286
|
+
"""Validate block number."""
|
|
287
|
+
if block_number is None:
|
|
288
|
+
return True # Allow None for latest block
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
if isinstance(block_number, str):
|
|
292
|
+
block_number = int(block_number)
|
|
293
|
+
|
|
294
|
+
if block_number < 0:
|
|
295
|
+
return False
|
|
296
|
+
|
|
297
|
+
return True
|
|
298
|
+
except (ValueError, TypeError):
|
|
299
|
+
return False
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def validate_limit(limit: Union[str, int]) -> bool:
|
|
303
|
+
"""Validate limit parameter."""
|
|
304
|
+
try:
|
|
305
|
+
if isinstance(limit, str):
|
|
306
|
+
limit = int(limit)
|
|
307
|
+
|
|
308
|
+
if limit <= 0 or limit > 1000:
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
return True
|
|
312
|
+
except (ValueError, TypeError):
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def validate_tensor_stake_amount(amount: Union[str, float, int]) -> bool:
|
|
317
|
+
"""Validate TENSOR stake amount with 18 decimal precision."""
|
|
318
|
+
try:
|
|
319
|
+
if isinstance(amount, str):
|
|
320
|
+
amount = float(amount)
|
|
321
|
+
elif isinstance(amount, int):
|
|
322
|
+
amount = float(amount)
|
|
323
|
+
|
|
324
|
+
if amount <= 0:
|
|
325
|
+
return False
|
|
326
|
+
|
|
327
|
+
# Check for TENSOR precision (18 decimals)
|
|
328
|
+
str_amount = f"{amount:.18f}"
|
|
329
|
+
if len(str_amount.split(".")[-1]) > 18:
|
|
330
|
+
return False
|
|
331
|
+
|
|
332
|
+
return True
|
|
333
|
+
except (ValueError, TypeError):
|
|
334
|
+
return False
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def validate_tensor_balance(amount: Union[str, float, int]) -> bool:
|
|
338
|
+
"""Validate TENSOR balance amount with 18 decimal precision."""
|
|
339
|
+
try:
|
|
340
|
+
if isinstance(amount, str):
|
|
341
|
+
amount = float(amount)
|
|
342
|
+
elif isinstance(amount, int):
|
|
343
|
+
amount = float(amount)
|
|
344
|
+
|
|
345
|
+
if amount < 0: # Allow zero balance
|
|
346
|
+
return False
|
|
347
|
+
|
|
348
|
+
# Check for TENSOR precision (18 decimals)
|
|
349
|
+
str_amount = f"{amount:.18f}"
|
|
350
|
+
if len(str_amount.split(".")[-1]) > 18:
|
|
351
|
+
return False
|
|
352
|
+
|
|
353
|
+
return True
|
|
354
|
+
except (ValueError, TypeError):
|
|
355
|
+
return False
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def validate_url(url: str) -> bool:
|
|
359
|
+
"""Validate URL format for configuration."""
|
|
360
|
+
if not url:
|
|
361
|
+
return False
|
|
362
|
+
|
|
363
|
+
# Should start with ws:// or wss:// or http:// or https://
|
|
364
|
+
if not re.match(r"^(ws|wss|http|https)://", url):
|
|
365
|
+
return False
|
|
366
|
+
|
|
367
|
+
# Basic domain validation
|
|
368
|
+
if len(url) < 10:
|
|
369
|
+
return False
|
|
370
|
+
|
|
371
|
+
return True
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def validate_path(path: str) -> bool:
|
|
375
|
+
"""Validate file/directory path for configuration."""
|
|
376
|
+
try:
|
|
377
|
+
Path(path).expanduser()
|
|
378
|
+
# Check if parent directory exists or can be created
|
|
379
|
+
return True
|
|
380
|
+
except Exception:
|
|
381
|
+
return False
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def validate_subnet_name(name: str) -> bool:
|
|
385
|
+
"""Validate subnet name."""
|
|
386
|
+
if not name or len(name) < 1 or len(name) > 100:
|
|
387
|
+
return False
|
|
388
|
+
# Allow alphanumeric, hyphens, underscores, spaces
|
|
389
|
+
import re
|
|
390
|
+
|
|
391
|
+
return bool(re.match(r"^[a-zA-Z0-9\-\_\s]+$", name))
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def validate_repo_url(repo: str) -> bool:
|
|
395
|
+
"""Validate repository URL."""
|
|
396
|
+
if not repo or len(repo) < 10 or len(repo) > 500:
|
|
397
|
+
return False
|
|
398
|
+
# Basic URL validation
|
|
399
|
+
import re
|
|
400
|
+
|
|
401
|
+
return bool(re.match(r"^https?://[^\s/$.?#].[^\s]*$", repo))
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def validate_subnet_description(description: str) -> bool:
|
|
405
|
+
"""Validate subnet description."""
|
|
406
|
+
if not description or len(description) < 10 or len(description) > 1000:
|
|
407
|
+
return False
|
|
408
|
+
return True
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def validate_stake_amount(amount: int) -> bool:
|
|
412
|
+
"""Validate stake amount."""
|
|
413
|
+
return amount > 0 and amount <= 10**18 # Max 1 billion TENSOR
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def validate_delegate_percentage(percentage: int) -> bool:
|
|
417
|
+
"""Validate delegate stake percentage."""
|
|
418
|
+
return 0 <= percentage <= 100
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def validate_epoch_value(epochs: int) -> bool:
|
|
422
|
+
"""Validate epoch-based values."""
|
|
423
|
+
return 0 <= epochs <= 1000000
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def validate_churn_limit(churn_limit: int) -> bool:
|
|
427
|
+
"""Validate churn limit."""
|
|
428
|
+
return 1 <= churn_limit <= 1000
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def validate_max_nodes(max_nodes: int) -> bool:
|
|
432
|
+
"""Validate maximum registered nodes."""
|
|
433
|
+
return 1 <= max_nodes <= 10000
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def validate_max_penalties(penalties: int) -> bool:
|
|
437
|
+
"""Validate maximum node penalties."""
|
|
438
|
+
return 1 <= penalties <= 100
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def validate_key_types(key_types: list) -> bool:
|
|
442
|
+
"""Validate key types (matching Rust enum names)."""
|
|
443
|
+
valid_types = ["Rsa", "Ed25519", "Secp256k1", "Ecdsa"]
|
|
444
|
+
return all(kt in valid_types for kt in key_types)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def validate_node_removal_system(system: str) -> bool:
|
|
448
|
+
"""Validate node removal system (matching Rust enum)."""
|
|
449
|
+
valid_systems = ["Consensus", "Stake", "Reputation"]
|
|
450
|
+
return system in valid_systems
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def validate_coldkey_addresses(addresses: list[str]) -> bool:
|
|
454
|
+
"""Validate a list of coldkey addresses (supports both SS58 and Ethereum formats)."""
|
|
455
|
+
if not addresses:
|
|
456
|
+
return True # Empty list is valid for subnet registration
|
|
457
|
+
|
|
458
|
+
for address in addresses:
|
|
459
|
+
# Accept either SS58 or Ethereum format
|
|
460
|
+
if not (validate_ss58_address(address) or validate_ethereum_address(address)):
|
|
461
|
+
return False
|
|
462
|
+
|
|
463
|
+
return True
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def validate_ss58_address(address: str) -> bool:
|
|
467
|
+
"""Validate SS58 address format."""
|
|
468
|
+
if not address or len(address) < 10 or len(address) > 100:
|
|
469
|
+
return False
|
|
470
|
+
# Basic SS58 format validation (starts with number and contains alphanumeric)
|
|
471
|
+
import re
|
|
472
|
+
|
|
473
|
+
return bool(re.match(r"^[1-9][a-zA-Z0-9]+$", address))
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def validate_delegate_reward_rate(rate: int) -> bool:
|
|
477
|
+
"""Validate delegate reward rate."""
|
|
478
|
+
if not isinstance(rate, int):
|
|
479
|
+
return False
|
|
480
|
+
|
|
481
|
+
if rate < 0:
|
|
482
|
+
return False
|
|
483
|
+
|
|
484
|
+
# Rate should be reasonable (not excessively high)
|
|
485
|
+
if rate > 1000000000000000000: # 1 TENSOR in smallest units
|
|
486
|
+
return False
|
|
487
|
+
|
|
488
|
+
return True
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Centralized logging configuration for HTCLI.
|
|
3
|
+
|
|
4
|
+
This module provides a standardized logging setup with proper formatting,
|
|
5
|
+
log levels, and file rotation for the HTCLI application.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import logging.handlers
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.logging import RichHandler
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HTCLILogger:
|
|
18
|
+
"""Centralized logger configuration for HTCLI."""
|
|
19
|
+
|
|
20
|
+
_initialized = False
|
|
21
|
+
_console = Console()
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def setup_logging(
|
|
25
|
+
cls,
|
|
26
|
+
level: str = "INFO",
|
|
27
|
+
log_file: Optional[Path] = None,
|
|
28
|
+
enable_file_logging: bool = True,
|
|
29
|
+
enable_console_logging: bool = False,
|
|
30
|
+
) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Set up centralized logging configuration.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
36
|
+
log_file: Path to log file (defaults to ~/.htcli/logs/htcli.log)
|
|
37
|
+
enable_file_logging: Whether to enable file logging
|
|
38
|
+
enable_console_logging: Whether to enable console logging
|
|
39
|
+
"""
|
|
40
|
+
if cls._initialized:
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
# Create log directory if it doesn't exist
|
|
44
|
+
if log_file is None:
|
|
45
|
+
log_dir = Path.home() / ".htcli" / "logs"
|
|
46
|
+
try:
|
|
47
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
48
|
+
except OSError:
|
|
49
|
+
enable_file_logging = False
|
|
50
|
+
log_file = log_dir / "htcli.log"
|
|
51
|
+
|
|
52
|
+
# Configure root logger
|
|
53
|
+
root_logger = logging.getLogger()
|
|
54
|
+
root_logger.setLevel(getattr(logging, level.upper()))
|
|
55
|
+
|
|
56
|
+
# Clear existing handlers
|
|
57
|
+
root_logger.handlers.clear()
|
|
58
|
+
|
|
59
|
+
# Console handler with Rich formatting (only if explicitly enabled)
|
|
60
|
+
# If console logging is disabled, do NOT add any console handlers
|
|
61
|
+
# All output should go to the log file
|
|
62
|
+
if enable_console_logging:
|
|
63
|
+
console_handler = RichHandler(
|
|
64
|
+
console=cls._console,
|
|
65
|
+
rich_tracebacks=True,
|
|
66
|
+
show_path=False,
|
|
67
|
+
show_time=True,
|
|
68
|
+
)
|
|
69
|
+
console_handler.setLevel(getattr(logging, level.upper()))
|
|
70
|
+
|
|
71
|
+
# Only show DEBUG/INFO on console if level is DEBUG
|
|
72
|
+
if level.upper() != "DEBUG":
|
|
73
|
+
console_handler.setLevel(logging.WARNING)
|
|
74
|
+
|
|
75
|
+
root_logger.addHandler(console_handler)
|
|
76
|
+
|
|
77
|
+
# File handler with rotation
|
|
78
|
+
if enable_file_logging:
|
|
79
|
+
try:
|
|
80
|
+
file_handler = logging.handlers.RotatingFileHandler(
|
|
81
|
+
log_file,
|
|
82
|
+
maxBytes=10 * 1024 * 1024, # 10MB
|
|
83
|
+
backupCount=5,
|
|
84
|
+
encoding="utf-8",
|
|
85
|
+
)
|
|
86
|
+
file_handler.setLevel(logging.DEBUG)
|
|
87
|
+
|
|
88
|
+
# File formatter with more detail
|
|
89
|
+
file_formatter = logging.Formatter(
|
|
90
|
+
fmt="%(asctime)s | %(levelname)-8s | %(name)s:%(lineno)d | %(message)s",
|
|
91
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
92
|
+
)
|
|
93
|
+
file_handler.setFormatter(file_formatter)
|
|
94
|
+
|
|
95
|
+
root_logger.addHandler(file_handler)
|
|
96
|
+
except OSError:
|
|
97
|
+
enable_file_logging = False
|
|
98
|
+
|
|
99
|
+
# Set specific logger levels
|
|
100
|
+
cls._configure_logger_levels()
|
|
101
|
+
|
|
102
|
+
cls._initialized = True
|
|
103
|
+
|
|
104
|
+
# Log initialization
|
|
105
|
+
logger = logging.getLogger(__name__)
|
|
106
|
+
logger.info(f"HTCLI logging initialized - Level: {level}")
|
|
107
|
+
if enable_file_logging:
|
|
108
|
+
logger.info(f"Log file: {log_file}")
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def _configure_logger_levels(cls) -> None:
|
|
112
|
+
"""Configure specific logger levels for different modules."""
|
|
113
|
+
# Reduce noise from external libraries - set to ERROR to suppress warnings
|
|
114
|
+
substrate_logger = logging.getLogger("substrateinterface")
|
|
115
|
+
substrate_logger.setLevel(logging.ERROR)
|
|
116
|
+
substrate_logger.handlers.clear() # Clear any handlers from substrateinterface
|
|
117
|
+
|
|
118
|
+
logging.getLogger("websockets").setLevel(logging.ERROR)
|
|
119
|
+
logging.getLogger("websockets").handlers.clear()
|
|
120
|
+
|
|
121
|
+
logging.getLogger("urllib3").setLevel(logging.ERROR)
|
|
122
|
+
logging.getLogger("requests").setLevel(logging.ERROR)
|
|
123
|
+
|
|
124
|
+
# Enable debug for HTCLI modules if needed
|
|
125
|
+
logging.getLogger("htcli").setLevel(logging.DEBUG)
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def get_logger(cls, name: str) -> logging.Logger:
|
|
129
|
+
"""
|
|
130
|
+
Get a logger instance with proper configuration.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
name: Logger name (usually __name__)
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Configured logger instance
|
|
137
|
+
"""
|
|
138
|
+
if not cls._initialized:
|
|
139
|
+
# Initialize with console logging disabled by default to keep terminal clean
|
|
140
|
+
cls.setup_logging(enable_console_logging=False)
|
|
141
|
+
|
|
142
|
+
return logging.getLogger(name)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def get_logger(name: str) -> logging.Logger:
|
|
146
|
+
"""
|
|
147
|
+
Convenience function to get a configured logger.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
name: Logger name (usually __name__)
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
Configured logger instance
|
|
154
|
+
"""
|
|
155
|
+
return HTCLILogger.get_logger(name)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def setup_logging(
|
|
159
|
+
level: str = "INFO",
|
|
160
|
+
log_file: Optional[Path] = None,
|
|
161
|
+
enable_file_logging: bool = True,
|
|
162
|
+
enable_console_logging: bool = False,
|
|
163
|
+
) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Convenience function to set up logging.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
169
|
+
log_file: Path to log file
|
|
170
|
+
enable_file_logging: Whether to enable file logging
|
|
171
|
+
enable_console_logging: Whether to enable console logging
|
|
172
|
+
"""
|
|
173
|
+
HTCLILogger.setup_logging(
|
|
174
|
+
level=level,
|
|
175
|
+
log_file=log_file,
|
|
176
|
+
enable_file_logging=enable_file_logging,
|
|
177
|
+
enable_console_logging=enable_console_logging,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# Do NOT initialize logging on module import
|
|
182
|
+
# Let the application entry point (main.py) control initialization
|
|
183
|
+
# setup_logging()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Network utilities for the Hypertensor CLI.
|
|
3
|
+
Handles subnet management and network-specific operations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .subnet import (
|
|
7
|
+
add_subnet_to_registry,
|
|
8
|
+
get_subnet_from_registry,
|
|
9
|
+
list_registered_subnets,
|
|
10
|
+
remove_subnet_from_registry,
|
|
11
|
+
validate_subnet_manifest,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"add_subnet_to_registry",
|
|
16
|
+
"get_subnet_from_registry",
|
|
17
|
+
"remove_subnet_from_registry",
|
|
18
|
+
"list_registered_subnets",
|
|
19
|
+
"validate_subnet_manifest",
|
|
20
|
+
]
|