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,844 @@
1
+ """
2
+ Node command handlers for RPC-based operations and extrinsic operations.
3
+ """
4
+
5
+ from typing import Any, Optional
6
+
7
+ from ...dependencies import get_client
8
+ from ...errors.handlers import handle_and_display_error, handle_and_display_node_error
9
+ from ...ui.components import HTCLILoadingContext
10
+ from ...utils import retrieve_wallet_with_validation
11
+ from .display import (
12
+ display_all_nodes_rpc,
13
+ display_node_info_rpc,
14
+ display_node_lifecycle_result,
15
+ display_node_register_result,
16
+ display_node_remove_result,
17
+ display_node_update_summary,
18
+ display_overwatch_commits_rpc,
19
+ display_overwatch_reveals_rpc,
20
+ )
21
+ from .error_handling import handle_node_error
22
+ from .prompts import (
23
+ normalize_delegate_reward_rate,
24
+ prompt_node_peer_id_update,
25
+ prompt_node_register,
26
+ prompt_node_remove,
27
+ )
28
+
29
+
30
+ def get_all_nodes_handler():
31
+ """Handle getting all nodes using RPC."""
32
+ try:
33
+ client = get_client()
34
+
35
+ # Check if client is connected
36
+ if not client.substrate:
37
+ from ...ui.display import print_error
38
+ print_error("Not connected to blockchain. Please check your network configuration.")
39
+ return
40
+
41
+ with HTCLILoadingContext("Fetching all nodes across all subnets..."):
42
+ result = client.rpc.node.get_all_subnet_nodes_info()
43
+
44
+ # Result can be an empty list (legitimate - no nodes) or None (error)
45
+ # The RPC method returns [] on error or when no nodes exist
46
+ if result is not None:
47
+ if len(result) == 0:
48
+ from ...ui.display import print_info
49
+ print_info("No nodes found across all subnets")
50
+ else:
51
+ display_all_nodes_rpc(result)
52
+ else:
53
+ from ...ui.display import print_error
54
+ print_error("Failed to retrieve node information from the blockchain")
55
+
56
+ except RuntimeError as e:
57
+ handle_and_display_error(e, operation="list")
58
+ except Exception as e:
59
+ error_msg = str(e)
60
+ if not handle_node_error(error_msg, None, None, "list", client if 'client' in locals() else None):
61
+ handle_and_display_node_error(e, operation="list")
62
+
63
+
64
+ def get_node_info_handler(subnet_id: Optional[int], node_id: Optional[int]):
65
+ """Handle getting specific node information using RPC."""
66
+ try:
67
+ if subnet_id is None:
68
+ from ...utils.prompts import integer_prompt
69
+
70
+ subnet_id = integer_prompt("Enter subnet ID")
71
+
72
+ if node_id is None:
73
+ from ...utils.prompts import integer_prompt
74
+
75
+ node_id = integer_prompt("Enter node ID")
76
+
77
+ client = get_client()
78
+
79
+ with HTCLILoadingContext(f"Fetching node {node_id} in subnet {subnet_id}..."):
80
+ result = client.rpc.node.get_subnet_node_info(subnet_id, node_id)
81
+
82
+ if result:
83
+ display_node_info_rpc(result, subnet_id, node_id)
84
+ else:
85
+ from ...ui.display import print_info
86
+
87
+ print_info(f"Node {node_id} not found in subnet {subnet_id}")
88
+
89
+ except Exception as e:
90
+ error_msg = str(e)
91
+ if not handle_node_error(error_msg, subnet_id, node_id, "info", client if 'client' in locals() else None):
92
+ handle_and_display_node_error(e, operation="info")
93
+
94
+
95
+ def get_overwatch_commits_handler(
96
+ epoch: Optional[int], overwatch_node_id: Optional[int]
97
+ ):
98
+ """Handle getting overwatch commits using RPC."""
99
+ try:
100
+ if epoch is None:
101
+ from ...utils.prompts import integer_prompt
102
+
103
+ epoch = integer_prompt("Enter epoch number")
104
+
105
+ if overwatch_node_id is None:
106
+ from ...utils.prompts import integer_prompt
107
+
108
+ overwatch_node_id = integer_prompt("Enter overwatch node ID")
109
+
110
+ client = get_client()
111
+
112
+ with HTCLILoadingContext(
113
+ f"Fetching overwatch commits for epoch {epoch}, node {overwatch_node_id}..."
114
+ ):
115
+ result = client.rpc.overwatch.get_overwatch_commits_for_epoch_and_node(
116
+ epoch, overwatch_node_id
117
+ )
118
+
119
+ if result:
120
+ display_overwatch_commits_rpc(result, epoch, overwatch_node_id)
121
+ else:
122
+ from ...ui.display import print_error
123
+
124
+ print_error(f"Failed to get overwatch commits for epoch {epoch}, node {overwatch_node_id}")
125
+
126
+ except Exception as e:
127
+ error_msg = str(e)
128
+ if not handle_node_error(error_msg, None, overwatch_node_id, "overwatch-commits", client if 'client' in locals() else None):
129
+ handle_and_display_node_error(e, operation="overwatch-commits")
130
+
131
+
132
+ def get_overwatch_reveals_handler(
133
+ epoch: Optional[int], overwatch_node_id: Optional[int]
134
+ ):
135
+ """Handle getting overwatch reveals using RPC."""
136
+ try:
137
+ if epoch is None:
138
+ from ...utils.prompts import integer_prompt
139
+
140
+ epoch = integer_prompt("Enter epoch number")
141
+
142
+ if overwatch_node_id is None:
143
+ from ...utils.prompts import integer_prompt
144
+
145
+ overwatch_node_id = integer_prompt("Enter overwatch node ID")
146
+
147
+ client = get_client()
148
+
149
+ with HTCLILoadingContext(
150
+ f"Fetching overwatch reveals for epoch {epoch}, node {overwatch_node_id}..."
151
+ ):
152
+ result = client.rpc.overwatch.get_overwatch_reveals_for_epoch_and_node(
153
+ epoch, overwatch_node_id
154
+ )
155
+
156
+ if result:
157
+ display_overwatch_reveals_rpc(result, epoch, overwatch_node_id)
158
+ else:
159
+ from ...ui.display import print_error
160
+
161
+ print_error(f"Failed to get overwatch reveals for epoch {epoch}, node {overwatch_node_id}")
162
+
163
+ except Exception as e:
164
+ error_msg = str(e)
165
+ if not handle_node_error(error_msg, None, overwatch_node_id, "overwatch-reveals", client if 'client' in locals() else None):
166
+ handle_and_display_node_error(e, operation="overwatch-reveals")
167
+
168
+
169
+ # ============================================================================
170
+ # EXTRINSIC HANDLERS - Write Operations
171
+ # ============================================================================
172
+
173
+
174
+ def register_node_handler(
175
+ subnet_id: Optional[int] = None,
176
+ validator_id: Optional[int] = None,
177
+ hotkey: Optional[str] = None,
178
+ stake_amount: Optional[float] = None,
179
+ peer_id: Optional[str] = None,
180
+ bootnode_peer_id: Optional[str] = None,
181
+ client_peer_id: Optional[str] = None,
182
+ coldkey: Optional[str] = None,
183
+ ):
184
+ """Handle node registration on a subnet."""
185
+ try:
186
+ from ...ui.display import print_error
187
+ from ...utils.validation import validate_stake_amount_prompt
188
+
189
+ def validate_cli_peer_id(label: str, value: Optional[str]) -> None:
190
+ if value is None:
191
+ return
192
+ if not value or not value.isalnum():
193
+ raise ValueError(f"Invalid {label}: must contain only letters and numbers")
194
+
195
+ if stake_amount is not None:
196
+ is_valid, error_msg = validate_stake_amount_prompt(stake_amount)
197
+ if not is_valid:
198
+ print_error(f"Invalid stake amount: {error_msg}")
199
+ raise ValueError(f"Invalid stake amount: {error_msg}")
200
+
201
+ try:
202
+ validate_cli_peer_id("peer_id", peer_id)
203
+ validate_cli_peer_id("bootnode_peer_id", bootnode_peer_id)
204
+ validate_cli_peer_id("client_peer_id", client_peer_id)
205
+ except ValueError as e:
206
+ print_error(str(e))
207
+ raise
208
+
209
+ # STEP 1: Get signing wallet (coldkey) FIRST so we can use it for hotkey disambiguation
210
+ if coldkey:
211
+ from ...utils.wallet.core import resolve_coldkey_and_get_keypair
212
+
213
+ wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
214
+ else:
215
+ wallet_name, keypair = retrieve_wallet_with_validation(
216
+ wallet_type="coldkey", purpose="sign the node registration transaction"
217
+ )
218
+
219
+ # STEP 2: Collect parameters via prompts (now with coldkey context for hotkey disambiguation)
220
+ request = prompt_node_register(
221
+ subnet_id=subnet_id,
222
+ validator_id=validator_id,
223
+ hotkey=hotkey,
224
+ stake_amount=stake_amount,
225
+ peer_id=peer_id,
226
+ bootnode_peer_id=bootnode_peer_id,
227
+ client_peer_id=client_peer_id,
228
+ coldkey_name=wallet_name, # Pass coldkey name for hotkey disambiguation
229
+ )
230
+
231
+ # STEP 3: Execute via client
232
+ client = get_client()
233
+
234
+ # Ensure connection is established
235
+ if not client.connect():
236
+ from ...ui.display import print_error
237
+ print_error("Failed to connect to blockchain")
238
+ return
239
+
240
+ # Check if substrate connection is available
241
+ if not client.substrate:
242
+ from ...ui.display import print_error
243
+ print_error("Blockchain connection failed!")
244
+ raise RuntimeError("Not connected to blockchain")
245
+
246
+ # Check if extrinsics layer is initialized
247
+ if not client.extrinsics:
248
+ from ...ui.display import print_error
249
+ print_error("Extrinsics layer not initialized. Connection may have failed.")
250
+ raise RuntimeError("Extrinsics layer not available")
251
+
252
+ # Get the actual EVM address from wallet info (not from keypair.ss58_address which returns public key)
253
+ from ...utils.logging import get_logger
254
+ from ...utils.wallet.crypto import get_wallet_info_by_name
255
+
256
+ logger = get_logger(__name__)
257
+ wallet_info = get_wallet_info_by_name(wallet_name, is_hotkey=False)
258
+ resolved_coldkey_address = wallet_info.get("evm_address") or wallet_info.get("ss58_address") or wallet_info.get("address")
259
+
260
+ logger.debug(f"Wallet '{wallet_name}' info: {wallet_info}")
261
+ logger.debug(f"keypair.ss58_address: {keypair.ss58_address}")
262
+ logger.debug(f"Resolved coldkey address (from wallet info): {resolved_coldkey_address}")
263
+ logger.debug(f"Using coldkey wallet '{wallet_name}' ({resolved_coldkey_address}) to sign node registration")
264
+
265
+ with HTCLILoadingContext(f"Registering node on subnet {request.subnet_id}..."):
266
+ response = client.extrinsics.node.register_subnet_node(request, keypair)
267
+
268
+ # Check for errors in result
269
+ if isinstance(response, dict) and not response.get("success", False):
270
+ error_msg = response.get("error", "Registration failed")
271
+ if handle_node_error(error_msg, request.subnet_id, None, "register", client):
272
+ return
273
+
274
+ # STEP 4: Display result
275
+ display_node_register_result(response)
276
+
277
+ except Exception as e:
278
+ error_msg = str(e)
279
+ # Extract subnet_id and client if available
280
+ subnet_id_for_error = None
281
+ client_for_error = None
282
+ try:
283
+ subnet_id_for_error = request.subnet_id if 'request' in locals() else None
284
+ client_for_error = client if 'client' in locals() else None
285
+ except (AttributeError, NameError):
286
+ pass
287
+
288
+ if not handle_node_error(error_msg, subnet_id_for_error, None, "register", client_for_error):
289
+ handle_and_display_node_error(e, operation="register")
290
+
291
+
292
+ def update_node_handler(
293
+ subnet_id: Optional[int] = None,
294
+ node_id: Optional[int] = None,
295
+ peer_id: Optional[str] = None,
296
+ bootnode: Optional[str] = None,
297
+ bootnode_peer_id: Optional[str] = None,
298
+ client_peer_id: Optional[str] = None,
299
+ delegate_reward_rate: Optional[int] = None,
300
+ unique: Optional[str] = None,
301
+ non_unique: Optional[str] = None,
302
+ coldkey: Optional[str] = None,
303
+ ):
304
+ """
305
+ Unified handler for updating a subnet node.
306
+
307
+ All updatable fields are optional. Any field provided will be updated
308
+ sequentially, and a consolidated summary will be displayed.
309
+ """
310
+ try:
311
+ from ...ui.display import print_error
312
+ from ...utils.blockchain.peer_id import validate_peer_id_format
313
+
314
+ # Ensure we know which node to update
315
+ if subnet_id is None or node_id is None:
316
+ from ...utils.prompts import integer_prompt
317
+
318
+ if subnet_id is None:
319
+ subnet_id = integer_prompt("Enter the Subnet ID", min_value=0)
320
+ if node_id is None:
321
+ node_id = integer_prompt("Enter the Node ID", min_value=0)
322
+
323
+ # Validate that at least one field is provided
324
+ if (
325
+ peer_id is None
326
+ and bootnode is None
327
+ and bootnode_peer_id is None
328
+ and client_peer_id is None
329
+ and delegate_reward_rate is None
330
+ and unique is None
331
+ and non_unique is None
332
+ ):
333
+ print_error(
334
+ "Error: At least one update parameter must be provided "
335
+ "(e.g. --peer-id, --bootnode, --bootnode-peer-id, --client-peer-id, "
336
+ "--delegate-rate, --unique, --non-unique)."
337
+ )
338
+ raise ValueError("No update fields provided")
339
+
340
+ # Basic validation for peer IDs if provided
341
+ def _validate_peer(label: str, value: Optional[str]) -> None:
342
+ if value is None:
343
+ return
344
+ if not isinstance(value, str) or not value:
345
+ raise ValueError(f"{label} must be a non-empty string")
346
+ if not validate_peer_id_format(value):
347
+ raise ValueError(
348
+ f"Invalid {label} format: must be a valid base58-encoded multihash "
349
+ "(e.g., starting with 'Qm', '12D3KooW', or '1')"
350
+ )
351
+
352
+ try:
353
+ _validate_peer("peer_id", peer_id)
354
+ _validate_peer("bootnode_peer_id", bootnode_peer_id)
355
+ _validate_peer("client_peer_id", client_peer_id)
356
+ except ValueError as ve:
357
+ print_error(str(ve))
358
+ raise
359
+
360
+ # Normalize delegate reward rate: accept 0-100 as %, otherwise treat as 1e18 format
361
+ delegate_rate_value: Optional[int] = None
362
+ if delegate_reward_rate is not None:
363
+ try:
364
+ delegate_rate_value = normalize_delegate_reward_rate(
365
+ delegate_reward_rate
366
+ )
367
+ except ValueError as ve:
368
+ print_error(f"Invalid delegate reward rate: {ve}")
369
+ raise
370
+
371
+ # Get signing wallet (coldkey)
372
+ if coldkey:
373
+ from ...utils.wallet.core import resolve_coldkey_and_get_keypair
374
+
375
+ wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
376
+ else:
377
+ wallet_name, keypair = retrieve_wallet_with_validation(
378
+ wallet_type="coldkey",
379
+ purpose="sign the node update transaction",
380
+ )
381
+
382
+ client = get_client()
383
+
384
+ # Ensure connection is established
385
+ if not client.connect():
386
+ print_error("Failed to connect to blockchain")
387
+ return
388
+
389
+ if not client.substrate:
390
+ print_error("Blockchain connection failed!")
391
+ raise RuntimeError("Not connected to blockchain")
392
+
393
+ if not client.extrinsics:
394
+ print_error(
395
+ "Extrinsics layer not initialized. Connection may have failed."
396
+ )
397
+ raise RuntimeError("Extrinsics layer not available")
398
+
399
+ # Fetch current node info once for old-value display (best-effort)
400
+ current_node_info = None
401
+ try:
402
+ current_node_info = client.rpc.node.get_subnet_node_info(
403
+ subnet_id, node_id
404
+ )
405
+ except Exception:
406
+ current_node_info = None
407
+
408
+ def _safe_decode_bytes(value: Any) -> Optional[str]:
409
+ if value is None:
410
+ return None
411
+ if isinstance(value, bytes):
412
+ try:
413
+ return value.decode("utf-8", errors="ignore")
414
+ except Exception:
415
+ return str(value)
416
+ return str(value)
417
+
418
+ updates: list[dict[str, Any]] = []
419
+
420
+ # Compute old values (best-effort) for summary
421
+ if peer_id is not None:
422
+ old_peer = (
423
+ _safe_decode_bytes(current_node_info.peer_id)
424
+ if current_node_info is not None
425
+ else None
426
+ )
427
+ else:
428
+ old_peer = None
429
+
430
+ if bootnode is not None:
431
+ old_bootnode = (
432
+ _safe_decode_bytes(current_node_info.bootnode)
433
+ if current_node_info is not None
434
+ else None
435
+ )
436
+ else:
437
+ old_bootnode = None
438
+
439
+ if bootnode_peer_id is not None:
440
+ old_bootnode_peer = (
441
+ _safe_decode_bytes(current_node_info.bootnode_peer_id)
442
+ if current_node_info is not None
443
+ else None
444
+ )
445
+ else:
446
+ old_bootnode_peer = None
447
+
448
+ if client_peer_id is not None:
449
+ old_client_peer = (
450
+ _safe_decode_bytes(current_node_info.client_peer_id)
451
+ if current_node_info is not None
452
+ else None
453
+ )
454
+ else:
455
+ old_client_peer = None
456
+
457
+ if delegate_rate_value is not None:
458
+ old_rate = (
459
+ getattr(current_node_info, "delegate_reward_rate", None)
460
+ if current_node_info is not None
461
+ else None
462
+ )
463
+ else:
464
+ old_rate = None
465
+
466
+ if unique is not None:
467
+ old_unique = (
468
+ _safe_decode_bytes(current_node_info.unique)
469
+ if current_node_info is not None
470
+ else None
471
+ )
472
+ else:
473
+ old_unique = None
474
+
475
+ if non_unique is not None:
476
+ old_non_unique = (
477
+ _safe_decode_bytes(current_node_info.non_unique)
478
+ if current_node_info is not None
479
+ else None
480
+ )
481
+ else:
482
+ old_non_unique = None
483
+
484
+ # Delegate all extrinsic calls to the client layer
485
+ with HTCLILoadingContext(
486
+ f"Updating node {node_id} on subnet {subnet_id}..."
487
+ ):
488
+ raw_results = client.extrinsics.node.update_subnet_node_parameters(
489
+ subnet_id=subnet_id,
490
+ subnet_node_id=node_id,
491
+ peer_id=peer_id,
492
+ bootnode=bootnode,
493
+ bootnode_peer_id=bootnode_peer_id,
494
+ client_peer_id=client_peer_id,
495
+ delegate_reward_rate=delegate_rate_value,
496
+ unique=unique,
497
+ non_unique=non_unique,
498
+ keypair=keypair,
499
+ )
500
+
501
+ # Attach old/new values for summary display
502
+ for entry in raw_results:
503
+ field = entry.get("field")
504
+ result = entry.get("result")
505
+ if field == "peer_id":
506
+ updates.append(
507
+ {
508
+ "field": field,
509
+ "old": old_peer,
510
+ "new": peer_id,
511
+ "result": result,
512
+ }
513
+ )
514
+ elif field == "bootnode":
515
+ updates.append(
516
+ {
517
+ "field": field,
518
+ "old": old_bootnode,
519
+ "new": bootnode,
520
+ "result": result,
521
+ }
522
+ )
523
+ elif field == "bootnode_peer_id":
524
+ updates.append(
525
+ {
526
+ "field": field,
527
+ "old": old_bootnode_peer,
528
+ "new": bootnode_peer_id,
529
+ "result": result,
530
+ }
531
+ )
532
+ elif field == "client_peer_id":
533
+ updates.append(
534
+ {
535
+ "field": field,
536
+ "old": old_client_peer,
537
+ "new": client_peer_id,
538
+ "result": result,
539
+ }
540
+ )
541
+ elif field == "delegate_reward_rate":
542
+ updates.append(
543
+ {
544
+ "field": field,
545
+ "old": old_rate,
546
+ "new": delegate_rate_value,
547
+ "result": result,
548
+ }
549
+ )
550
+ elif field == "unique":
551
+ updates.append(
552
+ {
553
+ "field": field,
554
+ "old": old_unique,
555
+ "new": unique,
556
+ "result": result,
557
+ }
558
+ )
559
+ elif field == "non_unique":
560
+ updates.append(
561
+ {
562
+ "field": field,
563
+ "old": old_non_unique,
564
+ "new": non_unique,
565
+ "result": result,
566
+ }
567
+ )
568
+
569
+ # Check for errors in results
570
+ for entry in updates:
571
+ result = entry.get("result")
572
+ if isinstance(result, dict) and not result.get("success", False):
573
+ error_msg = result.get("error", "Update failed")
574
+ field = entry.get("field", "update")
575
+ if handle_node_error(error_msg, subnet_id, node_id, f"update {field}", client):
576
+ return
577
+
578
+ # Display consolidated summary
579
+ if updates:
580
+ display_node_update_summary(
581
+ subnet_id=subnet_id,
582
+ node_id=node_id,
583
+ updates=updates,
584
+ client=client,
585
+ )
586
+
587
+ except Exception as e:
588
+ error_msg = str(e)
589
+ if not handle_node_error(error_msg, subnet_id, node_id, "update", client if 'client' in locals() else None):
590
+ handle_and_display_node_error(e, operation="update")
591
+
592
+
593
+ def update_node_bootnode_peer_id_handler(
594
+ subnet_id: Optional[int] = None,
595
+ node_id: Optional[int] = None,
596
+ bootnode_peer_id: Optional[str] = None,
597
+ coldkey: Optional[str] = None,
598
+ ):
599
+ """
600
+ Backwards-compatible wrapper to update only the bootnode peer ID.
601
+ """
602
+ # Collect parameters via prompts if needed, then delegate to unified handler
603
+ if subnet_id is None or node_id is None or bootnode_peer_id is None:
604
+ subnet_id, node_id, peer = prompt_node_peer_id_update(
605
+ subnet_id=subnet_id,
606
+ node_id=node_id,
607
+ peer_id=bootnode_peer_id,
608
+ peer_id_type="bootnode_peer_id",
609
+ )
610
+ bootnode_peer_id = peer
611
+
612
+ update_node_handler(
613
+ subnet_id=subnet_id,
614
+ node_id=node_id,
615
+ bootnode_peer_id=bootnode_peer_id,
616
+ coldkey=coldkey,
617
+ )
618
+
619
+
620
+ def update_node_client_peer_id_handler(
621
+ subnet_id: Optional[int] = None,
622
+ node_id: Optional[int] = None,
623
+ client_peer_id: Optional[str] = None,
624
+ coldkey: Optional[str] = None,
625
+ ):
626
+ """
627
+ Backwards-compatible wrapper to update only the client peer ID.
628
+ """
629
+ # Collect parameters via prompts if needed, then delegate to unified handler
630
+ if subnet_id is None or node_id is None or client_peer_id is None:
631
+ subnet_id, node_id, peer = prompt_node_peer_id_update(
632
+ subnet_id=subnet_id,
633
+ node_id=node_id,
634
+ peer_id=client_peer_id,
635
+ peer_id_type="client_peer_id",
636
+ )
637
+ client_peer_id = peer
638
+
639
+ update_node_handler(
640
+ subnet_id=subnet_id,
641
+ node_id=node_id,
642
+ client_peer_id=client_peer_id,
643
+ coldkey=coldkey,
644
+ )
645
+
646
+
647
+ def remove_node_handler(
648
+ subnet_id: Optional[int] = None,
649
+ node_id: Optional[int] = None,
650
+ coldkey: Optional[str] = None,
651
+ ):
652
+ """Handle removing a node from a subnet."""
653
+ try:
654
+ from ...models.requests.node import SubnetNodeRemoveRequest
655
+
656
+ # Collect IDs if missing
657
+ if subnet_id is None or node_id is None:
658
+ subnet_id, node_id = prompt_node_remove(subnet_id=subnet_id, node_id=node_id)
659
+
660
+ # Get signing wallet (coldkey)
661
+ if coldkey:
662
+ from ...utils.wallet.core import resolve_coldkey_and_get_keypair
663
+
664
+ wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
665
+ else:
666
+ wallet_name, keypair = retrieve_wallet_with_validation(
667
+ wallet_type="coldkey",
668
+ purpose="sign the node removal transaction",
669
+ )
670
+
671
+ client = get_client()
672
+
673
+ # Ensure connection
674
+ if not client.connect():
675
+ from ...ui.display import print_error
676
+
677
+ print_error("Failed to connect to blockchain")
678
+ return
679
+
680
+ if not client.substrate:
681
+ from ...ui.display import print_error
682
+
683
+ print_error("Blockchain connection failed!")
684
+ raise RuntimeError("Not connected to blockchain")
685
+
686
+ if not client.extrinsics:
687
+ from ...ui.display import print_error
688
+
689
+ print_error(
690
+ "Extrinsics layer not initialized. Connection may have failed."
691
+ )
692
+ raise RuntimeError("Extrinsics layer not available")
693
+
694
+ request = SubnetNodeRemoveRequest(
695
+ subnet_id=subnet_id,
696
+ subnet_node_id=node_id,
697
+ )
698
+
699
+ with HTCLILoadingContext(
700
+ f"Removing node {node_id} from subnet {subnet_id}..."
701
+ ):
702
+ result = client.extrinsics.node.remove_subnet_node(request, keypair)
703
+
704
+ # Check for errors in result
705
+ if isinstance(result, dict) and not result.get("success", False):
706
+ error_msg = result.get("error", "Removal failed")
707
+ if handle_node_error(error_msg, subnet_id, node_id, "remove", client):
708
+ return
709
+
710
+ display_node_remove_result(result, subnet_id, node_id)
711
+
712
+ except Exception as e:
713
+ error_msg = str(e)
714
+ if not handle_node_error(error_msg, subnet_id, node_id, "remove", client if 'client' in locals() else None):
715
+ handle_and_display_node_error(e, operation="remove")
716
+
717
+
718
+ def _resolve_node_lifecycle_ids(
719
+ subnet_id: Optional[int],
720
+ node_id: Optional[int],
721
+ action: str,
722
+ ) -> tuple[int, int]:
723
+ """Prompt for lifecycle command IDs when CLI options are omitted."""
724
+ if subnet_id is None or node_id is None:
725
+ from ...utils.prompts import integer_prompt
726
+
727
+ if subnet_id is None:
728
+ subnet_id = integer_prompt(f"Enter subnet ID to {action} node", min_value=0)
729
+ if node_id is None:
730
+ node_id = integer_prompt(f"Enter node ID to {action}", min_value=0)
731
+
732
+ return subnet_id, node_id
733
+
734
+
735
+ def _node_lifecycle_handler(
736
+ *,
737
+ subnet_id: Optional[int],
738
+ node_id: Optional[int],
739
+ coldkey: Optional[str],
740
+ action: str,
741
+ ):
742
+ """Submit a node activation/deactivation extrinsic."""
743
+ try:
744
+ from ...models.requests.node import (
745
+ SubnetNodeActivateRequest,
746
+ SubnetNodePauseRequest,
747
+ )
748
+
749
+ subnet_id, node_id = _resolve_node_lifecycle_ids(subnet_id, node_id, action)
750
+
751
+ if coldkey:
752
+ from ...utils.wallet.core import resolve_coldkey_and_get_keypair
753
+
754
+ _, keypair = resolve_coldkey_and_get_keypair(coldkey)
755
+ else:
756
+ _, keypair = retrieve_wallet_with_validation(
757
+ wallet_type="coldkey",
758
+ purpose=f"sign the node {action} transaction",
759
+ )
760
+
761
+ client = get_client()
762
+
763
+ if not client.connect():
764
+ from ...ui.display import print_error
765
+
766
+ print_error("Failed to connect to blockchain")
767
+ return
768
+
769
+ if not client.substrate:
770
+ from ...ui.display import print_error
771
+
772
+ print_error("Blockchain connection failed!")
773
+ raise RuntimeError("Not connected to blockchain")
774
+
775
+ if not client.extrinsics:
776
+ from ...ui.display import print_error
777
+
778
+ print_error(
779
+ "Extrinsics layer not initialized. Connection may have failed."
780
+ )
781
+ raise RuntimeError("Extrinsics layer not available")
782
+
783
+ if action == "activate":
784
+ request = SubnetNodeActivateRequest(
785
+ subnet_id=subnet_id,
786
+ subnet_node_id=node_id,
787
+ )
788
+ submit = client.extrinsics.node.activate_subnet_node
789
+ loading_message = f"Activating node {node_id} on subnet {subnet_id}..."
790
+ elif action == "deactivate":
791
+ request = SubnetNodePauseRequest(
792
+ subnet_id=subnet_id,
793
+ subnet_node_id=node_id,
794
+ )
795
+ submit = client.extrinsics.node.pause_subnet_node
796
+ loading_message = f"Deactivating node {node_id} on subnet {subnet_id}..."
797
+ else:
798
+ raise ValueError(f"Unsupported node lifecycle action: {action}")
799
+
800
+ with HTCLILoadingContext(loading_message):
801
+ result = submit(request, keypair)
802
+
803
+ if isinstance(result, dict) and not result.get("success", False):
804
+ error_msg = result.get("error", f"Node {action} failed")
805
+ if handle_node_error(error_msg, subnet_id, node_id, action, client):
806
+ return
807
+
808
+ display_node_lifecycle_result(result, subnet_id, node_id, action)
809
+
810
+ except Exception as e:
811
+ error_msg = str(e)
812
+ client_for_error = client if "client" in locals() else None
813
+ if not handle_node_error(
814
+ error_msg, subnet_id, node_id, action, client_for_error
815
+ ):
816
+ handle_and_display_node_error(e, operation=action)
817
+
818
+
819
+ def activate_node_handler(
820
+ subnet_id: Optional[int] = None,
821
+ node_id: Optional[int] = None,
822
+ coldkey: Optional[str] = None,
823
+ ):
824
+ """Handle activating a node on a subnet."""
825
+ _node_lifecycle_handler(
826
+ subnet_id=subnet_id,
827
+ node_id=node_id,
828
+ coldkey=coldkey,
829
+ action="activate",
830
+ )
831
+
832
+
833
+ def deactivate_node_handler(
834
+ subnet_id: Optional[int] = None,
835
+ node_id: Optional[int] = None,
836
+ coldkey: Optional[str] = None,
837
+ ):
838
+ """Handle deactivating a node on a subnet."""
839
+ _node_lifecycle_handler(
840
+ subnet_id=subnet_id,
841
+ node_id=node_id,
842
+ coldkey=coldkey,
843
+ action="deactivate",
844
+ )