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,346 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Node command prompting logic.
|
|
3
|
+
|
|
4
|
+
Handles user interaction and input validation for node operations.
|
|
5
|
+
Uses HTCLI UI components and Pydantic models for proper validation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import secrets
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from ...dependencies import get_client
|
|
12
|
+
from ...models.requests import SubnetNodeRegisterRequest
|
|
13
|
+
from ...ui.colors import info
|
|
14
|
+
from ...ui.display import HTCLIConsole, print_error
|
|
15
|
+
from ...ui.prompts import HTCLIPrompt, confirm_prompt
|
|
16
|
+
from ...utils.blockchain import generate_test_peer_id
|
|
17
|
+
from ...utils.blockchain.peer_id import validate_peer_id_format
|
|
18
|
+
from ...utils.validation import validate_stake_amount_prompt, validate_subnet_id_prompt
|
|
19
|
+
from ...utils.wallet.crypto import format_address_display
|
|
20
|
+
|
|
21
|
+
console = HTCLIConsole()
|
|
22
|
+
prompt = HTCLIPrompt()
|
|
23
|
+
|
|
24
|
+
DELEGATE_RATE_SCALE = 10**18
|
|
25
|
+
MIN_SCALED_DELEGATE_RATE = 10**12
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def normalize_delegate_reward_rate(value: int) -> int:
|
|
29
|
+
"""Normalize CLI delegate reward rate input to the chain's 1e18 scale."""
|
|
30
|
+
if value < 0:
|
|
31
|
+
raise ValueError("Delegate reward rate must be non-negative")
|
|
32
|
+
if value <= 100:
|
|
33
|
+
return int(value * DELEGATE_RATE_SCALE / 100)
|
|
34
|
+
if MIN_SCALED_DELEGATE_RATE <= value <= DELEGATE_RATE_SCALE:
|
|
35
|
+
return value
|
|
36
|
+
raise ValueError(
|
|
37
|
+
"Delegate reward rate must be 0-100 percent or a 1e18-format value"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def prompt_node_register(
|
|
42
|
+
subnet_id: Optional[int] = None,
|
|
43
|
+
validator_id: Optional[int] = None,
|
|
44
|
+
hotkey: Optional[str] = None,
|
|
45
|
+
stake_amount: Optional[float] = None,
|
|
46
|
+
peer_id: Optional[str] = None,
|
|
47
|
+
bootnode_peer_id: Optional[str] = None,
|
|
48
|
+
client_peer_id: Optional[str] = None,
|
|
49
|
+
coldkey_name: Optional[str] = None,
|
|
50
|
+
) -> SubnetNodeRegisterRequest:
|
|
51
|
+
"""Collect node registration parameters from user."""
|
|
52
|
+
|
|
53
|
+
console.print(info("Node Registration"))
|
|
54
|
+
console.print("Register a new node on an existing subnet.\n")
|
|
55
|
+
|
|
56
|
+
# Validate subnet_id if provided
|
|
57
|
+
if subnet_id is not None:
|
|
58
|
+
is_valid, error_msg = validate_subnet_id_prompt(subnet_id)
|
|
59
|
+
if not is_valid:
|
|
60
|
+
print_error(f"Invalid subnet ID: {error_msg}")
|
|
61
|
+
subnet_id = None
|
|
62
|
+
|
|
63
|
+
# Get subnet ID
|
|
64
|
+
if subnet_id is None:
|
|
65
|
+
subnet_id = prompt.integer_prompt(
|
|
66
|
+
"Enter the Subnet ID to register on",
|
|
67
|
+
min_value=0,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Validate validator_id if provided
|
|
71
|
+
if validator_id is not None and (
|
|
72
|
+
not isinstance(validator_id, int) or validator_id < 0
|
|
73
|
+
):
|
|
74
|
+
print_error("Invalid validator ID: must be a non-negative integer")
|
|
75
|
+
validator_id = None
|
|
76
|
+
|
|
77
|
+
# Get validator ID
|
|
78
|
+
if validator_id is None:
|
|
79
|
+
validator_id = prompt.integer_prompt(
|
|
80
|
+
"Enter the Validator ID that owns this node",
|
|
81
|
+
min_value=0,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
client = get_client()
|
|
85
|
+
|
|
86
|
+
# Get coldkey address if provided (for hotkey disambiguation)
|
|
87
|
+
coldkey_address = None
|
|
88
|
+
if coldkey_name:
|
|
89
|
+
try:
|
|
90
|
+
from ...utils.wallet.crypto import get_wallet_info_by_name
|
|
91
|
+
coldkey_info = get_wallet_info_by_name(coldkey_name, is_hotkey=False)
|
|
92
|
+
coldkey_address = coldkey_info.get("evm_address") or coldkey_info.get("ss58_address") or coldkey_info.get("address")
|
|
93
|
+
except Exception:
|
|
94
|
+
# Coldkey not found yet, will be resolved later - that's okay
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
# Get hotkey - try to resolve first (handles both wallet names and addresses)
|
|
98
|
+
# Use coldkey context for disambiguation if provided
|
|
99
|
+
if hotkey is not None:
|
|
100
|
+
try:
|
|
101
|
+
# Try to resolve the hotkey address with coldkey context for disambiguation
|
|
102
|
+
hotkey = client.offchain.wallet.resolve_hotkey_address(
|
|
103
|
+
hotkey,
|
|
104
|
+
owner_coldkey_name=coldkey_name,
|
|
105
|
+
owner_address=coldkey_address,
|
|
106
|
+
)
|
|
107
|
+
console.print(f"[htcli.success]✅ Using hotkey: {format_address_display(hotkey, truncate=False)}[/]")
|
|
108
|
+
except (ValueError, Exception) as e:
|
|
109
|
+
print_error(f"Invalid hotkey: {str(e)}")
|
|
110
|
+
hotkey = None
|
|
111
|
+
|
|
112
|
+
# Get hotkey if not provided or resolution failed
|
|
113
|
+
if hotkey is None:
|
|
114
|
+
from ...utils import retrieve_wallet_with_validation
|
|
115
|
+
from ...utils.wallet.crypto import get_wallet_info_by_name
|
|
116
|
+
|
|
117
|
+
console.print(info("\nHotkey Selection (used for node operations):"))
|
|
118
|
+
hotkey_wallet, hotkey_keypair = retrieve_wallet_with_validation(
|
|
119
|
+
wallet_type="hotkey", purpose="register the node"
|
|
120
|
+
)
|
|
121
|
+
# For ECDSA keys, compute EVM address directly from the keypair's public key
|
|
122
|
+
# This is more reliable than looking up wallet info with potentially mismatched owner params
|
|
123
|
+
# (the selected hotkey might have a different owner than the coldkey_name passed to this function)
|
|
124
|
+
from ...utils.wallet.crypto import public_key_to_evm_address
|
|
125
|
+
|
|
126
|
+
if hotkey_keypair is not None and hotkey_keypair.crypto_type == 2: # ECDSA
|
|
127
|
+
# Compute EVM address directly from public key - same logic as save_hotkey
|
|
128
|
+
hotkey = public_key_to_evm_address(hotkey_keypair.public_key)
|
|
129
|
+
elif hotkey_keypair is not None:
|
|
130
|
+
# For other key types, use ss58_address
|
|
131
|
+
hotkey = hotkey_keypair.ss58_address
|
|
132
|
+
else:
|
|
133
|
+
# Fallback: try to get wallet info without owner disambiguation
|
|
134
|
+
# (the hotkey we just selected/created may have a different owner)
|
|
135
|
+
hotkey_wallet_info = get_wallet_info_by_name(
|
|
136
|
+
hotkey_wallet,
|
|
137
|
+
is_hotkey=True,
|
|
138
|
+
)
|
|
139
|
+
hotkey = hotkey_wallet_info.get("evm_address") or hotkey_wallet_info.get("address") or hotkey_wallet_info.get("ss58_address")
|
|
140
|
+
|
|
141
|
+
if not hotkey:
|
|
142
|
+
raise ValueError(f"Could not determine address for hotkey wallet '{hotkey_wallet}'")
|
|
143
|
+
console.print(f"[htcli.success]✅ Using hotkey: {format_address_display(hotkey, truncate=False)}[/]")
|
|
144
|
+
|
|
145
|
+
# Validate stake_amount if provided (stake_amount is in TENSOR)
|
|
146
|
+
if stake_amount is not None:
|
|
147
|
+
# Convert to float if it's an integer (treat integers as TENSOR)
|
|
148
|
+
if isinstance(stake_amount, int):
|
|
149
|
+
stake_amount = float(stake_amount)
|
|
150
|
+
is_valid, error_msg = validate_stake_amount_prompt(stake_amount)
|
|
151
|
+
if not is_valid:
|
|
152
|
+
print_error(f"Invalid stake amount: {error_msg}")
|
|
153
|
+
stake_amount = None
|
|
154
|
+
|
|
155
|
+
# Get stake amount
|
|
156
|
+
if stake_amount is None:
|
|
157
|
+
from ...ui.prompts import amount_prompt
|
|
158
|
+
|
|
159
|
+
stake_amount = amount_prompt(
|
|
160
|
+
"Enter stake amount",
|
|
161
|
+
currency="TENSOR",
|
|
162
|
+
min_amount=1.0, # Minimum 1 TENSOR
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Auto-generate peer IDs if not provided
|
|
166
|
+
if peer_id is None or bootnode_peer_id is None or client_peer_id is None:
|
|
167
|
+
auto_generate = confirm_prompt("Auto-generate peer IDs?", default=True)
|
|
168
|
+
|
|
169
|
+
if auto_generate:
|
|
170
|
+
generated_peer_ids: set[str] = set()
|
|
171
|
+
|
|
172
|
+
def next_peer_id(current: Optional[str]) -> str:
|
|
173
|
+
if current:
|
|
174
|
+
generated_peer_ids.add(current)
|
|
175
|
+
return current
|
|
176
|
+
while True:
|
|
177
|
+
seed = secrets.randbits(32)
|
|
178
|
+
candidate = generate_test_peer_id(seed)
|
|
179
|
+
if candidate not in generated_peer_ids:
|
|
180
|
+
generated_peer_ids.add(candidate)
|
|
181
|
+
return candidate
|
|
182
|
+
|
|
183
|
+
if peer_id is None:
|
|
184
|
+
peer_id = next_peer_id(peer_id)
|
|
185
|
+
console.print(f"[htcli.info]Generated peer_id: {peer_id[:50]}...[/]")
|
|
186
|
+
else:
|
|
187
|
+
generated_peer_ids.add(peer_id)
|
|
188
|
+
|
|
189
|
+
if bootnode_peer_id is None:
|
|
190
|
+
bootnode_peer_id = next_peer_id(bootnode_peer_id)
|
|
191
|
+
console.print(
|
|
192
|
+
f"[htcli.info]Generated bootnode_peer_id: {bootnode_peer_id[:50]}...[/]"
|
|
193
|
+
)
|
|
194
|
+
else:
|
|
195
|
+
generated_peer_ids.add(bootnode_peer_id)
|
|
196
|
+
|
|
197
|
+
if client_peer_id is None:
|
|
198
|
+
client_peer_id = next_peer_id(client_peer_id)
|
|
199
|
+
console.print(
|
|
200
|
+
f"[htcli.info]Generated client_peer_id: {client_peer_id[:50]}...[/]"
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
# Manual entry
|
|
204
|
+
if peer_id is None:
|
|
205
|
+
peer_id = prompt.text_prompt("Enter peer_id")
|
|
206
|
+
if bootnode_peer_id is None:
|
|
207
|
+
bootnode_peer_id = prompt.text_prompt("Enter bootnode_peer_id")
|
|
208
|
+
if client_peer_id is None:
|
|
209
|
+
client_peer_id = prompt.text_prompt("Enter client_peer_id")
|
|
210
|
+
|
|
211
|
+
# Convert TENSOR to WEI for the request model (which expects WEI)
|
|
212
|
+
stake_amount_wei = int(stake_amount * 1e18)
|
|
213
|
+
|
|
214
|
+
# Set max_burn_amount (use stake amount as default, also in WEI)
|
|
215
|
+
max_burn_amount = stake_amount_wei
|
|
216
|
+
|
|
217
|
+
return SubnetNodeRegisterRequest(
|
|
218
|
+
subnet_id=subnet_id,
|
|
219
|
+
validator_id=validator_id,
|
|
220
|
+
hotkey=hotkey,
|
|
221
|
+
peer_id=peer_id,
|
|
222
|
+
bootnode_peer_id=bootnode_peer_id,
|
|
223
|
+
client_peer_id=client_peer_id,
|
|
224
|
+
stake_to_be_added=stake_amount_wei,
|
|
225
|
+
max_burn_amount=max_burn_amount,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def prompt_node_peer_id_update(
|
|
230
|
+
subnet_id: Optional[int] = None,
|
|
231
|
+
node_id: Optional[int] = None,
|
|
232
|
+
peer_id: Optional[str] = None,
|
|
233
|
+
peer_id_type: str = "peer_id", # "bootnode_peer_id" or "client_peer_id"
|
|
234
|
+
) -> tuple[int, int, str]:
|
|
235
|
+
"""
|
|
236
|
+
Collect parameters for node peer ID update.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
subnet_id: Optional subnet ID
|
|
240
|
+
node_id: Optional node ID
|
|
241
|
+
peer_id: Optional new peer ID
|
|
242
|
+
peer_id_type: Type of peer ID being updated ("bootnode_peer_id" or "client_peer_id")
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Tuple of (subnet_id, node_id, peer_id)
|
|
246
|
+
"""
|
|
247
|
+
console.print(info(f"Node {peer_id_type.replace('_', ' ').title()} Update"))
|
|
248
|
+
console.print(f"Update a node's {peer_id_type.replace('_', ' ')}.\n")
|
|
249
|
+
|
|
250
|
+
# Validate subnet_id if provided
|
|
251
|
+
if subnet_id is not None:
|
|
252
|
+
is_valid, error_msg = validate_subnet_id_prompt(subnet_id)
|
|
253
|
+
if not is_valid:
|
|
254
|
+
print_error(f"Invalid subnet ID: {error_msg}")
|
|
255
|
+
subnet_id = None
|
|
256
|
+
|
|
257
|
+
# Get subnet ID
|
|
258
|
+
if subnet_id is None:
|
|
259
|
+
subnet_id = prompt.integer_prompt(
|
|
260
|
+
"Enter the Subnet ID",
|
|
261
|
+
min_value=0,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Validate node_id if provided
|
|
265
|
+
if node_id is not None:
|
|
266
|
+
if not isinstance(node_id, int) or node_id < 0:
|
|
267
|
+
print_error("Invalid node ID: must be a non-negative integer")
|
|
268
|
+
node_id = None
|
|
269
|
+
|
|
270
|
+
# Get node ID
|
|
271
|
+
if node_id is None:
|
|
272
|
+
node_id = prompt.integer_prompt(
|
|
273
|
+
"Enter the Node ID",
|
|
274
|
+
min_value=0,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Validate peer_id if provided
|
|
278
|
+
if peer_id is not None:
|
|
279
|
+
if not peer_id or not isinstance(peer_id, str):
|
|
280
|
+
print_error("Invalid peer ID: must be a non-empty string")
|
|
281
|
+
peer_id = None
|
|
282
|
+
elif not validate_peer_id_format(peer_id):
|
|
283
|
+
print_error(
|
|
284
|
+
"Invalid peer ID format: must be a valid base58-encoded multihash "
|
|
285
|
+
"(e.g., starting with 'Qm', '12D3KooW', or '1')"
|
|
286
|
+
)
|
|
287
|
+
peer_id = None
|
|
288
|
+
|
|
289
|
+
# Get peer ID
|
|
290
|
+
if peer_id is None:
|
|
291
|
+
peer_id_label = peer_id_type.replace("_", " ").title()
|
|
292
|
+
peer_id = prompt.text_prompt(
|
|
293
|
+
f"Enter the new {peer_id_label}",
|
|
294
|
+
validator=lambda x: (
|
|
295
|
+
True if validate_peer_id_format(x) else "Invalid peer ID format. Must be a valid base58-encoded multihash."
|
|
296
|
+
),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
return subnet_id, node_id, peer_id
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def prompt_node_remove(
|
|
303
|
+
subnet_id: Optional[int] = None,
|
|
304
|
+
node_id: Optional[int] = None,
|
|
305
|
+
) -> tuple[int, int]:
|
|
306
|
+
"""
|
|
307
|
+
Collect parameters for node removal.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
subnet_id: Optional subnet ID
|
|
311
|
+
node_id: Optional node ID
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Tuple of (subnet_id, node_id)
|
|
315
|
+
"""
|
|
316
|
+
console.print(info("Node Removal"))
|
|
317
|
+
console.print("Remove a node from a subnet.\n")
|
|
318
|
+
|
|
319
|
+
# Validate subnet_id if provided
|
|
320
|
+
if subnet_id is not None:
|
|
321
|
+
is_valid, error_msg = validate_subnet_id_prompt(subnet_id)
|
|
322
|
+
if not is_valid:
|
|
323
|
+
print_error(f"Invalid subnet ID: {error_msg}")
|
|
324
|
+
subnet_id = None
|
|
325
|
+
|
|
326
|
+
# Get subnet ID
|
|
327
|
+
if subnet_id is None:
|
|
328
|
+
subnet_id = prompt.integer_prompt(
|
|
329
|
+
"Enter the Subnet ID",
|
|
330
|
+
min_value=0,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# Validate node_id if provided
|
|
334
|
+
if node_id is not None:
|
|
335
|
+
if not isinstance(node_id, int) or node_id < 0:
|
|
336
|
+
print_error("Invalid node ID: must be a non-negative integer")
|
|
337
|
+
node_id = None
|
|
338
|
+
|
|
339
|
+
# Get node ID
|
|
340
|
+
if node_id is None:
|
|
341
|
+
node_id = prompt.integer_prompt(
|
|
342
|
+
"Enter the Node ID",
|
|
343
|
+
min_value=0,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
return subnet_id, node_id
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Overwatch command module for managing overwatch nodes.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from textwrap import dedent
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from .handlers import (
|
|
11
|
+
add_overwatch_stake_handler,
|
|
12
|
+
check_overwatch_qualification_handler,
|
|
13
|
+
list_overwatch_nodes_handler,
|
|
14
|
+
register_overwatch_node_handler,
|
|
15
|
+
remove_overwatch_node_handler,
|
|
16
|
+
remove_overwatch_stake_handler,
|
|
17
|
+
set_overwatch_peer_id_handler,
|
|
18
|
+
get_overwatch_node_info_handler,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
app = typer.Typer(name="overwatch", help="Overwatch node operations")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
CHECK_QUALIFICATION_HELP = dedent(
|
|
25
|
+
"""\
|
|
26
|
+
Check if a coldkey can register an overwatch node.
|
|
27
|
+
|
|
28
|
+
This command checks all qualification requirements including:
|
|
29
|
+
|
|
30
|
+
\b
|
|
31
|
+
- Overwatch epochs started
|
|
32
|
+
- Network capacity available
|
|
33
|
+
- Coldkey age requirement
|
|
34
|
+
- Reputation score
|
|
35
|
+
- Subnet diversification ratio
|
|
36
|
+
- Average attestation rate
|
|
37
|
+
- Minimum stake balance
|
|
38
|
+
|
|
39
|
+
If --hotkey is provided, also validates:
|
|
40
|
+
|
|
41
|
+
\b
|
|
42
|
+
- Hotkey != coldkey
|
|
43
|
+
- Hotkey has no existing owner
|
|
44
|
+
- Hotkey not already registered
|
|
45
|
+
|
|
46
|
+
\b
|
|
47
|
+
Example:
|
|
48
|
+
htcli overwatch check-qualification --coldkey my-coldkey
|
|
49
|
+
"""
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
REGISTER_HELP = dedent(
|
|
53
|
+
"""\
|
|
54
|
+
Register a new overwatch node.
|
|
55
|
+
|
|
56
|
+
Requires meeting all qualification criteria (see check-qualification).
|
|
57
|
+
|
|
58
|
+
\b
|
|
59
|
+
Example:
|
|
60
|
+
htcli overwatch register --coldkey my-coldkey --hotkey my-hotkey --stake 1000
|
|
61
|
+
"""
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.command("check-qualification", help=CHECK_QUALIFICATION_HELP)
|
|
66
|
+
def check_overwatch_qualification(
|
|
67
|
+
coldkey: Optional[str] = typer.Option(
|
|
68
|
+
None,
|
|
69
|
+
"--coldkey",
|
|
70
|
+
"-c",
|
|
71
|
+
help="Coldkey wallet name or address to check qualification for",
|
|
72
|
+
),
|
|
73
|
+
hotkey: Optional[str] = typer.Option(
|
|
74
|
+
None,
|
|
75
|
+
"--hotkey",
|
|
76
|
+
"-h",
|
|
77
|
+
help="Optional hotkey to validate (checks if hotkey can be used for registration)",
|
|
78
|
+
),
|
|
79
|
+
):
|
|
80
|
+
"""Check if a coldkey can register an overwatch node."""
|
|
81
|
+
check_overwatch_qualification_handler(coldkey=coldkey, hotkey=hotkey)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.command("register", help=REGISTER_HELP)
|
|
85
|
+
def register_overwatch_node(
|
|
86
|
+
hotkey: Optional[str] = typer.Option(
|
|
87
|
+
None, "--hotkey", "-h", help="Hotkey address or name for the overwatch node"
|
|
88
|
+
),
|
|
89
|
+
stake: Optional[float] = typer.Option(
|
|
90
|
+
None, "--stake", "-s", help="Initial stake amount (TENSOR)"
|
|
91
|
+
),
|
|
92
|
+
coldkey: Optional[str] = typer.Option(
|
|
93
|
+
None,
|
|
94
|
+
"--coldkey",
|
|
95
|
+
"-c",
|
|
96
|
+
help="Coldkey wallet name or address to sign the registration",
|
|
97
|
+
),
|
|
98
|
+
):
|
|
99
|
+
"""Register a new overwatch node."""
|
|
100
|
+
register_overwatch_node_handler(
|
|
101
|
+
hotkey=hotkey,
|
|
102
|
+
stake_amount=stake,
|
|
103
|
+
coldkey=coldkey,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@app.command("remove")
|
|
108
|
+
def remove_overwatch_node(
|
|
109
|
+
node_id: Optional[int] = typer.Option(
|
|
110
|
+
None, "--node-id", "-n", help="Overwatch node ID to remove"
|
|
111
|
+
),
|
|
112
|
+
coldkey: Optional[str] = typer.Option(
|
|
113
|
+
None,
|
|
114
|
+
"--coldkey",
|
|
115
|
+
"-c",
|
|
116
|
+
help="Coldkey wallet name or address to sign the removal",
|
|
117
|
+
),
|
|
118
|
+
):
|
|
119
|
+
"""Remove your overwatch node."""
|
|
120
|
+
remove_overwatch_node_handler(
|
|
121
|
+
overwatch_node_id=node_id,
|
|
122
|
+
coldkey=coldkey,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@app.command("list")
|
|
127
|
+
def list_overwatch_nodes():
|
|
128
|
+
"""List all overwatch nodes."""
|
|
129
|
+
list_overwatch_nodes_handler()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@app.command("info")
|
|
133
|
+
def get_overwatch_node_info(
|
|
134
|
+
node_id: Optional[int] = typer.Option(
|
|
135
|
+
None, "--node-id", "-n", help="Overwatch node ID to get info for"
|
|
136
|
+
),
|
|
137
|
+
):
|
|
138
|
+
"""Get detailed information about an overwatch node."""
|
|
139
|
+
get_overwatch_node_info_handler(overwatch_node_id=node_id)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@app.command("set-peer-id")
|
|
143
|
+
def set_overwatch_peer_id(
|
|
144
|
+
subnet_id: Optional[int] = typer.Option(
|
|
145
|
+
None, "--subnet-id", "-s", help="Subnet ID to set peer ID for"
|
|
146
|
+
),
|
|
147
|
+
node_id: Optional[int] = typer.Option(
|
|
148
|
+
None, "--node-id", "-n", help="Overwatch node ID"
|
|
149
|
+
),
|
|
150
|
+
peer_id: Optional[str] = typer.Option(
|
|
151
|
+
None, "--peer-id", "-p", help="Peer ID for the subnet"
|
|
152
|
+
),
|
|
153
|
+
coldkey: Optional[str] = typer.Option(
|
|
154
|
+
None,
|
|
155
|
+
"--coldkey",
|
|
156
|
+
"-c",
|
|
157
|
+
help="Coldkey wallet name or address to sign the transaction",
|
|
158
|
+
),
|
|
159
|
+
):
|
|
160
|
+
"""Set peer ID for an overwatch node on a specific subnet."""
|
|
161
|
+
set_overwatch_peer_id_handler(
|
|
162
|
+
subnet_id=subnet_id,
|
|
163
|
+
overwatch_node_id=node_id,
|
|
164
|
+
peer_id=peer_id,
|
|
165
|
+
coldkey=coldkey,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@app.command("add-stake")
|
|
170
|
+
def add_overwatch_stake(
|
|
171
|
+
node_id: Optional[int] = typer.Option(
|
|
172
|
+
None, "--node-id", "-n", help="Overwatch node ID"
|
|
173
|
+
),
|
|
174
|
+
hotkey: Optional[str] = typer.Option(
|
|
175
|
+
None, "--hotkey", "-h", help="Hotkey of the overwatch node"
|
|
176
|
+
),
|
|
177
|
+
amount: Optional[float] = typer.Option(
|
|
178
|
+
None, "--amount", "-a", help="Amount to stake (TENSOR)"
|
|
179
|
+
),
|
|
180
|
+
coldkey: Optional[str] = typer.Option(
|
|
181
|
+
None,
|
|
182
|
+
"--coldkey",
|
|
183
|
+
"-c",
|
|
184
|
+
help="Coldkey wallet name or address to sign the transaction",
|
|
185
|
+
),
|
|
186
|
+
):
|
|
187
|
+
"""Add stake to an overwatch node."""
|
|
188
|
+
add_overwatch_stake_handler(
|
|
189
|
+
overwatch_node_id=node_id,
|
|
190
|
+
hotkey=hotkey,
|
|
191
|
+
stake_amount=amount,
|
|
192
|
+
coldkey=coldkey,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@app.command("remove-stake")
|
|
197
|
+
def remove_overwatch_stake(
|
|
198
|
+
hotkey: Optional[str] = typer.Option(
|
|
199
|
+
None, "--hotkey", "-h", help="Hotkey of the overwatch node"
|
|
200
|
+
),
|
|
201
|
+
amount: Optional[float] = typer.Option(
|
|
202
|
+
None, "--amount", "-a", help="Amount to unstake (TENSOR)"
|
|
203
|
+
),
|
|
204
|
+
coldkey: Optional[str] = typer.Option(
|
|
205
|
+
None,
|
|
206
|
+
"--coldkey",
|
|
207
|
+
"-c",
|
|
208
|
+
help="Coldkey wallet name or address to sign the transaction",
|
|
209
|
+
),
|
|
210
|
+
):
|
|
211
|
+
"""Remove stake from an overwatch node."""
|
|
212
|
+
remove_overwatch_stake_handler(
|
|
213
|
+
hotkey=hotkey,
|
|
214
|
+
stake_amount=amount,
|
|
215
|
+
coldkey=coldkey,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
__all__ = ["app"]
|