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,1225 @@
1
+ from pathlib import Path
2
+ from typing import Optional
3
+
4
+ import typer
5
+ from pydantic import ValidationError
6
+ from rich import console
7
+
8
+ from src.htcli.ui.colors import info # noqa: F401
9
+
10
+ from ...client.offchain.wallet import WalletManager
11
+ from ...models.enums.enum_types import KeyType
12
+ from ...models.requests import SubnetRegisterRequest
13
+ from ...ui.display import HTCLIConsole
14
+ from ...ui.prompts import confirm_prompt, integer_prompt, select_prompt, text_prompt
15
+ from ...utils import retrieve_wallet_with_validation
16
+ from ...utils.validation import (
17
+ validate_subnet_name,
18
+ validate_subnet_repo,
19
+ validate_subnet_min_stake,
20
+ validate_subnet_max_stake,
21
+ validate_subnet_delegate_stake_percentage,
22
+ validate_hotkey_address,
23
+ )
24
+
25
+
26
+ def prompt_get_subnet() -> int:
27
+ """Prompt for subnet ID."""
28
+ return integer_prompt(
29
+ "Enter the Subnet ID (UID)",
30
+ min_value=0,
31
+ )
32
+
33
+
34
+ def prompt_list_nodes() -> int:
35
+ """Prompt for subnet ID to list nodes from."""
36
+ return integer_prompt(
37
+ "Enter the Subnet ID (UID) to list nodes from",
38
+ min_value=0,
39
+ )
40
+
41
+
42
+ def prompt_check_activation() -> int:
43
+ """Prompt for subnet ID to check activation requirements for."""
44
+ console = HTCLIConsole()
45
+ console.print("\n[htcli.info]Check activation requirements for a subnet[/htcli.info]")
46
+ return integer_prompt(
47
+ "Enter the Subnet ID (UID) to check activation requirements",
48
+ min_value=0,
49
+ )
50
+
51
+
52
+ def prompt_activate_subnet() -> int:
53
+ """Prompt for subnet ID to activate."""
54
+ return integer_prompt(
55
+ "Enter the Subnet ID (UID) to activate",
56
+ min_value=0,
57
+ )
58
+
59
+
60
+ def prompt_pause_subnet() -> int:
61
+ """Prompt for subnet ID to pause."""
62
+ return integer_prompt(
63
+ "Enter the Subnet ID (UID) to pause",
64
+ min_value=0,
65
+ )
66
+
67
+
68
+ def prompt_unpause_subnet() -> int:
69
+ """Prompt for subnet ID to unpause."""
70
+ return integer_prompt(
71
+ "Enter the Subnet ID (UID) to unpause",
72
+ min_value=0,
73
+ )
74
+
75
+
76
+ def prompt_owner_update(
77
+ subnet_id: Optional[int] = None,
78
+ new_name: Optional[str] = None,
79
+ new_repo: Optional[str] = None,
80
+ ) -> tuple[int, Optional[str], Optional[str]]:
81
+ """Prompt for subnet ID and update fields."""
82
+ if subnet_id is None:
83
+ subnet_id = integer_prompt(
84
+ "Enter the Subnet ID (UID) to update",
85
+ min_value=0,
86
+ )
87
+
88
+ if new_name is None and new_repo is None:
89
+ # Ask what to update
90
+ update_options = []
91
+ if new_name is None:
92
+ update_options.append("name")
93
+ if new_repo is None:
94
+ update_options.append("repository URL")
95
+
96
+ console.print(info("What would you like to update?"))
97
+ for i, option in enumerate(update_options, 1):
98
+ console.print(f" {i}. {option}")
99
+
100
+ choice = integer_prompt(
101
+ f"Select option (1-{len(update_options)})",
102
+ min_value=1,
103
+ max_value=len(update_options),
104
+ )
105
+
106
+ if choice == 1 and "name" in update_options:
107
+ new_name = text_prompt("Enter the new name for the subnet")
108
+ elif choice == 2 and "repository URL" in update_options:
109
+ new_repo = text_prompt(
110
+ "Enter the new repository URL (optional)", default="", required=False
111
+ )
112
+ elif choice == 1 and "repository URL" in update_options:
113
+ new_repo = text_prompt(
114
+ "Enter the new repository URL (optional)", default="", required=False
115
+ )
116
+ else:
117
+ # Prompt for missing fields
118
+ if new_name is None:
119
+ new_name = text_prompt("Enter the new name for the subnet")
120
+ if new_repo is None:
121
+ new_repo = text_prompt(
122
+ "Enter the new repository URL (optional)", default="", required=False
123
+ )
124
+
125
+ return subnet_id, new_name, new_repo
126
+
127
+
128
+ def prompt_owner_update_name() -> tuple[int, str]:
129
+ """Prompt for subnet ID and new name."""
130
+ subnet_id = integer_prompt(
131
+ "Enter the Subnet ID (UID) to update",
132
+ min_value=0,
133
+ )
134
+ new_name = text_prompt(
135
+ "Enter the new name for the subnet",
136
+ )
137
+ return subnet_id, new_name
138
+
139
+
140
+ def prompt_owner_update_repo() -> tuple[int, str]:
141
+ """Prompt for subnet ID and new repo URL."""
142
+ subnet_id = integer_prompt(
143
+ "Enter the Subnet ID (UID) to update",
144
+ min_value=0,
145
+ )
146
+ new_repo = text_prompt(
147
+ "Enter the new repository URL (optional)",
148
+ default="",
149
+ required=False,
150
+ )
151
+ return subnet_id, new_repo
152
+
153
+
154
+ def prompt_owner_transfer() -> tuple[int, str]:
155
+ """Prompt for subnet ID and new owner (EVM address or wallet name)."""
156
+ subnet_id = integer_prompt(
157
+ "Enter the Subnet ID (UID) to transfer",
158
+ min_value=0,
159
+ )
160
+
161
+ new_owner = text_prompt(
162
+ "Enter the new owner's EVM address or wallet name (e.g., 0x... or wallet-name)",
163
+ required=True,
164
+ )
165
+
166
+ return subnet_id, new_owner
167
+
168
+
169
+ def prompt_owner_accept() -> int:
170
+ """Prompt for subnet ID to accept ownership of."""
171
+ return integer_prompt(
172
+ "Enter the Subnet ID (UID) to accept ownership for",
173
+ min_value=0,
174
+ )
175
+
176
+
177
+ def prompt_owner_remove() -> int:
178
+ """Prompt for subnet ID to remove."""
179
+ subnet_id = integer_prompt(
180
+ "Enter the Subnet ID (UID) to remove",
181
+ min_value=0,
182
+ )
183
+
184
+ # Use HTCLI confirmation prompt with warning
185
+ confirmed = confirm_prompt(
186
+ f"⚠️ Are you sure you want to permanently remove subnet {subnet_id}? This action cannot be undone.",
187
+ default=False,
188
+ )
189
+
190
+ if not confirmed:
191
+ raise ValueError("Subnet removal cancelled by user")
192
+
193
+ return subnet_id
194
+
195
+
196
+ def prompt_register_subnet(
197
+ name: Optional[str] = None,
198
+ repo: Optional[str] = None,
199
+ description: Optional[str] = None,
200
+ misc: Optional[str] = None,
201
+ min_stake: Optional[float] = None,
202
+ max_stake: Optional[float] = None,
203
+ max_cost: Optional[float] = None,
204
+ delegate_stake_percentage: Optional[int] = None,
205
+ initial_coldkeys: Optional[str] = None,
206
+ key_types: Optional[str] = None,
207
+ bootnodes: Optional[str] = None,
208
+ ) -> SubnetRegisterRequest:
209
+ """Iteratively prompt for subnet registration details."""
210
+ from ...ui.display import print_error
211
+
212
+ # STEP 1: Validate ALL provided parameters FIRST (before any client initialization)
213
+ # This ensures users see validation errors immediately, even if network is down
214
+ # Collect all validation errors before proceeding
215
+
216
+ validation_errors = []
217
+ # Validate name if provided
218
+ if name is not None:
219
+ # Handle empty string explicitly
220
+ if name == "" or (isinstance(name, str) and not name.strip()):
221
+ validation_errors.append("Invalid subnet name: Subnet name cannot be empty")
222
+ name = None
223
+ else:
224
+ is_valid, error_msg = validate_subnet_name(name)
225
+ if not is_valid:
226
+ validation_errors.append(f"Invalid subnet name: {error_msg}")
227
+ name = None
228
+
229
+ # Validate repo if provided
230
+ if repo is not None:
231
+ # Handle empty string explicitly
232
+ if repo == "" or (isinstance(repo, str) and not repo.strip()):
233
+ validation_errors.append("Invalid repository URL: Repository URL cannot be empty")
234
+ repo = None
235
+ else:
236
+ is_valid, error_msg = validate_subnet_repo(repo)
237
+ if not is_valid:
238
+ validation_errors.append(f"Invalid repository URL: {error_msg}")
239
+ repo = None
240
+
241
+ # Note: min_stake and max_stake validation is handled inline during prompting
242
+ # We don't add errors here since we'll reprompt interactively
243
+ # Just convert to None if invalid so they get reprompted
244
+ # This handles CLI-provided values that might be invalid
245
+ if min_stake is not None:
246
+ try:
247
+ # Convert TENSOR to WEI if it's a float
248
+ if isinstance(min_stake, float):
249
+ min_stake_wei = int(min_stake * 1e18)
250
+ is_valid, error_msg = validate_subnet_min_stake(min_stake_wei)
251
+ if not is_valid:
252
+ min_stake = None # Will be reprompted inline
253
+ else:
254
+ min_stake = min_stake_wei # Convert to int (wei) if valid
255
+ # If already in wei (int), validate as-is
256
+ elif isinstance(min_stake, int):
257
+ is_valid, error_msg = validate_subnet_min_stake(min_stake)
258
+ if not is_valid:
259
+ min_stake = None # Will be reprompted inline
260
+ except Exception:
261
+ min_stake = None # Will be reprompted inline
262
+
263
+ if max_stake is not None:
264
+ try:
265
+ # Convert TENSOR to WEI if it's a float
266
+ if isinstance(max_stake, float):
267
+ max_stake_wei = int(max_stake * 1e18)
268
+ # Note: We can't validate against min_stake here since it might not be set yet
269
+ # This validation will happen inline during prompting
270
+ is_valid, error_msg = validate_subnet_max_stake(max_stake_wei, None)
271
+ if not is_valid:
272
+ max_stake = None # Will be reprompted inline
273
+ else:
274
+ max_stake = max_stake_wei # Convert to int (wei) if valid
275
+ # If already in wei (int), validate as-is (without min_stake check)
276
+ elif isinstance(max_stake, int):
277
+ is_valid, error_msg = validate_subnet_max_stake(max_stake, None)
278
+ if not is_valid:
279
+ max_stake = None # Will be reprompted inline
280
+ except Exception:
281
+ max_stake = None # Will be reprompted inline
282
+
283
+ # Validate delegate_stake_percentage if provided (before prompting)
284
+ if delegate_stake_percentage is not None:
285
+ # If provided as percentage (0-100), convert to wei format for validation
286
+ if delegate_stake_percentage <= 100:
287
+ delegate_stake_percentage_wei = int(delegate_stake_percentage * 1e18 / 100)
288
+ else:
289
+ # Already in wei format
290
+ delegate_stake_percentage_wei = delegate_stake_percentage
291
+
292
+ is_valid, error_msg = validate_subnet_delegate_stake_percentage(delegate_stake_percentage_wei)
293
+ if not is_valid:
294
+ validation_errors.append(f"Invalid delegate stake percentage: {error_msg}")
295
+ delegate_stake_percentage = None
296
+ else:
297
+ delegate_stake_percentage = delegate_stake_percentage_wei
298
+
299
+ # Note: max_registered_nodes is NOT part of RegistrationSubnetData
300
+ # It's a subnet configuration field set separately after registration
301
+ # We skip validation here since it's not used in registration
302
+
303
+ # Display all validation errors at once
304
+ if validation_errors:
305
+ for error in validation_errors:
306
+ print_error(error)
307
+ print_error("Please correct the errors above. You will be prompted for valid values.")
308
+
309
+ # STEP 2: Now prompt for missing values (client-dependent operations happen here)
310
+
311
+
312
+ if name is None:
313
+ name = text_prompt("Enter subnet name")
314
+
315
+ if repo is None:
316
+ # Prompt with validation using a validator function
317
+ def repo_validator(value: str) -> bool:
318
+ """Validator function for repo URL."""
319
+ is_valid, _ = validate_subnet_repo(value)
320
+ return is_valid
321
+
322
+ repo = text_prompt(
323
+ "Enter repository URL",
324
+ required=True,
325
+ validator=repo_validator,
326
+ error_message="Repository URL must be a valid HTTP/HTTPS URL (e.g., https://github.com/user/repo)",
327
+ ).strip()
328
+ else:
329
+ repo = repo.strip()
330
+ # Validation already happened above in STEP 1, but ensure it's still valid
331
+ # (repo.strip() might have changed it)
332
+ is_valid, error_msg = validate_subnet_repo(repo)
333
+ if not is_valid:
334
+ print_error(f"Invalid repository URL: {error_msg}")
335
+ repo = None
336
+ # Re-prompt with validator
337
+ def repo_validator(value: str) -> bool:
338
+ """Validator function for repo URL."""
339
+ is_valid, _ = validate_subnet_repo(value)
340
+ return is_valid
341
+
342
+ repo = text_prompt(
343
+ "Enter repository URL",
344
+ required=True,
345
+ validator=repo_validator,
346
+ error_message="Repository URL must be a valid HTTP/HTTPS URL (e.g., https://github.com/user/repo)",
347
+ ).strip()
348
+
349
+ # Prompt for min_stake with inline validation and reprompting
350
+ if min_stake is None:
351
+ from ...ui.prompts import amount_prompt
352
+ from ...ui.display import print_error
353
+
354
+ while min_stake is None:
355
+ try:
356
+ # Use default from constants (100 TENSOR)
357
+ min_stake_tensor = amount_prompt(
358
+ "Enter minimum stake required for nodes (in TENSOR)",
359
+ currency="TENSOR",
360
+ default=100.0,
361
+ )
362
+ # Convert TENSOR to WEI
363
+ min_stake_wei = int(min_stake_tensor * 1e18)
364
+
365
+ # Validate immediately
366
+ is_valid, error_msg = validate_subnet_min_stake(min_stake_wei)
367
+ if not is_valid:
368
+ print_error(f"Invalid minimum stake ({min_stake_tensor} TENSOR): {error_msg}")
369
+ min_stake = None # Will reprompt
370
+ else:
371
+ min_stake = min_stake_wei
372
+ except (KeyboardInterrupt, typer.Abort):
373
+ raise
374
+ except Exception as e:
375
+ print_error(f"Invalid input: {str(e)}")
376
+ min_stake = None # Retry the prompt
377
+
378
+ # Prompt for max_stake with inline validation and reprompting
379
+ # Must validate against min_stake as well
380
+ if max_stake is None:
381
+ from ...ui.prompts import amount_prompt
382
+ from ...ui.display import print_error
383
+
384
+ while max_stake is None:
385
+ try:
386
+ # Use default from constants (1000 TENSOR)
387
+ max_stake_tensor = amount_prompt(
388
+ "Enter maximum stake allowed for nodes (in TENSOR)",
389
+ currency="TENSOR",
390
+ default=1000.0,
391
+ )
392
+ # Convert TENSOR to WEI
393
+ max_stake_wei = int(max_stake_tensor * 1e18)
394
+
395
+ # Validate immediately (check against min_stake if available)
396
+ is_valid, error_msg = validate_subnet_max_stake(max_stake_wei, min_stake)
397
+ if not is_valid:
398
+ print_error(f"Invalid maximum stake ({max_stake_tensor} TENSOR): {error_msg}")
399
+ max_stake = None # Will reprompt
400
+ else:
401
+ max_stake = max_stake_wei
402
+ except (KeyboardInterrupt, typer.Abort):
403
+ raise
404
+ except Exception as e:
405
+ print_error(f"Invalid input: {str(e)}")
406
+ max_stake = None # Retry the prompt
407
+ elif isinstance(max_stake, int):
408
+ # If provided via CLI and already validated (int/wei), validate against min_stake
409
+ is_valid, error_msg = validate_subnet_max_stake(max_stake, min_stake)
410
+ if not is_valid:
411
+ print_error(f"Invalid maximum stake: {error_msg}")
412
+ max_stake = None
413
+ # Reprompt
414
+ from ...ui.prompts import amount_prompt
415
+ from ...ui.display import print_error
416
+
417
+ while max_stake is None:
418
+ try:
419
+ max_stake_tensor = amount_prompt(
420
+ "Enter maximum stake allowed for nodes (in TENSOR)",
421
+ currency="TENSOR",
422
+ default=1000.0,
423
+ )
424
+ max_stake_wei = int(max_stake_tensor * 1e18)
425
+ is_valid, error_msg = validate_subnet_max_stake(max_stake_wei, min_stake)
426
+ if not is_valid:
427
+ print_error(f"Invalid maximum stake ({max_stake_tensor} TENSOR): {error_msg}")
428
+ max_stake = None
429
+ else:
430
+ max_stake = max_stake_wei
431
+ except (KeyboardInterrupt, typer.Abort):
432
+ raise
433
+ except Exception as e:
434
+ print_error(f"Invalid input: {str(e)}")
435
+ max_stake = None
436
+
437
+ if max_cost is None:
438
+ # Prompt for max cost in TENSOR (user-friendly)
439
+ from ...ui.prompts import amount_prompt
440
+
441
+ while max_cost is None:
442
+ try:
443
+ max_cost_tensor = amount_prompt(
444
+ "Enter maximum cost",
445
+ currency="TENSOR",
446
+ min_amount=0.0,
447
+ )
448
+ max_cost = int(max_cost_tensor * 1e18) # Convert to planck
449
+ except (KeyboardInterrupt, typer.Abort):
450
+ # Allow Ctrl+C to break out immediately instead of re-prompting
451
+ raise
452
+ except Exception as e:
453
+ from ...ui.display import print_error
454
+ print_error(f"Invalid input: {str(e)}")
455
+ max_cost = None # Retry the prompt
456
+ elif isinstance(max_cost, float):
457
+ # If provided via CLI as float, convert to wei
458
+ max_cost = int(max_cost * 1e18)
459
+
460
+ # Collect additional configuration - only prompt if not provided
461
+ if description is None:
462
+ description = text_prompt(
463
+ "Enter a short description",
464
+ default="",
465
+ required=False,
466
+ ) or "" # Ensure it's always a string, not None
467
+
468
+ if misc is None:
469
+ misc = text_prompt(
470
+ "Enter miscellaneous info (optional)",
471
+ default="",
472
+ required=False,
473
+ ) or "" # Ensure it's always a string, not None
474
+
475
+ # Note: churn_limit and other configuration fields are NOT part of RegistrationSubnetData
476
+ # They are set separately after registration using owner update calls
477
+ # We skip prompting for them here to match the blockchain structure
478
+
479
+ if delegate_stake_percentage is None:
480
+ while delegate_stake_percentage is None:
481
+ try:
482
+ delegate_stake_percentage_pct = integer_prompt(
483
+ "Enter delegate stake percentage (0-100)",
484
+ default=20,
485
+ min_value=0,
486
+ max_value=100,
487
+ )
488
+ # Convert to wei format
489
+ delegate_stake_percentage = int(delegate_stake_percentage_pct * 1e18 / 100)
490
+ except (ValueError, typer.Abort):
491
+ raise
492
+
493
+
494
+ # Collect initial coldkeys interactively if not provided
495
+ # parsed_initial_coldkeys can be either:
496
+ # - A list of addresses (legacy, will be converted to dict with default 1)
497
+ # - A dict mapping address -> max_registrations (new format)
498
+ parsed_initial_coldkeys = []
499
+ if initial_coldkeys:
500
+ # If provided via CLI, parse as comma-separated addresses
501
+ # We'll prompt for max_registrations for each address
502
+ addr_list = [
503
+ addr.strip() for addr in initial_coldkeys.split(",") if addr.strip()
504
+ ]
505
+
506
+ # Validate count immediately
507
+ if len(addr_list) < 3:
508
+ console = HTCLIConsole()
509
+ console.print(
510
+ f"\n[htcli.warning]❌ You provided {len(addr_list)} address(es), but need at least 3[/htcli.warning]"
511
+ )
512
+ console.print(
513
+ "[htcli.warning]⚠️ Subnet registration requires at least 3 initial coldkeys[/htcli.warning]"
514
+ )
515
+ raise typer.Exit(code=1)
516
+
517
+ # Prompt for number of nodes each coldkey can register
518
+ console = HTCLIConsole()
519
+ console.print(
520
+ "\n[htcli.info]💡 Node Registration Limits:[/htcli.info]"
521
+ )
522
+ console.print(
523
+ " • Specify how many nodes each coldkey can register during the registration period"
524
+ )
525
+ console.print(
526
+ " • Default is 1 node per coldkey (minimum required)"
527
+ )
528
+ console.print("")
529
+
530
+ parsed_initial_coldkeys = {}
531
+ for addr in addr_list:
532
+ # Show shortened address for display
533
+ addr_display = addr[:10] + "..." + addr[-8:] if len(addr) > 20 else addr
534
+
535
+ max_nodes = integer_prompt(
536
+ f"Max nodes for coldkey {addr_display}",
537
+ default=1,
538
+ min_value=1,
539
+ max_value=1000, # Reasonable upper limit
540
+ )
541
+ parsed_initial_coldkeys[addr] = max_nodes
542
+ else:
543
+ # Explain initial coldkeys (REQUIRED parameter)
544
+ console = HTCLIConsole()
545
+ console.print("\n[htcli.info]💡 Initial Coldkeys (Required):[/htcli.info]")
546
+ console.print(
547
+ " • [bold]Must provide at least 3 coldkeys[/bold] for subnet registration"
548
+ )
549
+ console.print(
550
+ " • These coldkeys can register nodes during the registration period"
551
+ )
552
+ console.print(
553
+ " • After activation, the subnet becomes [bold]permissionless[/bold] automatically"
554
+ )
555
+ console.print(
556
+ " • The whitelist is removed and anyone can register nodes"
557
+ )
558
+ console.print("")
559
+
560
+ # Ask user how to provide initial coldkeys
561
+ choice = select_prompt(
562
+ "How would you like to provide initial coldkeys?",
563
+ [
564
+ ("select", "Select from existing coldkey wallets (need 3+)"),
565
+ ("enter", "Enter addresses manually (need 3+)"),
566
+ ],
567
+ default="select",
568
+ )
569
+
570
+ if choice == "select":
571
+
572
+ wallet_manager = WalletManager()
573
+ # Show existing coldkey wallets in simple numbered list
574
+ try:
575
+ wallets_response = wallet_manager.list_wallets()
576
+ all_wallets = wallets_response.get("data", [])
577
+ coldkeys = [w for w in all_wallets if not w.get("is_hotkey", False)]
578
+ if coldkeys:
579
+ console = HTCLIConsole()
580
+ console.print("\n[bold]Available coldkey wallets:[/bold]")
581
+ for i, key in enumerate(coldkeys, 1):
582
+ name_color = "[bold cyan]"
583
+ address_color = "[cyan]"
584
+ encrypted_status = ""
585
+ if key.get("is_encrypted", False):
586
+ encrypted_status = " [dim](encrypted - password required)[/dim]"
587
+ console.print(
588
+ f"{i}. {name_color}{key['name']}[/]{encrypted_status} - {address_color}{key['ss58_address']}[/]"
589
+ )
590
+ console.print("")
591
+ except Exception:
592
+ # Non-fatal - continue to selection even if listing fails
593
+ pass
594
+ # Multi-select by indices (comma-separated), with fallback to interactive selector
595
+ wallet_selection_retry = True
596
+ while wallet_selection_retry:
597
+ try:
598
+ # Build index map for coldkeys
599
+ idx_to_wallet = (
600
+ {str(i + 1): w for i, w in enumerate(coldkeys)} if coldkeys else {}
601
+ )
602
+ if idx_to_wallet:
603
+ console.print(
604
+ "\n[htcli.warning]⚠️ You must select at least 3 coldkeys (required for consensus)[/htcli.warning]"
605
+ )
606
+ console.print(
607
+ "[htcli.info] Example: Enter '1,2,3' to select first three wallets[/htcli.info]\n"
608
+ )
609
+ selection = text_prompt(
610
+ "Select at least 3 coldkey indices (comma-separated, e.g. '1,2,3')",
611
+ required=True,
612
+ default="1,2,3", # Helpful default
613
+ ).strip()
614
+ else:
615
+ selection = ""
616
+
617
+ if selection:
618
+ indices = [s.strip() for s in selection.split(",") if s.strip()]
619
+ successful_selections = 0
620
+ failed_selections = []
621
+
622
+ # Store selected coldkeys info for prompting
623
+ selected_coldkeys_info = []
624
+ for idx in indices:
625
+ wallet = idx_to_wallet.get(idx)
626
+ if wallet:
627
+ addr = wallet.get("ss58_address")
628
+ if addr:
629
+ # Check if already added (avoid duplicates)
630
+ if not any(c["address"] == addr for c in selected_coldkeys_info):
631
+ selected_coldkeys_info.append({
632
+ "name": wallet.get("name", "Unknown"),
633
+ "address": addr,
634
+ })
635
+ successful_selections += 1
636
+
637
+ # Validate count after selection
638
+ if len(selected_coldkeys_info) < 3:
639
+ console.print(
640
+ f"\n[htcli.warning]❌ You successfully selected {len(selected_coldkeys_info)} coldkey(s), but need at least 3[/htcli.warning]"
641
+ )
642
+ if failed_selections:
643
+ console.print(
644
+ f"[htcli.warning] {len(failed_selections)} wallet(s) were skipped due to unlock failures[/htcli.warning]"
645
+ )
646
+ console.print(
647
+ "[htcli.warning]⚠️ Subnet registration requires at least 3 initial coldkeys[/htcli.warning]"
648
+ )
649
+ # Ask user to provide wallets again instead of exiting
650
+ from ...ui.prompts import confirm_prompt
651
+ if not confirm_prompt("Would you like to select wallets again?", default=True):
652
+ raise typer.Exit(code=1)
653
+ # Retry by continuing the while loop
654
+ continue
655
+
656
+ # If we get here, we have enough wallets - exit retry loop
657
+ wallet_selection_retry = False
658
+
659
+ # Prompt for number of nodes each coldkey can register
660
+ console.print(
661
+ "\n[htcli.info]💡 Node Registration Limits:[/htcli.info]"
662
+ )
663
+ console.print(
664
+ " • Specify how many nodes each coldkey can register during the registration period"
665
+ )
666
+ console.print(
667
+ " • Default is 1 node per coldkey (minimum required)"
668
+ )
669
+ console.print("")
670
+
671
+ # Prompt for max_registrations for each selected coldkey
672
+ parsed_initial_coldkeys = {}
673
+ for coldkey_info in selected_coldkeys_info:
674
+ wallet_name = coldkey_info["name"]
675
+ wallet_addr = coldkey_info["address"]
676
+
677
+ # Show shortened address for display
678
+ addr_display = wallet_addr[:10] + "..." + wallet_addr[-8:] if len(wallet_addr) > 20 else wallet_addr
679
+
680
+ max_nodes = integer_prompt(
681
+ f"Max nodes for {wallet_name} ({addr_display})",
682
+ default=1,
683
+ min_value=1,
684
+ max_value=1000, # Reasonable upper limit
685
+ )
686
+ parsed_initial_coldkeys[wallet_addr] = max_nodes
687
+ else:
688
+ console.print(
689
+ "[htcli.warning]⚠️ Unable to select coldkeys automatically. Please re-run with manual addresses (--initial-coldkeys) or ensure wallet list is available.[/htcli.warning]"
690
+ )
691
+ raise typer.Exit(code=1)
692
+ except typer.Exit:
693
+ raise
694
+ except Exception as e:
695
+ # If anything fails, check if we should retry
696
+ from ...ui.prompts import confirm_prompt
697
+ if confirm_prompt("An error occurred. Would you like to try selecting wallets again?", default=True):
698
+ continue
699
+ # If user doesn't want to retry, re-raise the exception
700
+ raise
701
+
702
+ elif choice == "create":
703
+ from ...dependencies import get_client
704
+ from ...utils.wallet.core import _create_new_wallet
705
+
706
+ client = get_client()
707
+ keep_creating = True
708
+ created_addresses = []
709
+ while keep_creating:
710
+ try:
711
+ name, keypair = _create_new_wallet(client, wallet_type="coldkey")
712
+ address = keypair.ss58_address
713
+ if address not in created_addresses:
714
+ created_addresses.append(address)
715
+ except Exception:
716
+ break
717
+ keep_creating = confirm_prompt(
718
+ "Create and add another coldkey?", default=False
719
+ )
720
+
721
+ # After creating wallets, prompt for max_registrations for each
722
+ if created_addresses:
723
+ # Validate count
724
+ if len(created_addresses) < 3:
725
+ console.print(
726
+ f"\n[htcli.warning]❌ You created {len(created_addresses)} coldkey(s), but need at least 3[/htcli.warning]"
727
+ )
728
+ console.print(
729
+ "[htcli.warning]⚠️ Subnet registration requires at least 3 initial coldkeys[/htcli.warning]"
730
+ )
731
+ raise typer.Exit(code=1)
732
+
733
+ console.print(
734
+ "\n[htcli.info]💡 Node Registration Limits:[/htcli.info]"
735
+ )
736
+ console.print(
737
+ " • Specify how many nodes each coldkey can register during the registration period"
738
+ )
739
+ console.print(
740
+ " • Default is 1 node per coldkey (minimum required)"
741
+ )
742
+ console.print("")
743
+
744
+ parsed_initial_coldkeys = {}
745
+ for addr in created_addresses:
746
+ # Get wallet name if available
747
+ wallet_info = None
748
+ try:
749
+ from ...utils.wallet.crypto import get_wallet_info_by_address
750
+ wallet_info = get_wallet_info_by_address(addr, is_hotkey=False)
751
+ except Exception:
752
+ pass
753
+
754
+ wallet_name = wallet_info.get("name", "Unknown") if wallet_info else "Unknown"
755
+ addr_display = addr[:10] + "..." + addr[-8:] if len(addr) > 20 else addr
756
+
757
+ max_nodes = integer_prompt(
758
+ f"Max nodes for {wallet_name} ({addr_display})",
759
+ default=1,
760
+ min_value=1,
761
+ max_value=1000,
762
+ )
763
+ parsed_initial_coldkeys[addr] = max_nodes
764
+ else:
765
+ # No wallets created, set empty dict (will fail validation)
766
+ parsed_initial_coldkeys = {}
767
+
768
+ elif choice == "enter":
769
+ console.print(
770
+ "\n[htcli.warning]⚠️ Remember: You need at least 3 addresses for consensus[/htcli.warning]"
771
+ )
772
+ addr_text = text_prompt(
773
+ "Enter at least 3 coldkey addresses (comma-separated)", required=True
774
+ )
775
+ addr_list = [
776
+ a.strip() for a in addr_text.split(",") if a.strip()
777
+ ]
778
+ # Validate count immediately
779
+ if len(addr_list) < 3:
780
+ console.print(
781
+ f"[htcli.warning]❌ You entered {len(addr_list)} address(es), but need at least 3[/htcli.warning]"
782
+ )
783
+ console.print(
784
+ "[htcli.warning]⚠️ Subnet registration requires at least 3 initial coldkeys[/htcli.warning]"
785
+ )
786
+ raise typer.Exit(code=1)
787
+
788
+ # Prompt for number of nodes each coldkey can register
789
+ console.print(
790
+ "\n[htcli.info]💡 Node Registration Limits:[/htcli.info]"
791
+ )
792
+ console.print(
793
+ " • Specify how many nodes each coldkey can register during the registration period"
794
+ )
795
+ console.print(
796
+ " • Default is 1 node per coldkey (minimum required)"
797
+ )
798
+ console.print("")
799
+
800
+ parsed_initial_coldkeys = {}
801
+ for addr in addr_list:
802
+ # Show shortened address for display
803
+ addr_display = addr[:10] + "..." + addr[-8:] if len(addr) > 20 else addr
804
+
805
+ max_nodes = integer_prompt(
806
+ f"Max nodes for coldkey {addr_display}",
807
+ default=1,
808
+ min_value=1,
809
+ max_value=1000, # Reasonable upper limit
810
+ )
811
+ parsed_initial_coldkeys[addr] = max_nodes
812
+ else:
813
+ # No valid choice - should not happen with select_prompt
814
+ console.print(
815
+ "[htcli.warning]❌ Invalid choice. Subnet registration requires at least 3 initial coldkeys.[/htcli.warning]"
816
+ )
817
+ raise typer.Exit(code=1)
818
+
819
+ # If initial coldkeys are provided and key_types missing, infer sensible default
820
+ # Check if parsed_initial_coldkeys is dict or list
821
+ coldkey_count = len(parsed_initial_coldkeys) if parsed_initial_coldkeys else 0
822
+ if coldkey_count > 0 and not key_types:
823
+ key_types = "Ecdsa" # This will be parsed to KeyType.ECDSA later
824
+
825
+ # Collect bootnodes if provided or interactively
826
+ parsed_bootnodes: list[str] = []
827
+ if bootnodes:
828
+ # Filter out None and empty values, ensure all are strings
829
+ parsed_bootnodes = [
830
+ b.strip() for b in bootnodes.split(",")
831
+ if b and isinstance(b, str) and b.strip()
832
+ ]
833
+ else:
834
+ def _get_default_bootnodes() -> list[str]:
835
+ default_bootnodes: list[str] = []
836
+
837
+
838
+
839
+ if not default_bootnodes:
840
+ default_bootnodes = [
841
+ "/ip4/127.0.0.1/tcp/31330/p2p/QmShJYgxNoKn7xqdRQj5PBcNfPSsbWkgFBPA4mK5PH73JC",
842
+ "/ip4/127.0.0.1/tcp/31330/p2p/QmbRz8Bt1pMcVnUzVQpL2icveZz2MF7VtELC44v8kVNwiH",
843
+ "/ip4/127.0.0.1/tcp/31330/p2p/QmTJ8uyLJBwVprejUQfYFAywdXWfdnUQbC1Xif6QiTNta1",
844
+ ]
845
+
846
+ return default_bootnodes
847
+
848
+ default_bootnodes = _get_default_bootnodes()
849
+
850
+ from ...ui.display import print_info
851
+
852
+ console = HTCLIConsole()
853
+ console.print("\n[htcli.info]Default Hypertensor bootnodes:[/htcli.info]")
854
+ for addr in default_bootnodes:
855
+ console.print(f" • [htcli.value]{addr}[/htcli.value]")
856
+ console.print("")
857
+
858
+ bootnode_choice = select_prompt(
859
+ "How would you like to provide bootnodes?",
860
+ [
861
+ ("default", "Use default Hypertensor bootnodes"),
862
+ ("custom", "Enter bootnodes manually (comma-separated)"),
863
+ ],
864
+ default="default",
865
+ )
866
+
867
+ if bootnode_choice == "custom":
868
+ boot_text = text_prompt(
869
+ "Enter bootnode multiaddrs (comma-separated)", required=True
870
+ )
871
+ # Filter out None and empty values, ensure all are strings
872
+ parsed_bootnodes = [
873
+ b.strip() for b in boot_text.split(",")
874
+ if b and isinstance(b, str) and b.strip()
875
+ ]
876
+ print_info("Using custom bootnodes.")
877
+ else:
878
+ # Ensure default_bootnodes are all strings (should already be, but be safe)
879
+ parsed_bootnodes = [b for b in default_bootnodes if b and isinstance(b, str)]
880
+ print_info(
881
+ "Using default Hypertensor bootnodes from subnet template.\n"
882
+ "Update the subnet bootnodes after registration if needed."
883
+ )
884
+
885
+ # Set defaults for optional parameters if not provided
886
+ if key_types is None:
887
+ key_types = "Ed25519"
888
+
889
+ # Parse key types if provided and convert to KeyType enum values
890
+ parsed_key_types = set()
891
+ if key_types:
892
+ # Split comma-separated key types and strip whitespace
893
+ key_type_strings = [kt.strip() for kt in key_types.split(",") if kt.strip()]
894
+
895
+ # Map string names to KeyType enum values (case-insensitive)
896
+ key_type_map = {
897
+ "rsa": KeyType.RSA,
898
+ "ed25519": KeyType.ED25519,
899
+ "secp256k1": KeyType.SECP256K1,
900
+ "ecdsa": KeyType.ECDSA,
901
+ }
902
+
903
+ for kt_str in key_type_strings:
904
+ kt_lower = kt_str.lower()
905
+ if kt_lower in key_type_map:
906
+ parsed_key_types.add(key_type_map[kt_lower])
907
+ else:
908
+ # If invalid key type, show warning and skip
909
+ from ...ui.display import print_warning
910
+
911
+ print_warning(f"Unknown key type '{kt_str}', skipping...")
912
+ else:
913
+ # Default to Ed25519 if no key types specified
914
+ parsed_key_types = {KeyType.ED25519}
915
+
916
+ # Validate that we have at least 3 initial coldkeys (blockchain requirement)
917
+ # parsed_initial_coldkeys is now a dict {address: max_registrations} or a list [addresses]
918
+ # Handle both cases for backward compatibility
919
+ if isinstance(parsed_initial_coldkeys, dict):
920
+ initial_coldkeys_dict = parsed_initial_coldkeys
921
+ coldkey_count = len(initial_coldkeys_dict)
922
+ else:
923
+ # Legacy: list of addresses - convert to dict with default of 1
924
+ initial_coldkeys_dict = {addr: 1 for addr in parsed_initial_coldkeys}
925
+ coldkey_count = len(parsed_initial_coldkeys)
926
+
927
+ if coldkey_count < 3:
928
+ console = HTCLIConsole()
929
+ console.print(
930
+ f"\n[htcli.warning]❌ Subnet registration requires at least 3 initial coldkeys, but only {coldkey_count} provided[/htcli.warning]"
931
+ )
932
+ console.print(
933
+ "[htcli.info]💡 After activation, the subnet becomes permissionless automatically[/htcli.info]"
934
+ )
935
+ raise typer.Exit(code=1)
936
+
937
+ # Ensure all values are at least 1 (blockchain requirement)
938
+ initial_coldkeys_dict = {
939
+ addr: max(max_registrations, 1)
940
+ for addr, max_registrations in initial_coldkeys_dict.items()
941
+ }
942
+
943
+ # delegate_stake_percentage is already in wei format from validation above
944
+ # If it wasn't provided, use default
945
+ if delegate_stake_percentage is None:
946
+ delegate_stake_percentage_wei = 100_000_000_000_000_000 # Default 10%
947
+ else:
948
+ delegate_stake_percentage_wei = delegate_stake_percentage
949
+
950
+ bootnodes_set = {
951
+ bn for bn in parsed_bootnodes if bn is not None and isinstance(bn, str) and bn.strip()
952
+ }
953
+
954
+ # Note: Only include fields that are part of RegistrationSubnetData
955
+ # Other fields (churn_limit, queue_epochs, etc.) are set separately after registration
956
+ # Ensure description and misc are always strings (not None)
957
+ return SubnetRegisterRequest(
958
+ name=name,
959
+ repo=repo,
960
+ description=description or "",
961
+ misc=misc or "",
962
+ min_stake=min_stake,
963
+ max_stake=max_stake,
964
+ max_cost=max_cost,
965
+ delegate_stake_percentage=delegate_stake_percentage_wei,
966
+ initial_coldkeys=initial_coldkeys_dict,
967
+ key_types=parsed_key_types,
968
+ # Filter out None values and ensure all are strings before creating set
969
+ bootnodes=bootnodes_set, # Ensure bootnodes is never None and contains only valid strings
970
+ )
971
+
972
+
973
+ def prompt_owner_update_extended(
974
+ subnet_id: Optional[int] = None,
975
+ new_name: Optional[str] = None,
976
+ new_repo: Optional[str] = None,
977
+ target_node_registrations: Optional[int] = None,
978
+ node_burn_rate_alpha: Optional[int] = None,
979
+ queue_immunity_epochs: Optional[int] = None,
980
+ min_weight_decrease_threshold: Optional[int] = None,
981
+ min_node_reputation: Optional[int] = None,
982
+ absent_reputation_penalty: Optional[int] = None,
983
+ included_reputation_boost: Optional[int] = None,
984
+ below_min_weight_penalty: Optional[int] = None,
985
+ non_attestor_penalty: Optional[int] = None,
986
+ validator_absent_penalty: Optional[int] = None,
987
+ validator_non_consensus_penalty: Optional[int] = None,
988
+ non_consensus_attestor_penalty: Optional[int] = None,
989
+ ) -> dict:
990
+ """Prompt for extended subnet update options in a config-edit style.
991
+
992
+ For each configurable field, show a prompt where the user can type a new
993
+ value or press enter to skip. Only fields with values entered are returned.
994
+ """
995
+ if subnet_id is None:
996
+ subnet_id = integer_prompt("Enter the Subnet ID (UID) to update", min_value=0)
997
+
998
+ console_instance = HTCLIConsole()
999
+ console_instance.print(
1000
+ info(
1001
+ "Subnet update (leave a field blank to keep its current value)."
1002
+ )
1003
+ )
1004
+
1005
+ result: dict = {"subnet_id": subnet_id}
1006
+
1007
+ def prompt_optional_int(message: str) -> Optional[int]:
1008
+ raw = text_prompt(message, default="", required=False)
1009
+ if raw is None:
1010
+ return None
1011
+ raw = raw.strip()
1012
+ if raw == "":
1013
+ return None
1014
+ try:
1015
+ return int(raw)
1016
+ except ValueError:
1017
+ from ...ui.display import print_error
1018
+
1019
+ print_error("Invalid integer, skipping this field.")
1020
+ return None
1021
+
1022
+ # Name
1023
+ if new_name is not None:
1024
+ result["new_name"] = new_name
1025
+ else:
1026
+ name_input = text_prompt(
1027
+ "New subnet name (leave blank to keep current)", default="", required=False
1028
+ )
1029
+ if name_input and name_input.strip():
1030
+ result["new_name"] = name_input.strip()
1031
+
1032
+ # Repo
1033
+ if new_repo is not None:
1034
+ result["new_repo"] = new_repo
1035
+ else:
1036
+ repo_input = text_prompt(
1037
+ "New repository URL (leave blank to keep current)",
1038
+ default="",
1039
+ required=False,
1040
+ )
1041
+ if repo_input and repo_input.strip():
1042
+ result["new_repo"] = repo_input.strip()
1043
+
1044
+ # Numeric config fields
1045
+ if target_node_registrations is not None:
1046
+ result["target_node_registrations"] = target_node_registrations
1047
+ else:
1048
+ value = prompt_optional_int(
1049
+ "Target node registrations per epoch (blank to keep current)"
1050
+ )
1051
+ if value is not None:
1052
+ result["target_node_registrations"] = value
1053
+
1054
+ if node_burn_rate_alpha is not None:
1055
+ result["node_burn_rate_alpha"] = node_burn_rate_alpha
1056
+ else:
1057
+ value = prompt_optional_int(
1058
+ "Node burn rate alpha (blank to keep current)"
1059
+ )
1060
+ if value is not None:
1061
+ result["node_burn_rate_alpha"] = value
1062
+
1063
+ if queue_immunity_epochs is not None:
1064
+ result["queue_immunity_epochs"] = queue_immunity_epochs
1065
+ else:
1066
+ value = prompt_optional_int(
1067
+ "Queue immunity epochs (blank to keep current)"
1068
+ )
1069
+ if value is not None:
1070
+ result["queue_immunity_epochs"] = value
1071
+
1072
+ if min_weight_decrease_threshold is not None:
1073
+ result["min_weight_decrease_threshold"] = min_weight_decrease_threshold
1074
+ else:
1075
+ value = prompt_optional_int(
1076
+ "Min weight decrease threshold (blank to keep current)"
1077
+ )
1078
+ if value is not None:
1079
+ result["min_weight_decrease_threshold"] = value
1080
+
1081
+ if min_node_reputation is not None:
1082
+ result["min_node_reputation"] = min_node_reputation
1083
+ else:
1084
+ value = prompt_optional_int(
1085
+ "Min node reputation (blank to keep current)"
1086
+ )
1087
+ if value is not None:
1088
+ result["min_node_reputation"] = value
1089
+
1090
+ if absent_reputation_penalty is not None:
1091
+ result["absent_reputation_penalty"] = absent_reputation_penalty
1092
+ else:
1093
+ value = prompt_optional_int(
1094
+ "Absent reputation penalty (blank to keep current)"
1095
+ )
1096
+ if value is not None:
1097
+ result["absent_reputation_penalty"] = value
1098
+
1099
+ if included_reputation_boost is not None:
1100
+ result["included_reputation_boost"] = included_reputation_boost
1101
+ else:
1102
+ value = prompt_optional_int(
1103
+ "Included reputation boost (blank to keep current)"
1104
+ )
1105
+ if value is not None:
1106
+ result["included_reputation_boost"] = value
1107
+
1108
+ if below_min_weight_penalty is not None:
1109
+ result["below_min_weight_penalty"] = below_min_weight_penalty
1110
+ else:
1111
+ value = prompt_optional_int(
1112
+ "Below-min weight penalty (blank to keep current)"
1113
+ )
1114
+ if value is not None:
1115
+ result["below_min_weight_penalty"] = value
1116
+
1117
+ if non_attestor_penalty is not None:
1118
+ result["non_attestor_penalty"] = non_attestor_penalty
1119
+ else:
1120
+ value = prompt_optional_int(
1121
+ "Non-attestor penalty (blank to keep current)"
1122
+ )
1123
+ if value is not None:
1124
+ result["non_attestor_penalty"] = value
1125
+
1126
+ if validator_absent_penalty is not None:
1127
+ result["validator_absent_penalty"] = validator_absent_penalty
1128
+ else:
1129
+ value = prompt_optional_int(
1130
+ "Validator absent penalty (blank to keep current)"
1131
+ )
1132
+ if value is not None:
1133
+ result["validator_absent_penalty"] = value
1134
+
1135
+ if validator_non_consensus_penalty is not None:
1136
+ result["validator_non_consensus_penalty"] = validator_non_consensus_penalty
1137
+ else:
1138
+ value = prompt_optional_int(
1139
+ "Validator non-consensus penalty (blank to keep current)"
1140
+ )
1141
+ if value is not None:
1142
+ result["validator_non_consensus_penalty"] = value
1143
+
1144
+ if non_consensus_attestor_penalty is not None:
1145
+ result["non_consensus_attestor_penalty"] = non_consensus_attestor_penalty
1146
+ else:
1147
+ value = prompt_optional_int(
1148
+ "Non-consensus attestor penalty (blank to keep current)"
1149
+ )
1150
+ if value is not None:
1151
+ result["non_consensus_attestor_penalty"] = value
1152
+
1153
+ # Fill in other fields from parameters without overwriting prompted values
1154
+ if "new_name" not in result and new_name is not None:
1155
+ result["new_name"] = new_name
1156
+ if "new_repo" not in result and new_repo is not None:
1157
+ result["new_repo"] = new_repo
1158
+ if "target_node_registrations" not in result and target_node_registrations is not None:
1159
+ result["target_node_registrations"] = target_node_registrations
1160
+ if "node_burn_rate_alpha" not in result and node_burn_rate_alpha is not None:
1161
+ result["node_burn_rate_alpha"] = node_burn_rate_alpha
1162
+ if "queue_immunity_epochs" not in result and queue_immunity_epochs is not None:
1163
+ result["queue_immunity_epochs"] = queue_immunity_epochs
1164
+ if "min_weight_decrease_threshold" not in result and min_weight_decrease_threshold is not None:
1165
+ result["min_weight_decrease_threshold"] = min_weight_decrease_threshold
1166
+ if "min_node_reputation" not in result and min_node_reputation is not None:
1167
+ result["min_node_reputation"] = min_node_reputation
1168
+ if "absent_reputation_penalty" not in result and absent_reputation_penalty is not None:
1169
+ result["absent_reputation_penalty"] = absent_reputation_penalty
1170
+ if "included_reputation_boost" not in result and included_reputation_boost is not None:
1171
+ result["included_reputation_boost"] = included_reputation_boost
1172
+ if "below_min_weight_penalty" not in result and below_min_weight_penalty is not None:
1173
+ result["below_min_weight_penalty"] = below_min_weight_penalty
1174
+ if "non_attestor_penalty" not in result and non_attestor_penalty is not None:
1175
+ result["non_attestor_penalty"] = non_attestor_penalty
1176
+ if "validator_absent_penalty" not in result and validator_absent_penalty is not None:
1177
+ result["validator_absent_penalty"] = validator_absent_penalty
1178
+ if (
1179
+ "validator_non_consensus_penalty" not in result
1180
+ and validator_non_consensus_penalty is not None
1181
+ ):
1182
+ result["validator_non_consensus_penalty"] = validator_non_consensus_penalty
1183
+ if (
1184
+ "non_consensus_attestor_penalty" not in result
1185
+ and non_consensus_attestor_penalty is not None
1186
+ ):
1187
+ result["non_consensus_attestor_penalty"] = non_consensus_attestor_penalty
1188
+
1189
+ return result
1190
+
1191
+
1192
+ def prompt_set_emergency_validator_set(
1193
+ subnet_id: Optional[int] = None, node_ids: Optional[str] = None
1194
+ ) -> tuple[int, str]:
1195
+ """Prompt for emergency validator set parameters."""
1196
+ if subnet_id is None:
1197
+ subnet_id = integer_prompt("Enter the Subnet ID (UID)", min_value=0)
1198
+ if node_ids is None:
1199
+ node_ids = text_prompt(
1200
+ "Enter comma-separated list of subnet node IDs for emergency validator set",
1201
+ required=True,
1202
+ )
1203
+ return subnet_id, node_ids
1204
+
1205
+
1206
+ def prompt_clear_emergency_validator_set() -> int:
1207
+ """Prompt for clearing emergency validator set."""
1208
+ return integer_prompt("Enter the Subnet ID (UID) to clear emergency validator set", min_value=0)
1209
+
1210
+
1211
+ def prompt_bootnode_access(
1212
+ subnet_id: Optional[int] = None,
1213
+ account: Optional[str] = None,
1214
+ action: str = "add",
1215
+ ) -> tuple[int, str]:
1216
+ """Prompt for bootnode access parameters."""
1217
+ if subnet_id is None:
1218
+ subnet_id = integer_prompt("Enter the Subnet ID (UID)", min_value=0)
1219
+ if account is None:
1220
+ action_label = "grant" if action == "add" else "revoke"
1221
+ account = text_prompt(
1222
+ f"Enter account address to {action_label} bootnode access to",
1223
+ required=True,
1224
+ )
1225
+ return subnet_id, account