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