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