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,2855 @@
1
+ from typing import Optional
2
+
3
+ import typer
4
+
5
+ from ...dependencies import get_client
6
+ from ...errors.base import SubnetActivationError, SubnetError, SubnetRegistrationError
7
+ from ...errors.handlers import (
8
+ handle_and_display_error,
9
+ handle_and_display_node_error,
10
+ handle_substrate_error,
11
+ )
12
+ from ...ui.colors import success
13
+ from ...ui.components import HTCLILoadingContext
14
+ from ...ui.display import print_error, print_info
15
+ from ...utils import retrieve_wallet_with_validation
16
+ from ...utils.constants import (
17
+ MAX_SUBNET_PENALTY_COUNT,
18
+ MIN_SUBNET_DELEGATE_STAKE_FACTOR_PERCENT,
19
+ MIN_SUBNET_NODES,
20
+ format_percentage,
21
+ )
22
+ from ...utils.logging import get_logger
23
+ from .error_handling import handle_subnet_error
24
+
25
+ logger = get_logger(__name__)
26
+ from .display import (
27
+ display_activation_requirements, # New display functions
28
+ display_bootnodes_rpc,
29
+ display_generic_success,
30
+ display_subnet_info,
31
+ display_subnet_info_rpc,
32
+ display_subnet_list,
33
+ display_subnet_list_with_coldkey,
34
+ display_subnet_nodes,
35
+ display_subnet_nodes_rpc,
36
+ display_subnets_overview,
37
+ )
38
+ from .prompts import (
39
+ prompt_activate_subnet,
40
+ prompt_bootnode_access,
41
+ prompt_check_activation,
42
+ prompt_clear_emergency_validator_set,
43
+ prompt_get_subnet,
44
+ prompt_list_nodes,
45
+ prompt_owner_accept,
46
+ prompt_owner_remove,
47
+ prompt_owner_transfer,
48
+ prompt_owner_update,
49
+ prompt_owner_update_extended,
50
+ prompt_owner_update_name,
51
+ prompt_owner_update_repo,
52
+ prompt_pause_subnet,
53
+ prompt_register_subnet,
54
+ prompt_set_emergency_validator_set,
55
+ prompt_unpause_subnet,
56
+ )
57
+
58
+ DELEGATE_STAKE_FACTOR_DISPLAY = format_percentage(
59
+ MIN_SUBNET_DELEGATE_STAKE_FACTOR_PERCENT, precision=3
60
+ )
61
+
62
+ ACTIVATION_REQUIREMENTS_TEXT = f"""📋 Activation requirements:
63
+
64
+ 1. Minimum nodes:
65
+ • Register at least {MIN_SUBNET_NODES} active/electable nodes
66
+
67
+ 2. Delegate stake:
68
+ • Total delegate stake across your nodes must meet the floating minimum
69
+ (~{DELEGATE_STAKE_FACTOR_DISPLAY} of total supply × node-count multiplier)
70
+
71
+ 3. Penalties:
72
+ • Subnet penalty count must stay at or below {MAX_SUBNET_PENALTY_COUNT}
73
+
74
+ 4. Timing:
75
+ • Make sure the minimum registration epochs have elapsed before retrying"""
76
+
77
+ ACTIVATION_NEXT_STEPS_TEXT = """💡 Next Steps:
78
+
79
+ • Register additional nodes if you're below the minimum
80
+ • Delegate more stake (or attract delegations) to exceed the floating threshold
81
+ • Resolve any penalties affecting the subnet before retrying
82
+ • Retry activation once the conditions above are comfortably met"""
83
+
84
+
85
+ # Read Handlers
86
+ def registration_cost_handler():
87
+ """Handle subnet registration cost query."""
88
+ try:
89
+ client = get_client()
90
+
91
+ # Ensure client is connected and RPC is available
92
+ if not client.rpc:
93
+ raise RuntimeError(
94
+ "Client RPC layer not initialized. Connection may have failed."
95
+ )
96
+
97
+ with HTCLILoadingContext("Querying subnet registration cost..."):
98
+ current_cost = client.rpc.chain.get_subnet_registration_cost()
99
+ block_number = client.rpc.chain.get_block_number()
100
+ current_epoch = client.rpc.chain.get_current_epoch()
101
+
102
+ # Display - provide helpful message if cost is None
103
+ if current_cost is None:
104
+ print_error("Failed to query registration cost from blockchain.")
105
+ print_info(
106
+ "This may indicate a connection issue or the storage values are not set yet."
107
+ )
108
+ else:
109
+ from ...commands.chain.display import display_registration_cost
110
+
111
+ display_registration_cost(current_cost, block_number, current_epoch)
112
+
113
+ except Exception as e:
114
+ handle_and_display_error(e)
115
+
116
+
117
+ def check_registration_cost_handler():
118
+ """Check subnet registration cost and compare with all wallet balances."""
119
+ try:
120
+ from ...ui.display import print_error, print_info, print_warning
121
+ from ...utils.wallet.crypto import list_keys
122
+
123
+ client = get_client()
124
+
125
+ print_info("💰 Fetching subnet registration cost...")
126
+ print_info("📡 Querying Network pallet storage values...")
127
+
128
+ # Get current registration cost
129
+ cost_wei = client.rpc.chain.get_subnet_registration_cost()
130
+
131
+ if cost_wei is None:
132
+ print_error("Failed to fetch registration cost from blockchain")
133
+ print_warning("\n💡 Possible causes:")
134
+ print_warning(" 1. Network pallet storage values not initialized")
135
+ print_warning(" 2. Blockchain connection issue")
136
+ print_warning(" 3. Storage value names may have changed")
137
+ print_warning("\n📋 Required storage values:")
138
+ print_warning(" - Network::LastRegistrationCost")
139
+ print_warning(" - Network::MinRegistrationCost")
140
+ print_warning(" - Network::LastSubnetRegistrationBlock")
141
+ print_warning(" - Network::RegistrationCostDecayBlocks")
142
+ print_warning(" - Network::RegistrationCostAlpha")
143
+ print_warning("\n🔧 Check the logs above to see which value is missing")
144
+ return
145
+
146
+ cost_tensor = cost_wei / 1e18
147
+
148
+ # Get all coldkey wallets
149
+ all_wallets = list_keys()
150
+ coldkeys = [w for w in all_wallets if not w.get("is_hotkey", False)]
151
+
152
+ if not coldkeys:
153
+ from ...ui.display import print_warning
154
+
155
+ print_warning(
156
+ "No coldkey wallets found. Create one with: htcli wallet generate-coldkey"
157
+ )
158
+ return
159
+
160
+ print_info(f"📊 Checking {len(coldkeys)} coldkey wallet balances...")
161
+
162
+ # Fetch balances for all coldkeys with progress indicator
163
+ from ...ui.components import HTCLIProgress
164
+
165
+ wallet_data = []
166
+ with HTCLIProgress() as progress:
167
+ task = progress.add_task("Fetching balances...", total=len(coldkeys))
168
+
169
+ for wallet in coldkeys:
170
+ wallet_name = wallet["name"]
171
+ address = wallet.get("ss58_address")
172
+
173
+ progress.update(
174
+ task,
175
+ advance=1,
176
+ description=f"Checking [bold]{wallet_name}[/bold]...",
177
+ )
178
+
179
+ try:
180
+ balance_response = client.rpc.wallet.get_balance(address)
181
+ if balance_response.success:
182
+ balance_wei = balance_response.balance
183
+ balance_tensor = balance_wei / 1e18
184
+ sufficient = balance_wei >= cost_wei
185
+ shortfall = (
186
+ (cost_wei - balance_wei) / 1e18 if not sufficient else 0
187
+ )
188
+
189
+ wallet_data.append(
190
+ {
191
+ "name": wallet_name,
192
+ "address": address,
193
+ "balance": balance_tensor,
194
+ "sufficient": sufficient,
195
+ "shortfall": shortfall,
196
+ }
197
+ )
198
+ else:
199
+ wallet_data.append(
200
+ {
201
+ "name": wallet_name,
202
+ "address": address,
203
+ "balance": 0,
204
+ "sufficient": False,
205
+ "shortfall": cost_tensor,
206
+ "error": "Failed to fetch balance",
207
+ }
208
+ )
209
+ except Exception as e:
210
+ wallet_data.append(
211
+ {
212
+ "name": wallet_name,
213
+ "address": address,
214
+ "balance": 0,
215
+ "sufficient": False,
216
+ "shortfall": cost_tensor,
217
+ "error": str(e),
218
+ }
219
+ )
220
+
221
+ # Display results
222
+ from .display import display_registration_cost_comparison
223
+
224
+ display_registration_cost_comparison(cost_tensor, wallet_data)
225
+
226
+ except Exception as e:
227
+ handle_substrate_error(e)
228
+
229
+
230
+ def list_subnets_handler(
231
+ all_subnets: bool,
232
+ active_only: bool,
233
+ coldkey: Optional[str] = None,
234
+ ):
235
+ """Handle listing subnets with proper error handling and optional coldkey filtering."""
236
+ try:
237
+ client = get_client()
238
+
239
+ # Ensure connection is established
240
+ if not client.connect():
241
+ print_error("Failed to connect to blockchain")
242
+ return
243
+
244
+ # Normalize empty string from "--coldkey \"\"" into None to trigger prompting
245
+ # if isinstance(coldkey, str) and coldkey.strip() == "":
246
+ # coldkey = None
247
+
248
+ # If no mode specified, default to listing all subnets
249
+ if not all_subnets:
250
+ # Default to all subnets when no parameters are provided
251
+ all_subnets = True
252
+
253
+ # Handle coldkey filtering
254
+ if coldkey:
255
+ # Resolve coldkey and fetch subnets with spinner
256
+ subnet_data = None
257
+ with HTCLILoadingContext("Resolving coldkey and fetching subnets..."):
258
+ original_coldkey = coldkey
259
+
260
+ # If coldkey looks like a wallet name, resolve to address
261
+ if not (coldkey.startswith("0x") or len(coldkey) in (48, 49)):
262
+ # Treat as wallet name: resolve to its address if exists
263
+ try:
264
+ from ...utils.wallet.crypto import get_wallet_info_by_name
265
+
266
+ info = get_wallet_info_by_name(coldkey, is_hotkey=False)
267
+ # Prefer EVM address, fallback to ss58_address
268
+ coldkey = (
269
+ info.get("address") or info.get("ss58_address") or coldkey
270
+ )
271
+ logger.debug(
272
+ f"Resolved wallet name '{original_coldkey}' to address '{coldkey}'"
273
+ )
274
+ except FileNotFoundError:
275
+ # Wallet name not found - show error and exit
276
+ print_error(f"Wallet '{coldkey}' not found")
277
+ return
278
+ except Exception as e:
279
+ logger.warning(f"Error resolving wallet name '{coldkey}': {e}")
280
+ # Leave as provided; will try to use as address
281
+
282
+ # Normalize Ethereum addresses to lowercase (addresses are case-insensitive)
283
+ if coldkey.startswith("0x"):
284
+ coldkey = coldkey.lower()
285
+
286
+ # Get subnet IDs for this coldkey using efficient storage queries
287
+ subnet_ids = client.rpc.subnet.get_subnet_ids_for_coldkey(coldkey)
288
+
289
+ if not subnet_ids:
290
+ # No subnets found - will show message after spinner clears
291
+ subnet_data = []
292
+ else:
293
+ # Fetch subnet info for the found subnet IDs
294
+ subnets_info = []
295
+ for subnet_id in sorted(subnet_ids):
296
+ try:
297
+ subnet_info = client.rpc.subnet.get_subnet_info(subnet_id)
298
+ if subnet_info:
299
+ subnets_info.append(subnet_info)
300
+ except Exception as e:
301
+ logger.warning(
302
+ f"Failed to get subnet {subnet_id} info: {e}"
303
+ )
304
+ continue
305
+
306
+ if subnets_info:
307
+ # Convert SubnetInfo models to dicts for display
308
+ subnet_data = []
309
+ for subnet in subnets_info:
310
+ # Use model_dump() to get all fields
311
+ subnet_dict = subnet.model_dump()
312
+
313
+ # Decode byte fields to strings
314
+ for field in ["name", "repo", "description", "misc"]:
315
+ if field in subnet_dict and isinstance(
316
+ subnet_dict[field], bytes
317
+ ):
318
+ try:
319
+ decoded = subnet_dict[field].decode("utf-8")
320
+ subnet_dict[field] = decoded if decoded else ""
321
+ except:
322
+ subnet_dict[field] = ""
323
+
324
+ # Convert state enum to string if needed
325
+ if "state" in subnet_dict and not isinstance(
326
+ subnet_dict["state"], str
327
+ ):
328
+ if hasattr(subnet_dict["state"], "value"):
329
+ subnet_dict["state"] = subnet_dict["state"].value
330
+ elif hasattr(subnet_dict["state"], "name"):
331
+ subnet_dict["state"] = subnet_dict["state"].name
332
+
333
+ subnet_data.append(subnet_dict)
334
+ else:
335
+ subnet_data = []
336
+
337
+ # Display results AFTER spinner is cleared
338
+ if subnet_data is None:
339
+ # Error case - already handled
340
+ pass
341
+ elif len(subnet_data) == 0:
342
+ # No subnets found - informational message
343
+ print_info(f"No subnets found for coldkey: {coldkey}")
344
+ else:
345
+ # Display the subnet list
346
+ display_subnet_list(subnet_data)
347
+ elif all_subnets:
348
+ # Use RPC method to get all subnets info
349
+ with HTCLILoadingContext("Fetching all subnets"):
350
+ all_subnets_models = client.rpc.subnet.get_all_subnets_info()
351
+
352
+ logger.info(f"Retrieved {len(all_subnets_models)} subnets from RPC")
353
+
354
+ # Convert Pydantic models to dicts for display - include all fields for comprehensive table
355
+ all_subnets_data = []
356
+ for subnet in all_subnets_models:
357
+ subnet_id = (
358
+ subnet.id
359
+ if hasattr(subnet, "id")
360
+ else (
361
+ subnet.get("id", "unknown")
362
+ if isinstance(subnet, dict)
363
+ else "unknown"
364
+ )
365
+ )
366
+ logger.debug(f"Processing subnet ID: {subnet_id}")
367
+ # Use model_dump() to get all fields, then decode byte fields
368
+ subnet_dict = subnet.model_dump()
369
+
370
+ # Decode byte fields to strings
371
+ for field in ["name", "repo", "description", "misc"]:
372
+ if field in subnet_dict and isinstance(subnet_dict[field], bytes):
373
+ try:
374
+ decoded = subnet_dict[field].decode("utf-8")
375
+ subnet_dict[field] = decoded if decoded else ""
376
+ except:
377
+ subnet_dict[field] = ""
378
+
379
+ # Convert state enum to string if needed
380
+ state_value = subnet_dict.get("state")
381
+ if state_value and not isinstance(state_value, str):
382
+ if hasattr(state_value, "value"):
383
+ subnet_dict["state"] = state_value.value
384
+ elif hasattr(state_value, "name"):
385
+ subnet_dict["state"] = state_value.name
386
+ else:
387
+ subnet_dict["state"] = str(state_value)
388
+ elif not state_value:
389
+ subnet_dict["state"] = "Unknown"
390
+
391
+ # Filter by active_only if specified
392
+ if active_only:
393
+ state_str = subnet_dict.get("state", "").lower()
394
+ if state_str != "active":
395
+ continue # Skip non-active subnets
396
+
397
+ all_subnets_data.append(subnet_dict)
398
+
399
+ # Display the subnet list
400
+ display_subnet_list(all_subnets_data)
401
+
402
+ except Exception as e:
403
+ # Use specific subnet error handling
404
+ if "subnet" in str(e).lower() or "network" in str(e).lower():
405
+ error = handle_substrate_error(e)
406
+ if not isinstance(
407
+ error, (SubnetError, SubnetActivationError, SubnetRegistrationError)
408
+ ):
409
+ error = SubnetError(f"Failed to list subnets: {str(e)}")
410
+ error.display()
411
+ else:
412
+ handle_and_display_error(e)
413
+
414
+
415
+ def get_subnet_handler(subnet_id: Optional[int]):
416
+ """Handle getting subnet information with proper error handling."""
417
+ try:
418
+ if subnet_id is None:
419
+ subnet_id = prompt_get_subnet()
420
+
421
+ if subnet_id < 0:
422
+ raise ValueError("Subnet ID must be a non-negative integer")
423
+
424
+ client = get_client()
425
+ with HTCLILoadingContext(f"Fetching data for subnet {subnet_id}..."):
426
+ result = client.rpc.subnet.get_subnet_data(subnet_id)
427
+ display_subnet_info(result.data)
428
+ except ValueError as e:
429
+ # Handle validation errors
430
+ from ...ui.display import print_error
431
+
432
+ print_error(f"Invalid subnet ID: {str(e)}")
433
+ except Exception as e:
434
+ # Use specific subnet error handling
435
+ if "subnet" in str(e).lower() or "not found" in str(e).lower():
436
+ error = handle_substrate_error(e)
437
+ if not isinstance(
438
+ error, (SubnetError, SubnetActivationError, SubnetRegistrationError)
439
+ ):
440
+ error = SubnetError(f"Failed to get subnet {subnet_id}: {str(e)}")
441
+ error.display()
442
+ else:
443
+ handle_and_display_error(e)
444
+
445
+
446
+ def list_nodes_handler(subnet_id: Optional[int]):
447
+ try:
448
+ if subnet_id is None:
449
+ subnet_id = prompt_list_nodes()
450
+ client = get_client()
451
+ with HTCLILoadingContext(f"Fetching nodes for subnet {subnet_id}..."):
452
+ result = client.rpc.subnet.get_subnet_nodes(subnet_id)
453
+ display_subnet_nodes(result["data"])
454
+ except Exception as e:
455
+ handle_and_display_error(e)
456
+
457
+
458
+ def check_activation_handler(subnet_id: Optional[int]):
459
+ try:
460
+ if subnet_id is None:
461
+ subnet_id = prompt_check_activation()
462
+ client = get_client()
463
+ with HTCLILoadingContext(f"Checking activation for subnet {subnet_id}..."):
464
+ result = client.rpc.subnet.check_subnet_activation_requirements(subnet_id)
465
+ display_activation_requirements(result["data"])
466
+ except Exception as e:
467
+ handle_and_display_error(e)
468
+
469
+
470
+ # Write Handlers
471
+ def register_subnet_handler(
472
+ coldkey: Optional[str] = None,
473
+ name: Optional[str] = None,
474
+ repo: Optional[str] = None,
475
+ description: Optional[str] = None,
476
+ misc: Optional[str] = None,
477
+ min_stake: Optional[float] = None,
478
+ max_stake: Optional[float] = None,
479
+ max_cost: Optional[float] = None,
480
+ delegate_stake_percentage: Optional[int] = None,
481
+ initial_coldkeys: Optional[str] = None,
482
+ key_types: Optional[str] = None,
483
+ bootnodes: Optional[str] = None,
484
+ ):
485
+ """Handle subnet registration with simplified flow."""
486
+ try:
487
+ # Collect missing parameters interactively
488
+ try:
489
+ # Note: Fields like churn_limit, queue_epochs, max_registered_nodes, etc.
490
+ # are NOT part of RegistrationSubnetData and are ignored during registration.
491
+ # They can be set separately after registration using owner update commands.
492
+ request = prompt_register_subnet(
493
+ name=name,
494
+ repo=repo,
495
+ description=description,
496
+ misc=misc,
497
+ min_stake=min_stake,
498
+ max_stake=max_stake,
499
+ max_cost=max_cost,
500
+ delegate_stake_percentage=delegate_stake_percentage,
501
+ initial_coldkeys=initial_coldkeys,
502
+ key_types=key_types,
503
+ bootnodes=bootnodes,
504
+ )
505
+ except (KeyboardInterrupt, typer.Abort):
506
+ # Re-raise cancellation exceptions to properly exit
507
+ raise
508
+ except Exception as ve:
509
+ # Handle Pydantic ValidationError specifically (must check before ValueError)
510
+ from pydantic import ValidationError
511
+
512
+ from ...errors.handlers import (
513
+ display_pydantic_validation_error,
514
+ get_validation_suggestions,
515
+ )
516
+
517
+ if isinstance(ve, ValidationError):
518
+ suggestions = get_validation_suggestions(ve)
519
+ display_pydantic_validation_error(
520
+ ve,
521
+ model_name="SubnetRegisterRequest",
522
+ suggestions=suggestions,
523
+ )
524
+ return
525
+ # Handle other ValueError exceptions
526
+ elif isinstance(ve, ValueError):
527
+ from ...ui.display import print_error
528
+
529
+ print_error(f"Invalid input: {str(ve)}")
530
+ return
531
+ # Re-raise if it's not a handled exception
532
+ raise
533
+
534
+ # Get client and check connection status
535
+ from ...ui.display import print_error, print_info
536
+
537
+ client = get_client()
538
+
539
+ # Check if substrate connection is available
540
+ if not client.substrate:
541
+ print_error("Blockchain connection failed!")
542
+ raise RuntimeError("Not connected to blockchain")
543
+
544
+ # Test basic connectivity
545
+ try:
546
+ _ = client.substrate.properties
547
+ except Exception as conn_error:
548
+ print_error(f"Blockchain connectivity failed: {str(conn_error)}")
549
+ raise RuntimeError(
550
+ f"Blockchain connectivity failed: {str(conn_error)}"
551
+ ) from conn_error
552
+
553
+ # Get keypair using provided coldkey or prompt for it
554
+ if coldkey:
555
+ # Resolve coldkey from provided input (address or wallet name)
556
+ from ...utils.wallet.core import resolve_coldkey_and_get_keypair
557
+
558
+ try:
559
+ wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
560
+ except ValueError as e:
561
+ print_error(f"Failed to resolve coldkey '{coldkey}': {str(e)}")
562
+ print_error("Please provide a valid coldkey wallet name or address.")
563
+ raise
564
+ else:
565
+ # Fall back to interactive prompting
566
+ wallet_name, keypair = retrieve_wallet_with_validation(
567
+ wallet_type="coldkey",
568
+ purpose="sign the transaction",
569
+ only_existing_wallets=True,
570
+ )
571
+
572
+ from ...ui.display import HTCLIConsole
573
+
574
+ console = HTCLIConsole()
575
+
576
+ # Query current registration cost
577
+ print_info("💰 Querying current subnet registration cost...")
578
+ current_cost = client.rpc.chain.get_subnet_registration_cost()
579
+
580
+ if current_cost:
581
+ cost_tensor = current_cost / 1e18
582
+ console.print("")
583
+ console.print("[bold yellow]" + "=" * 70 + "[/]")
584
+ console.print("[bold yellow]⚠️ SUBNET REGISTRATION COST NOTICE[/]")
585
+ console.print("[bold yellow]" + "=" * 70 + "[/]")
586
+ console.print(
587
+ f"[htcli.warning]Current registration cost: [bold]{cost_tensor:,.2f} TENSOR[/bold][/]"
588
+ )
589
+ console.print(
590
+ f"[htcli.info]Your max_cost: {request.max_cost / 1e18:,.2f} TENSOR[/]"
591
+ )
592
+ console.print("")
593
+
594
+ if request.max_cost < current_cost:
595
+ console.print(
596
+ "[bold red]❌ ERROR: Your max_cost is LOWER than current cost![/]"
597
+ )
598
+ console.print(f"[htcli.error]Required: {cost_tensor:,.2f} TENSOR[/]")
599
+ console.print(
600
+ f"[htcli.error]Your max: {request.max_cost / 1e18:,.2f} TENSOR[/]"
601
+ )
602
+ console.print("")
603
+ console.print("[htcli.info]💡 To proceed, increase --max-cost:[/]")
604
+ console.print(f"[htcli.info] --max-cost {int(current_cost * 1.1)}[/]")
605
+ console.print("[bold yellow]" + "=" * 70 + "[/]")
606
+ print_error("Registration aborted due to insufficient max_cost")
607
+ return
608
+
609
+ console.print("[htcli.success]✅ Your max_cost is sufficient[/]")
610
+ console.print(f"[htcli.info]Expected cost: ~{cost_tensor:,.2f} TENSOR[/]")
611
+ console.print(
612
+ f"[htcli.info]Buffer: {((request.max_cost - current_cost) / 1e18):,.2f} TENSOR[/]"
613
+ )
614
+ console.print("[bold yellow]" + "=" * 70 + "[/]")
615
+ console.print("")
616
+
617
+ # Check balance using wallet's stored address (not keypair's derived address)
618
+ # NOTE: Commented out to let the chain return its own error
619
+ # user_balance = client.rpc.chain.get_account_balance(wallet_address)
620
+ # if user_balance is not None:
621
+ # balance_tensor = user_balance / 1e18
622
+ # console.print(
623
+ # f"[htcli.info]Your balance: {balance_tensor:,.2f} TENSOR[/]"
624
+ # )
625
+ # if user_balance < current_cost:
626
+ # console.print("[bold red]❌ INSUFFICIENT BALANCE![/]")
627
+ # console.print(
628
+ # f"[htcli.error]Required: {cost_tensor:,.2f} TENSOR[/]"
629
+ # )
630
+ # console.print(
631
+ # f"[htcli.error]Your balance: {balance_tensor:,.2f} TENSOR[/]"
632
+ # )
633
+ # console.print(
634
+ # f"[htcli.error]Shortfall: {(cost_tensor - balance_tensor):,.2f} TENSOR[/]"
635
+ # )
636
+ # console.print("")
637
+ # console.print("[htcli.info]💡 To proceed with registration:[/htcli.info]")
638
+ # console.print(f"[htcli.info] • Transfer at least {(cost_tensor - balance_tensor):,.2f} TENSOR to this wallet[/htcli.info]")
639
+ # console.print(f"[htcli.info] • Or use a different wallet with sufficient balance[/htcli.info]")
640
+ # return
641
+ # console.print(
642
+ # "[htcli.success]✅ Sufficient balance for registration[/]"
643
+ # )
644
+ # console.print("")
645
+
646
+ # Show registration summary before submitting
647
+ from ...ui.components import HTCLIPanel
648
+
649
+ console = HTCLIConsole()
650
+
651
+ # Format initial coldkeys info with max nodes per coldkey
652
+ initial_coldkeys_info = ""
653
+ if request.initial_coldkeys:
654
+ coldkey_count = len(request.initial_coldkeys)
655
+ total_max_nodes = sum(request.initial_coldkeys.values())
656
+
657
+ # Show summary: count and total max nodes
658
+ initial_coldkeys_info = f"{coldkey_count} coldkey(s)"
659
+
660
+ # If all have same value (default case), show simplified
661
+ unique_values = set(request.initial_coldkeys.values())
662
+ if len(unique_values) == 1:
663
+ max_nodes = list(unique_values)[0]
664
+ initial_coldkeys_info += f" (each can register up to {max_nodes} node{'s' if max_nodes > 1 else ''})"
665
+ else:
666
+ # Show range or detailed breakdown
667
+ min_nodes = min(request.initial_coldkeys.values())
668
+ max_nodes = max(request.initial_coldkeys.values())
669
+ if min_nodes == max_nodes:
670
+ initial_coldkeys_info += f" (each can register up to {max_nodes} node{'s' if max_nodes > 1 else ''})"
671
+ else:
672
+ initial_coldkeys_info += f" (can register {min_nodes}-{max_nodes} nodes each, total: {total_max_nodes} max nodes)"
673
+
674
+ initial_coldkeys_info += " (becomes permissionless after activation)"
675
+ else:
676
+ initial_coldkeys_info = "0 (becomes permissionless after activation)"
677
+
678
+ summary = f"""[htcli.accent]Subnet Registration Summary[/htcli.accent]
679
+
680
+ [htcli.value]Name:[/htcli.value] {request.name}
681
+ [htcli.value]Repository:[/htcli.value] {request.repo}
682
+ [htcli.value]Min Stake:[/htcli.value] {request.min_stake / 1e18:,.2f} TENSOR
683
+ [htcli.value]Max Stake:[/htcli.value] {request.max_stake / 1e18:,.2f} TENSOR
684
+ [htcli.value]Max Cost:[/htcli.value] {request.max_cost / 1e18:,.2f} TENSOR
685
+ [htcli.value]Initial Coldkeys:[/htcli.value] {initial_coldkeys_info}
686
+
687
+ [htcli.info]💡 Submitting to blockchain...[/htcli.info]
688
+ [htcli.info]💡 Note: Configuration fields (churn_limit, queue_epochs, etc.) can be set after registration using owner update commands.[/htcli.info]
689
+ """
690
+
691
+ panel = HTCLIPanel(
692
+ summary,
693
+ title="🚀 Submitting Subnet Registration",
694
+ border_style="htcli.info",
695
+ highlight=True,
696
+ )
697
+ panel.render(console.console)
698
+ console.print("")
699
+
700
+ with HTCLILoadingContext("Registering subnet on blockchain..."):
701
+ result = client.extrinsics.subnet.register_subnet(request, keypair=keypair)
702
+
703
+ # Display result with clear messages (not JSON)
704
+ if result.success:
705
+ resolved_block_number = getattr(result, "block_number", None)
706
+ block_hash = getattr(result, "block_hash", None)
707
+
708
+ if not resolved_block_number and block_hash:
709
+ try:
710
+ resolved = client.rpc.chain.get_block_number(block_hash=block_hash)
711
+ if resolved is not None:
712
+ resolved_block_number = resolved
713
+ except Exception as fetch_error:
714
+ logger.debug(
715
+ f"Unable to resolve block number for hash {block_hash}: {fetch_error}"
716
+ )
717
+
718
+ block_display = (
719
+ resolved_block_number if resolved_block_number else "Pending"
720
+ )
721
+ block_hash_display = block_hash or "N/A"
722
+
723
+ success_msg = f"""[htcli.success]✅ Subnet registered successfully![/htcli.success]
724
+
725
+ [htcli.value]Subnet ID:[/htcli.value] {result.subnet_id if hasattr(result, "subnet_id") else "Pending"}
726
+ [htcli.value]Transaction:[/htcli.value] {result.transaction_hash if hasattr(result, "transaction_hash") else "N/A"}
727
+ [htcli.value]Block:[/htcli.value] {block_display}
728
+ [htcli.value]Block Hash:[/htcli.value] {block_hash_display}
729
+
730
+ [htcli.info]💡 Your subnet is now registered! You can now register nodes.[/htcli.info]
731
+ """
732
+ success_panel = HTCLIPanel(
733
+ success_msg,
734
+ title="🎉 Registration Complete",
735
+ border_style="htcli.success",
736
+ highlight=True,
737
+ )
738
+ success_panel.render(console.console)
739
+ else:
740
+ # Format error message beautifully like success message
741
+ error_msg = result.error if hasattr(result, "error") else "Unknown error"
742
+
743
+ # Parse blockchain error name for better display
744
+ error_name = "Registration Failed"
745
+ error_description = error_msg
746
+ if ":" in error_msg:
747
+ parts = error_msg.split(":", 1)
748
+ error_name = parts[0].strip()
749
+ error_description = parts[1].strip()
750
+
751
+ # Get helpful tips based on error type - COMPREHENSIVE ERROR COVERAGE
752
+ tips = []
753
+
754
+ # Balance and cost errors
755
+ if "NotEnoughBalanceToRegisterSubnet" in error_msg:
756
+ tips = [
757
+ f"Your wallet needs approximately {request.max_cost / 1e18:,.2f} TENSOR",
758
+ "Transfer more TENSOR to your wallet before retrying",
759
+ "Or use a different wallet with sufficient balance",
760
+ ]
761
+ elif "CostGreaterThanMaxCost" in error_msg:
762
+ tips = [
763
+ "The registration cost exceeds your specified max_cost",
764
+ "Increase your --max-cost parameter (e.g., double it)",
765
+ "Registration costs increase as more subnets are created",
766
+ ]
767
+
768
+ # Stake validation errors
769
+ elif "InvalidSubnetMinStake" in error_msg:
770
+ tips = [
771
+ "min_stake must be between 100-250 TENSOR",
772
+ f"Current value: {request.min_stake / 1e18:,.2f} TENSOR",
773
+ "Use --min-stake flag with a value in this range",
774
+ ]
775
+ elif "InvalidSubnetMaxStake" in error_msg:
776
+ tips = [
777
+ "max_stake must be ≤ 1000 TENSOR (NetworkMaxStakeBalance)",
778
+ f"Current value: {request.max_stake / 1e18:,.2f} TENSOR",
779
+ "Use --max-stake flag with a lower value",
780
+ ]
781
+ elif "InvalidSubnetStakeParameters" in error_msg:
782
+ tips = [
783
+ "min_stake must be less than or equal to max_stake",
784
+ f"Current min_stake: {request.min_stake / 1e18:,.2f} TENSOR",
785
+ f"Current max_stake: {request.max_stake / 1e18:,.2f} TENSOR",
786
+ "Adjust your stake parameters to satisfy: min_stake ≤ max_stake",
787
+ ]
788
+
789
+ # Delegate percentage errors
790
+ elif (
791
+ "InvalidMinDelegateStakePercentage" in error_msg
792
+ or "InvalidDelegateStakePercentage" in error_msg
793
+ ):
794
+ tips = [
795
+ "delegate_percentage must be between 5% and 95%",
796
+ "Use --delegate-percentage flag (e.g., 20 for 20%)",
797
+ "This controls how much of emissions go to delegate stakers",
798
+ ]
799
+
800
+ # Node configuration errors
801
+ elif "InvalidMaxRegisteredNodes" in error_msg:
802
+ tips = [
803
+ "max_registered_nodes must be between 1 and 64",
804
+ f"Current value: {request.max_registered_nodes}",
805
+ "Blockchain enforces MaxMaxRegisteredNodes = 64",
806
+ ]
807
+ elif "InvalidMaxSubnetNodePenalties" in error_msg:
808
+ tips = [
809
+ "max_node_penalties must be within the allowed range",
810
+ "Check MinMaxSubnetNodePenalties and MaxMaxSubnetNodePenalties",
811
+ "Adjust --max-node-penalties parameter",
812
+ ]
813
+
814
+ # Bootnode errors
815
+ elif "BootnodesEmpty" in error_msg:
816
+ tips = [
817
+ "At least one bootnode is required for subnet registration",
818
+ "Use --bootnodes flag with valid multiaddr format",
819
+ "Example: /dns4/bootnode.example.com/tcp/30333/p2p/12D3KooW...",
820
+ ]
821
+ elif "TooManyBootnodes" in error_msg:
822
+ tips = [
823
+ "Too many bootnodes provided",
824
+ "Maximum allowed bootnodes: 32 (MaxBootnodes)",
825
+ "Remove some bootnode entries and retry",
826
+ ]
827
+
828
+ # Initial coldkeys errors
829
+ elif "InvalidSubnetRegistrationInitialColdkeys" in error_msg:
830
+ tips = [
831
+ "Initial coldkeys validation failed",
832
+ "Must provide at least 3 coldkeys (MinSubnetNodes = 3)",
833
+ "Each coldkey must have at least 1 registration slot",
834
+ "After subnet activation, it becomes permissionless automatically",
835
+ "The whitelist is removed and anyone can register nodes",
836
+ ]
837
+
838
+ # Epoch configuration errors
839
+ elif "InvalidChurnLimit" in error_msg:
840
+ tips = [
841
+ "churn_limit is outside the allowed range",
842
+ "Must be between MinChurnLimit and MaxChurnLimit",
843
+ "Adjust --churn-limit parameter",
844
+ ]
845
+ elif "InvalidRegistrationQueueEpochs" in error_msg:
846
+ tips = [
847
+ "subnet_node_queue_epochs is outside the allowed range",
848
+ "Must be between MinQueueEpochs and MaxQueueEpochs",
849
+ "Adjust --registration-queue-epochs parameter",
850
+ ]
851
+ elif "InvalidIdleClassificationEpochs" in error_msg:
852
+ tips = [
853
+ "idle_classification_epochs is outside the allowed range",
854
+ "Must be between MinIdleClassificationEpochs and MaxIdleClassificationEpochs",
855
+ "Adjust --activation-grace-epochs parameter",
856
+ ]
857
+ elif "InvalidIncludedClassificationEpochs" in error_msg:
858
+ tips = [
859
+ "included_classification_epochs is outside the allowed range",
860
+ "Must be between MinIncludedClassificationEpochs and MaxIncludedClassificationEpochs",
861
+ "Adjust --included-classification-epochs parameter",
862
+ ]
863
+
864
+ # Uniqueness constraint errors
865
+ elif "SubnetNameExist" in error_msg:
866
+ tips = [
867
+ "This subnet name is already taken by another subnet",
868
+ "Choose a unique name for your subnet",
869
+ "Use --name flag with a different value",
870
+ ]
871
+ elif "SubnetRepoExist" in error_msg:
872
+ tips = [
873
+ "This repository URL is already registered to another subnet",
874
+ "Use a different repository URL",
875
+ "Each subnet must have a unique repository",
876
+ ]
877
+
878
+ # Capacity errors
879
+ elif "MaxSubnets" in error_msg:
880
+ tips = [
881
+ "Maximum number of subnets reached on the network",
882
+ "Wait for inactive subnets to be removed",
883
+ "Inactive subnets are periodically cleaned up by the protocol",
884
+ ]
885
+
886
+ # Generic fallback
887
+ else:
888
+ tips = [
889
+ "Check all parameter values are within valid ranges",
890
+ "Ensure wallet has sufficient balance",
891
+ "Verify network connection is stable",
892
+ "Review the documentation for parameter requirements",
893
+ ]
894
+
895
+ # Build formatted error message
896
+ tips_text = "\n".join([f" • {tip}" for tip in tips])
897
+
898
+ error_display = f"""[htcli.error]❌ {error_name}[/htcli.error]
899
+
900
+ [htcli.value]Error:[/htcli.value] {error_description}
901
+
902
+ [htcli.info]💡 How to fix this:[/htcli.info]
903
+ {tips_text}
904
+ """
905
+
906
+ error_panel = HTCLIPanel(
907
+ error_display,
908
+ title="⚠️ Subnet Registration Failed",
909
+ border_style="htcli.error",
910
+ highlight=True,
911
+ )
912
+ error_panel.render(console.console)
913
+ console.print("")
914
+ except ValueError as e:
915
+ # Handle validation errors - provide more specific error messages
916
+ error_msg = str(e)
917
+ if "incompatible private key format" in error_msg:
918
+ from ...ui.display import print_error
919
+
920
+ print_error(f"Wallet format error: {error_msg}")
921
+ print_error(
922
+ "Please recreate your wallet or use a different wallet with compatible format."
923
+ )
924
+ else:
925
+ from ...ui.display import print_error
926
+
927
+ print_error(f"Validation error: {error_msg}")
928
+ except (KeyboardInterrupt, typer.Abort):
929
+ # Re-raise cancellation exceptions to properly exit
930
+ raise
931
+ except RuntimeError as e:
932
+ # Handle connection errors
933
+ from ...ui.display import print_error
934
+
935
+ print_error(f"Connection Error: {str(e)}")
936
+ except Exception as e:
937
+ # Check for wallet-related errors first and provide better context
938
+ error_str = str(e).lower()
939
+ if any(
940
+ pattern in error_str
941
+ for pattern in [
942
+ "wallet",
943
+ "key",
944
+ "signature",
945
+ "password",
946
+ "coldkey",
947
+ "hotkey",
948
+ ]
949
+ ):
950
+ from ...errors.handlers import handle_wallet_error
951
+ from ...ui.display import print_error
952
+
953
+ print_error(f"Wallet error: {str(e)}")
954
+ wallet_error = handle_wallet_error(e, operation="subnet registration")
955
+ wallet_error.display()
956
+ return
957
+
958
+ # Use specific subnet registration error handling
959
+ if "registration" in error_str or "subnet" in error_str:
960
+ error = handle_substrate_error(e)
961
+ if not isinstance(error, SubnetRegistrationError):
962
+ error = SubnetRegistrationError(f"Failed to register subnet: {str(e)}")
963
+ error.display()
964
+ else:
965
+ handle_and_display_error(e)
966
+
967
+
968
+ def activate_subnet_handler(subnet_id: Optional[int], coldkey: Optional[str] = None):
969
+ """Handle subnet activation with proper error handling."""
970
+ result_dict = None # Initialize to avoid UnboundLocalError
971
+ try:
972
+ if subnet_id is None:
973
+ subnet_id = prompt_activate_subnet()
974
+
975
+ if subnet_id < 0:
976
+ raise ValueError("Subnet ID must be a non-negative integer")
977
+
978
+ # Get signing wallet using provided coldkey or prompt for it
979
+ if coldkey:
980
+ # Resolve coldkey from provided input (address or wallet name)
981
+ from ...utils.wallet.core import resolve_coldkey_and_get_keypair
982
+
983
+ try:
984
+ wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
985
+ except ValueError as e:
986
+ from ...ui.display import print_error
987
+
988
+ print_error(f"Failed to resolve coldkey '{coldkey}': {str(e)}")
989
+ print_error("Please provide a valid coldkey wallet name or address.")
990
+ raise
991
+ else:
992
+ # Fall back to interactive prompting
993
+ wallet_name, keypair = retrieve_wallet_with_validation(
994
+ wallet_type="coldkey", purpose="sign the activation transaction"
995
+ )
996
+
997
+ client = get_client()
998
+
999
+ # Ensure connection is established
1000
+ if not client.connect():
1001
+ from ...ui.display import print_error
1002
+
1003
+ print_error("Failed to connect to blockchain")
1004
+ return
1005
+
1006
+ # Check if substrate connection is available
1007
+ if not client.substrate:
1008
+ from ...ui.display import print_error
1009
+
1010
+ print_error("Blockchain connection failed!")
1011
+ raise RuntimeError("Not connected to blockchain")
1012
+
1013
+ # Check if extrinsics layer is initialized
1014
+ if not client.extrinsics:
1015
+ from ...ui.display import print_error
1016
+
1017
+ print_error("Extrinsics layer not initialized. Connection may have failed.")
1018
+ raise RuntimeError("Extrinsics layer not available")
1019
+
1020
+ from ...models.requests.subnet import SubnetActivateRequest
1021
+
1022
+ request = SubnetActivateRequest(subnet_id=subnet_id)
1023
+
1024
+ with HTCLILoadingContext(f"Activating subnet {subnet_id}..."):
1025
+ result = client.extrinsics.subnet.activate_subnet(request, keypair=keypair)
1026
+
1027
+ # Convert SimpleExtrinsicResponse to dict for display
1028
+ result_dict = {
1029
+ "message": result.message or f"Subnet {subnet_id} activated successfully",
1030
+ "transaction_hash": result.transaction_hash,
1031
+ "block_number": result.block_number,
1032
+ "block_hash": result.block_hash,
1033
+ "success": result.success,
1034
+ }
1035
+ if result.error:
1036
+ result_dict["error"] = result.error
1037
+
1038
+ if not result.success:
1039
+ error_msg = result_dict.get("error", "Activation failed")
1040
+
1041
+ # Handle specific activation errors with helpful messages
1042
+ from ...ui.components import HTCLIPanel
1043
+ from ...ui.display import HTCLIConsole, print_error, print_info
1044
+
1045
+ console = HTCLIConsole()
1046
+
1047
+ if "NotSubnetOwner" in error_msg:
1048
+ error_panel = HTCLIPanel(
1049
+ f"Only the subnet owner can activate subnet {subnet_id}.\n\n"
1050
+ f"💡 Make sure you're using the coldkey wallet that registered this subnet.",
1051
+ title="❌ Permission Denied",
1052
+ border_style="htcli.error",
1053
+ highlight=True,
1054
+ )
1055
+ error_panel.render(console.console)
1056
+ return
1057
+ elif "InvalidSubnetId" in error_msg:
1058
+ error_panel = HTCLIPanel(
1059
+ f"Subnet {subnet_id} does not exist.\n\n"
1060
+ f"💡 Check the subnet ID and try again.\n"
1061
+ f" Use: htcli subnet list to see available subnets",
1062
+ title="❌ Invalid Subnet ID",
1063
+ border_style="htcli.error",
1064
+ highlight=True,
1065
+ )
1066
+ error_panel.render(console.console)
1067
+ return
1068
+ elif "SubnetActivatedAlready" in error_msg:
1069
+ error_panel = HTCLIPanel(
1070
+ f"Subnet {subnet_id} is already activated.\n\n"
1071
+ f"💡 The subnet is already in active state.\n"
1072
+ f" Use: htcli subnet info --subnet-id {subnet_id} to check subnet status",
1073
+ title="✅ Already Activated",
1074
+ border_style="htcli.info",
1075
+ highlight=True,
1076
+ )
1077
+ error_panel.render(console.console)
1078
+ return
1079
+ elif "MinSubnetRegistrationEpochsNotMet" in error_msg:
1080
+ error_panel = HTCLIPanel(
1081
+ f"Subnet {subnet_id} cannot be activated yet.\n\n"
1082
+ f"⏱️ Minimum registration epochs not met.\n"
1083
+ f" Subnets must wait a minimum number of epochs after registration before activation.\n\n"
1084
+ f"💡 Wait a few more epochs and try again once the minimum has elapsed.",
1085
+ title="⏳ Too Early to Activate",
1086
+ border_style="htcli.warning",
1087
+ highlight=True,
1088
+ )
1089
+ error_panel.render(console.console)
1090
+ return
1091
+ elif "SubnetActivationConditionsNotMetYet" in error_msg:
1092
+ # Show error message and summarize requirements
1093
+ error_panel = HTCLIPanel(
1094
+ f"Subnet {subnet_id} cannot be activated yet because activation conditions are not met.",
1095
+ title="⚠️ Activation Conditions Not Met",
1096
+ border_style="htcli.error",
1097
+ highlight=True,
1098
+ )
1099
+ error_panel.render(console.console)
1100
+ console.print("")
1101
+
1102
+ requirements_panel = HTCLIPanel(
1103
+ ACTIVATION_REQUIREMENTS_TEXT,
1104
+ title="🧮 What You Need",
1105
+ border_style="htcli.info",
1106
+ highlight=True,
1107
+ )
1108
+ requirements_panel.render(console.console)
1109
+
1110
+ next_steps_panel = HTCLIPanel(
1111
+ ACTIVATION_NEXT_STEPS_TEXT,
1112
+ title="🚀 Getting Ready",
1113
+ border_style="htcli.info",
1114
+ highlight=True,
1115
+ )
1116
+ console.print("")
1117
+ next_steps_panel.render(console.console)
1118
+ return
1119
+ elif "SubnetDeactivated" in error_msg:
1120
+ # Subnet was removed during activation attempt (instead of being activated)
1121
+ # This happens when activation conditions aren't met during enactment period
1122
+ removal_reason = (
1123
+ getattr(result, "removal_reason", None) if result else None
1124
+ )
1125
+ reason_display = removal_reason or "activation conditions not met"
1126
+
1127
+ error_panel = HTCLIPanel(
1128
+ f"⚠️ Subnet {subnet_id} was **removed** instead of being activated.\n\n"
1129
+ f"📋 **Reason**: {reason_display}\n\n"
1130
+ f"This typically happens when:\n"
1131
+ f" • The subnet didn't meet minimum node requirements\n"
1132
+ f" • The subnet didn't have enough delegate stake\n"
1133
+ f" • The enactment period expired\n\n"
1134
+ f"💡 To try again, you'll need to register a new subnet and ensure\n"
1135
+ f" all activation requirements are met before the enactment deadline.",
1136
+ title="❌ Subnet Removed During Activation",
1137
+ border_style="htcli.error",
1138
+ highlight=True,
1139
+ )
1140
+ error_panel.render(console.console)
1141
+ return
1142
+ else:
1143
+ # Generic error - show the error message
1144
+ from ...ui.display import print_error
1145
+
1146
+ print_error(error_msg)
1147
+ return
1148
+
1149
+ display_generic_success(result_dict)
1150
+ except ValueError as e:
1151
+ # Handle validation errors
1152
+ from ...ui.display import print_error
1153
+
1154
+ print_error(f"Invalid subnet ID: {str(e)}")
1155
+ except Exception as e:
1156
+ # Use specific subnet activation error handling
1157
+ error_msg = str(e)
1158
+
1159
+ from ...ui.components import HTCLIPanel
1160
+ from ...ui.display import HTCLIConsole, print_error, print_info
1161
+
1162
+ console = HTCLIConsole()
1163
+
1164
+ # Handle specific activation errors with helpful messages
1165
+ if "NotSubnetOwner" in error_msg:
1166
+ error_panel = HTCLIPanel(
1167
+ f"Only the subnet owner can activate subnet {subnet_id}.\n\n"
1168
+ f"💡 Make sure you're using the coldkey wallet that registered this subnet.",
1169
+ title="❌ Permission Denied",
1170
+ border_style="htcli.error",
1171
+ highlight=True,
1172
+ )
1173
+ error_panel.render(console.console)
1174
+ return
1175
+ elif "InvalidSubnetId" in error_msg:
1176
+ error_panel = HTCLIPanel(
1177
+ f"Subnet {subnet_id} does not exist.\n\n"
1178
+ f"💡 Check the subnet ID and try again.\n"
1179
+ f" Use: htcli subnet list to see available subnets",
1180
+ title="❌ Invalid Subnet ID",
1181
+ border_style="htcli.error",
1182
+ highlight=True,
1183
+ )
1184
+ error_panel.render(console.console)
1185
+ return
1186
+ elif "SubnetActivatedAlready" in error_msg:
1187
+ error_panel = HTCLIPanel(
1188
+ f"Subnet {subnet_id} is already activated.\n\n"
1189
+ f"💡 The subnet is already in active state.\n"
1190
+ f" Use: htcli subnet info --subnet-id {subnet_id} to check subnet status",
1191
+ title="✅ Already Activated",
1192
+ border_style="htcli.info",
1193
+ highlight=True,
1194
+ )
1195
+ error_panel.render(console.console)
1196
+ return
1197
+ elif "MinSubnetRegistrationEpochsNotMet" in error_msg:
1198
+ error_panel = HTCLIPanel(
1199
+ f"Subnet {subnet_id} cannot be activated yet.\n\n"
1200
+ f"⏱️ Minimum registration epochs not met.\n"
1201
+ f" Subnets must wait a minimum number of epochs after registration before activation.\n\n"
1202
+ f"💡 Wait a few more epochs and try again once the minimum has elapsed.",
1203
+ title="⏳ Too Early to Activate",
1204
+ border_style="htcli.warning",
1205
+ highlight=True,
1206
+ )
1207
+ error_panel.render(console.console)
1208
+ return
1209
+ elif "SubnetActivationConditionsNotMetYet" in error_msg:
1210
+ # Show error message and summarize requirements
1211
+ error_panel = HTCLIPanel(
1212
+ f"Subnet {subnet_id} cannot be activated yet because activation conditions are not met.",
1213
+ title="⚠️ Activation Conditions Not Met",
1214
+ border_style="htcli.error",
1215
+ highlight=True,
1216
+ )
1217
+ error_panel.render(console.console)
1218
+ console.print("")
1219
+
1220
+ requirements_panel = HTCLIPanel(
1221
+ ACTIVATION_REQUIREMENTS_TEXT,
1222
+ title="🧮 What You Need",
1223
+ border_style="htcli.info",
1224
+ highlight=True,
1225
+ )
1226
+ requirements_panel.render(console.console)
1227
+
1228
+ next_steps_panel = HTCLIPanel(
1229
+ ACTIVATION_NEXT_STEPS_TEXT,
1230
+ title="🚀 Getting Ready",
1231
+ border_style="htcli.info",
1232
+ highlight=True,
1233
+ )
1234
+ console.print("")
1235
+ next_steps_panel.render(console.console)
1236
+ return
1237
+ else:
1238
+ # Generic error - show the error message
1239
+ from ...ui.display import print_error
1240
+
1241
+ print_error(error_msg)
1242
+ handle_and_display_error(e)
1243
+
1244
+
1245
+ def pause_subnet_handler(subnet_id: Optional[int], coldkey: Optional[str] = None):
1246
+ try:
1247
+ if subnet_id is None:
1248
+ subnet_id = prompt_pause_subnet()
1249
+
1250
+ # Get signing wallet using comprehensive wallet retrieval mechanism
1251
+ if coldkey:
1252
+ from ...utils.wallet.core import resolve_coldkey_and_get_keypair
1253
+
1254
+ wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
1255
+ else:
1256
+ wallet_name, keypair = retrieve_wallet_with_validation(
1257
+ wallet_type="coldkey",
1258
+ purpose="sign the transaction",
1259
+ only_existing_wallets=True,
1260
+ )
1261
+
1262
+ client = get_client()
1263
+ if not client.connect():
1264
+ raise RuntimeError("Could not connect to blockchain")
1265
+
1266
+ # Create request object
1267
+ from ...models.requests.subnet import SubnetActivateRequest
1268
+
1269
+ request = SubnetActivateRequest(subnet_id=subnet_id)
1270
+
1271
+ with HTCLILoadingContext(f"Pausing subnet {subnet_id}..."):
1272
+ result = client.extrinsics.subnet.owner_pause_subnet(request, keypair)
1273
+
1274
+ # Check for errors in result
1275
+ if isinstance(result, dict) and not result.get("success", False):
1276
+ error_msg = result.get("error", "Pause failed")
1277
+ if handle_subnet_error(error_msg, subnet_id, "pause", client):
1278
+ return
1279
+
1280
+ display_generic_success(result)
1281
+ except Exception as e:
1282
+ error_msg = str(e)
1283
+ if not handle_subnet_error(error_msg, subnet_id, "pause"):
1284
+ handle_and_display_error(e)
1285
+
1286
+
1287
+ def unpause_subnet_handler(subnet_id: Optional[int], coldkey: Optional[str] = None):
1288
+ try:
1289
+ if subnet_id is None:
1290
+ subnet_id = prompt_unpause_subnet()
1291
+ # Get signing wallet using comprehensive wallet retrieval mechanism
1292
+ if coldkey:
1293
+ from ...utils.wallet.core import resolve_coldkey_and_get_keypair
1294
+
1295
+ wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
1296
+ else:
1297
+ wallet_name, keypair = retrieve_wallet_with_validation(
1298
+ wallet_type="coldkey",
1299
+ purpose="sign the transaction",
1300
+ only_existing_wallets=True,
1301
+ )
1302
+
1303
+ client = get_client()
1304
+ if not client.connect():
1305
+ raise RuntimeError("Could not connect to blockchain")
1306
+
1307
+ # Create request object
1308
+ from ...models.requests.subnet import SubnetActivateRequest
1309
+
1310
+ request = SubnetActivateRequest(subnet_id=subnet_id)
1311
+
1312
+ with HTCLILoadingContext(f"Unpausing subnet {subnet_id}..."):
1313
+ result = client.extrinsics.subnet.owner_unpause_subnet(request, keypair)
1314
+
1315
+ # Check for errors in result
1316
+ if isinstance(result, dict) and not result.get("success", False):
1317
+ error_msg = result.get("error", "Unpause failed")
1318
+ if handle_subnet_error(error_msg, subnet_id, "unpause", client):
1319
+ return
1320
+
1321
+ display_generic_success(result)
1322
+ except Exception as e:
1323
+ error_msg = str(e)
1324
+ if not handle_subnet_error(error_msg, subnet_id, "unpause"):
1325
+ handle_and_display_error(e)
1326
+
1327
+
1328
+ # Owner Handlers
1329
+ def owner_update_handler(
1330
+ subnet_id: Optional[int] = None,
1331
+ new_name: Optional[str] = None,
1332
+ new_repo: Optional[str] = None,
1333
+ target_node_registrations: Optional[int] = None,
1334
+ node_burn_rate_alpha: Optional[int] = None,
1335
+ queue_immunity_epochs: Optional[int] = None,
1336
+ min_weight_decrease_threshold: Optional[int] = None,
1337
+ min_node_reputation: Optional[int] = None,
1338
+ absent_reputation_penalty: Optional[int] = None,
1339
+ included_reputation_boost: Optional[int] = None,
1340
+ below_min_weight_penalty: Optional[int] = None,
1341
+ non_attestor_penalty: Optional[int] = None,
1342
+ validator_absent_penalty: Optional[int] = None,
1343
+ validator_non_consensus_penalty: Optional[int] = None,
1344
+ non_consensus_attestor_penalty: Optional[int] = None,
1345
+ coldkey: Optional[str] = None,
1346
+ ):
1347
+ """Unified handler for updating subnet properties."""
1348
+ try:
1349
+
1350
+ # Prompt for missing information
1351
+ if subnet_id is None or (
1352
+ new_name is None
1353
+ and new_repo is None
1354
+ and target_node_registrations is None
1355
+ and node_burn_rate_alpha is None
1356
+ and queue_immunity_epochs is None
1357
+ and min_weight_decrease_threshold is None
1358
+ and min_node_reputation is None
1359
+ and absent_reputation_penalty is None
1360
+ and included_reputation_boost is None
1361
+ and below_min_weight_penalty is None
1362
+ and non_attestor_penalty is None
1363
+ and validator_absent_penalty is None
1364
+ and validator_non_consensus_penalty is None
1365
+ and non_consensus_attestor_penalty is None
1366
+ ):
1367
+ from .prompts import prompt_owner_update_extended
1368
+
1369
+ prompt_result = prompt_owner_update_extended(
1370
+ subnet_id,
1371
+ new_name,
1372
+ new_repo,
1373
+ target_node_registrations,
1374
+ node_burn_rate_alpha,
1375
+ queue_immunity_epochs,
1376
+ min_weight_decrease_threshold,
1377
+ min_node_reputation,
1378
+ absent_reputation_penalty,
1379
+ included_reputation_boost,
1380
+ below_min_weight_penalty,
1381
+ non_attestor_penalty,
1382
+ validator_absent_penalty,
1383
+ validator_non_consensus_penalty,
1384
+ non_consensus_attestor_penalty,
1385
+ )
1386
+ subnet_id = prompt_result.get("subnet_id", subnet_id)
1387
+ new_name = prompt_result.get("new_name", new_name)
1388
+ new_repo = prompt_result.get("new_repo", new_repo)
1389
+ target_node_registrations = prompt_result.get(
1390
+ "target_node_registrations", target_node_registrations
1391
+ )
1392
+ node_burn_rate_alpha = prompt_result.get(
1393
+ "node_burn_rate_alpha", node_burn_rate_alpha
1394
+ )
1395
+ queue_immunity_epochs = prompt_result.get(
1396
+ "queue_immunity_epochs", queue_immunity_epochs
1397
+ )
1398
+ min_weight_decrease_threshold = prompt_result.get(
1399
+ "min_weight_decrease_threshold", min_weight_decrease_threshold
1400
+ )
1401
+ min_node_reputation = prompt_result.get(
1402
+ "min_node_reputation", min_node_reputation
1403
+ )
1404
+ absent_reputation_penalty = prompt_result.get(
1405
+ "absent_reputation_penalty", absent_reputation_penalty
1406
+ )
1407
+ included_reputation_boost = prompt_result.get(
1408
+ "included_reputation_boost", included_reputation_boost
1409
+ )
1410
+ below_min_weight_penalty = prompt_result.get(
1411
+ "below_min_weight_penalty", below_min_weight_penalty
1412
+ )
1413
+ non_attestor_penalty = prompt_result.get(
1414
+ "non_attestor_penalty", non_attestor_penalty
1415
+ )
1416
+ validator_absent_penalty = prompt_result.get(
1417
+ "validator_absent_penalty", validator_absent_penalty
1418
+ )
1419
+ validator_non_consensus_penalty = prompt_result.get(
1420
+ "validator_non_consensus_penalty", validator_non_consensus_penalty
1421
+ )
1422
+ non_consensus_attestor_penalty = prompt_result.get(
1423
+ "non_consensus_attestor_penalty", non_consensus_attestor_penalty
1424
+ )
1425
+
1426
+ # Validate subnet_id is provided
1427
+ if subnet_id is None:
1428
+ print_error("Subnet ID is required. Exiting.")
1429
+ return
1430
+
1431
+ # Validate subnet_id is non-negative
1432
+ if subnet_id < 0:
1433
+ print_error("Subnet ID must be a non-negative integer.")
1434
+ return
1435
+
1436
+ # Check if all update fields are None (nothing to change)
1437
+ if (
1438
+ new_name is None
1439
+ and new_repo is None
1440
+ and target_node_registrations is None
1441
+ and node_burn_rate_alpha is None
1442
+ and queue_immunity_epochs is None
1443
+ and min_weight_decrease_threshold is None
1444
+ and min_node_reputation is None
1445
+ and absent_reputation_penalty is None
1446
+ and included_reputation_boost is None
1447
+ and below_min_weight_penalty is None
1448
+ and non_attestor_penalty is None
1449
+ and validator_absent_penalty is None
1450
+ and validator_non_consensus_penalty is None
1451
+ and non_consensus_attestor_penalty is None
1452
+ ):
1453
+ print_info("No fields were provided to be changed. Exiting.")
1454
+ return
1455
+
1456
+ # Get signing wallet using comprehensive wallet retrieval mechanism
1457
+ if coldkey:
1458
+ from ...utils.wallet.core import resolve_coldkey_and_get_keypair
1459
+
1460
+ wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
1461
+ else:
1462
+ wallet_name, keypair = retrieve_wallet_with_validation(
1463
+ wallet_type="coldkey",
1464
+ purpose="sign the transaction",
1465
+ only_existing_wallets=True,
1466
+ )
1467
+
1468
+ client = get_client()
1469
+ if not client.connect():
1470
+ raise RuntimeError("Could not connect to blockchain")
1471
+
1472
+ # Delegate all extrinsics to the client layer in one place
1473
+ with HTCLILoadingContext(f"Updating subnet {subnet_id} parameters..."):
1474
+ results = client.extrinsics.subnet.owner_update_parameters(
1475
+ subnet_id=subnet_id,
1476
+ new_name=new_name,
1477
+ new_repo=new_repo,
1478
+ target_node_registrations=target_node_registrations,
1479
+ node_burn_rate_alpha=node_burn_rate_alpha,
1480
+ queue_immunity_epochs=queue_immunity_epochs,
1481
+ min_weight_decrease_threshold=min_weight_decrease_threshold,
1482
+ min_node_reputation=min_node_reputation,
1483
+ absent_reputation_penalty=absent_reputation_penalty,
1484
+ included_reputation_boost=included_reputation_boost,
1485
+ below_min_weight_penalty=below_min_weight_penalty,
1486
+ non_attestor_penalty=non_attestor_penalty,
1487
+ validator_absent_penalty=validator_absent_penalty,
1488
+ validator_non_consensus_penalty=validator_non_consensus_penalty,
1489
+ non_consensus_attestor_penalty=non_consensus_attestor_penalty,
1490
+ keypair=keypair,
1491
+ )
1492
+
1493
+ # Check for any errors and let the shared handler convert them
1494
+ for field, result in results:
1495
+ if isinstance(result, dict) and not result.get("success", False):
1496
+ error_msg = result.get("error", "Update failed")
1497
+ if handle_subnet_error(error_msg, subnet_id, f"update {field}", client):
1498
+ return
1499
+
1500
+ # Display consolidated results in a single panel
1501
+ if results:
1502
+ from .display import display_subnet_updates_summary
1503
+
1504
+ display_subnet_updates_summary(subnet_id, results, client)
1505
+
1506
+ except Exception as e:
1507
+ error_msg = str(e)
1508
+ if not handle_subnet_error(error_msg, subnet_id, "update"):
1509
+ handle_and_display_error(e)
1510
+
1511
+
1512
+ def owner_update_name_handler(
1513
+ subnet_id: Optional[int], new_name: Optional[str], coldkey: Optional[str] = None
1514
+ ):
1515
+ try:
1516
+ from ...models.requests.subnet import SubnetUpdateRequest
1517
+
1518
+ if subnet_id is None or new_name is None:
1519
+ subnet_id, new_name = prompt_owner_update_name()
1520
+
1521
+ # Get signing wallet using comprehensive wallet retrieval mechanism
1522
+ if coldkey:
1523
+ from ...utils.wallet.core import resolve_coldkey_and_get_keypair
1524
+
1525
+ wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
1526
+ else:
1527
+ wallet_name, keypair = retrieve_wallet_with_validation(
1528
+ wallet_type="coldkey",
1529
+ purpose="sign the transaction",
1530
+ only_existing_wallets=True,
1531
+ )
1532
+
1533
+ client = get_client()
1534
+ if not client.connect():
1535
+ raise RuntimeError("Could not connect to blockchain")
1536
+
1537
+ with HTCLILoadingContext(f"Updating name for subnet {subnet_id}..."):
1538
+ request = SubnetUpdateRequest(subnet_id=subnet_id, value=new_name)
1539
+ result = client.extrinsics.subnet.owner_update_name(
1540
+ request, keypair=keypair
1541
+ )
1542
+
1543
+ # Check for errors in result
1544
+ if isinstance(result, dict) and not result.get("success", False):
1545
+ error_msg = result.get("error", "Update failed")
1546
+ if handle_subnet_error(error_msg, subnet_id, "update name", client):
1547
+ return
1548
+
1549
+ display_generic_success(result)
1550
+ except Exception as e:
1551
+ error_msg = str(e)
1552
+ if not handle_subnet_error(error_msg, subnet_id, "update name"):
1553
+ handle_and_display_error(e)
1554
+
1555
+
1556
+ def owner_update_repo_handler(
1557
+ subnet_id: Optional[int], new_repo: Optional[str], coldkey: Optional[str] = None
1558
+ ):
1559
+ try:
1560
+ from ...models.requests.subnet import SubnetUpdateRequest
1561
+
1562
+ if subnet_id is None or new_repo is None:
1563
+ subnet_id, new_repo = prompt_owner_update_repo()
1564
+
1565
+ # Get signing wallet using comprehensive wallet retrieval mechanism
1566
+ if coldkey:
1567
+ from ...utils.wallet.core import resolve_coldkey_and_get_keypair
1568
+
1569
+ wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
1570
+ else:
1571
+ wallet_name, keypair = retrieve_wallet_with_validation(
1572
+ wallet_type="coldkey",
1573
+ purpose="sign the transaction",
1574
+ only_existing_wallets=True,
1575
+ )
1576
+
1577
+ client = get_client()
1578
+ if not client.connect():
1579
+ raise RuntimeError("Could not connect to blockchain")
1580
+
1581
+ with HTCLILoadingContext(f"Updating repo for subnet {subnet_id}..."):
1582
+ request = SubnetUpdateRequest(subnet_id=subnet_id, value=new_repo)
1583
+ result = client.extrinsics.subnet.owner_update_repo(
1584
+ request, keypair=keypair
1585
+ )
1586
+
1587
+ # Check for errors in result
1588
+ if isinstance(result, dict) and not result.get("success", False):
1589
+ error_msg = result.get("error", "Update failed")
1590
+ if handle_subnet_error(error_msg, subnet_id, "update repository", client):
1591
+ return
1592
+
1593
+ display_generic_success(result)
1594
+ except Exception as e:
1595
+ error_msg = str(e)
1596
+ if not handle_subnet_error(error_msg, subnet_id, "update repository"):
1597
+ handle_and_display_error(e)
1598
+
1599
+
1600
+ def owner_transfer_handler(
1601
+ subnet_id: Optional[int], new_owner: Optional[str], coldkey: Optional[str] = None
1602
+ ):
1603
+ try:
1604
+ if subnet_id is None or new_owner is None:
1605
+ subnet_id, new_owner = prompt_owner_transfer()
1606
+
1607
+ # Resolve new_owner: can be either EVM address or wallet name
1608
+ from ...utils.blockchain.validation import validate_address
1609
+ from ...utils.wallet.crypto import get_wallet_info_by_name
1610
+ from ...utils.wallet.crypto import public_key_to_evm_address
1611
+
1612
+ if not new_owner:
1613
+ raise ValueError("New owner address cannot be empty")
1614
+
1615
+ new_owner = new_owner.strip()
1616
+
1617
+ # First, check if it's already a valid EVM address
1618
+ if validate_address(new_owner):
1619
+ # Normalize EVM addresses to lowercase (addresses are case-insensitive)
1620
+ new_owner = new_owner.lower()
1621
+ else:
1622
+ # Try to resolve as wallet name
1623
+ try:
1624
+ wallet_info = get_wallet_info_by_name(
1625
+ new_owner,
1626
+ is_hotkey=False, # Looking for coldkey wallets
1627
+ )
1628
+
1629
+ if wallet_info:
1630
+ # Try to get EVM address from wallet info
1631
+ resolved_address = wallet_info.get(
1632
+ "evm_address"
1633
+ ) or wallet_info.get("address")
1634
+
1635
+ # If we have an address, check if it's EVM format
1636
+ if resolved_address and validate_address(resolved_address):
1637
+ new_owner = resolved_address.lower()
1638
+ else:
1639
+ # Try to derive from public key
1640
+ public_key_hex = wallet_info.get("public_key")
1641
+ if public_key_hex:
1642
+ try:
1643
+ resolved_address = public_key_to_evm_address(
1644
+ bytes.fromhex(public_key_hex)
1645
+ )
1646
+ if validate_address(resolved_address):
1647
+ new_owner = resolved_address.lower()
1648
+ else:
1649
+ raise ValueError(
1650
+ f"Wallet '{new_owner}' does not have a valid EVM address. "
1651
+ "Please use an EVM address (0x...) directly."
1652
+ )
1653
+ except Exception as e:
1654
+ raise ValueError(
1655
+ f"Failed to derive EVM address from wallet '{new_owner}': {str(e)}"
1656
+ ) from e
1657
+ else:
1658
+ raise ValueError(
1659
+ f"Wallet '{new_owner}' does not have a valid EVM address. "
1660
+ "Please use an EVM address (0x...) directly."
1661
+ )
1662
+ else:
1663
+ raise ValueError(f"Wallet '{new_owner}' not found")
1664
+ except FileNotFoundError:
1665
+ raise ValueError(
1666
+ f"Wallet '{new_owner}' not found. "
1667
+ "Please provide either a wallet name or a valid EVM address (0x...)."
1668
+ )
1669
+ except ValueError:
1670
+ # Re-raise ValueError as-is (already has good error message)
1671
+ raise
1672
+ except Exception as e:
1673
+ raise ValueError(
1674
+ f"Failed to resolve new owner '{new_owner}': {str(e)}"
1675
+ ) from e
1676
+
1677
+ # Get signing wallet using comprehensive wallet retrieval mechanism
1678
+ if coldkey:
1679
+ from ...utils.wallet.core import resolve_coldkey_and_get_keypair
1680
+
1681
+ wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
1682
+ else:
1683
+ wallet_name, keypair = retrieve_wallet_with_validation(
1684
+ wallet_type="coldkey",
1685
+ purpose="sign the transfer transaction",
1686
+ only_existing_wallets=True,
1687
+ )
1688
+
1689
+ client = get_client()
1690
+ if not client.connect():
1691
+ raise RuntimeError("Could not connect to blockchain")
1692
+
1693
+ from ...models.requests.subnet import SubnetOwnershipTransferRequest
1694
+
1695
+ request = SubnetOwnershipTransferRequest(
1696
+ subnet_id=subnet_id, new_owner=new_owner
1697
+ )
1698
+
1699
+ # Use spinner with message that updates
1700
+ from ...ui.components import HTCLILoadingContext
1701
+
1702
+ with HTCLILoadingContext(
1703
+ f"Submitting transfer ownership transaction for subnet {subnet_id}..."
1704
+ ):
1705
+ result = client.extrinsics.subnet.transfer_subnet_ownership(
1706
+ request, keypair=keypair
1707
+ )
1708
+
1709
+ # Debug: Log what we received
1710
+ from ...utils.logging import get_logger
1711
+
1712
+ logger = get_logger(__name__)
1713
+ logger.debug(f"Transfer ownership result: {result}")
1714
+ logger.debug(
1715
+ f"Result keys: {list(result.keys()) if isinstance(result, dict) else 'not a dict'}"
1716
+ )
1717
+
1718
+ # Extract transaction details first
1719
+ tx_hash = result.get("transaction_hash") or result.get("extrinsic_hash")
1720
+ block_number = result.get("block_number")
1721
+ block_hash = result.get("block_hash")
1722
+
1723
+ # CRITICAL: Check for errors in result OR missing transaction details
1724
+ # If success is False OR we don't have a transaction hash, treat as failure
1725
+ if isinstance(result, dict) and (
1726
+ not result.get("success", False) or not tx_hash
1727
+ ):
1728
+ raw_error = result.get(
1729
+ "error", "Transfer failed - no transaction hash returned"
1730
+ )
1731
+ if not tx_hash and result.get("success", False):
1732
+ raw_error = "Transaction submitted but no transaction hash returned. The transaction may have failed."
1733
+
1734
+ # Try to parse error if it's a string representation of a dict
1735
+ error_msg = raw_error
1736
+ if (
1737
+ isinstance(raw_error, str)
1738
+ and raw_error.startswith("{")
1739
+ and "'code'" in raw_error
1740
+ ):
1741
+ try:
1742
+ import ast
1743
+
1744
+ error_dict = ast.literal_eval(raw_error)
1745
+ if isinstance(error_dict, dict):
1746
+ # Extract the actual error message
1747
+ error_data = error_dict.get("data", "")
1748
+ error_message = error_dict.get("message", "")
1749
+ error_code = error_dict.get("code", "")
1750
+ error_msg = (
1751
+ f"{error_message}: {error_data}"
1752
+ if error_data
1753
+ else error_message
1754
+ )
1755
+ if error_code:
1756
+ error_msg = f"[Code {error_code}] {error_msg}"
1757
+ except Exception:
1758
+ # If parsing fails, use original
1759
+ pass
1760
+
1761
+ # Log the original error before parsing
1762
+ logger.error(
1763
+ f"Transfer ownership failed - ORIGINAL ERROR (raw): {raw_error}"
1764
+ )
1765
+ logger.error(f"Transfer ownership failed - PARSED ERROR: {error_msg}")
1766
+ logger.error(f"Full result dict: {result}")
1767
+
1768
+ # Display original error before parsing
1769
+ from ...ui.display import HTCLIConsole as DebugConsole
1770
+
1771
+ debug_console = DebugConsole()
1772
+ debug_console.print()
1773
+ debug_console.print(
1774
+ "[bold yellow]🔍 DEBUG: Original Error (before parsing):[/bold yellow]"
1775
+ )
1776
+ debug_console.print(f"[yellow]Raw: {raw_error}[/yellow]")
1777
+ debug_console.print(f"[yellow]Parsed: {error_msg}[/yellow]")
1778
+ debug_console.print(f"[dim]Full result: {result}[/dim]")
1779
+ debug_console.print()
1780
+
1781
+ if handle_subnet_error(error_msg, subnet_id, "transfer ownership", client):
1782
+ return
1783
+ # If handle_subnet_error didn't handle it, raise an exception to prevent showing success
1784
+ raise RuntimeError(error_msg)
1785
+
1786
+ # CRITICAL: Double-check we have transaction hash before showing success
1787
+ # This is a safety check - we should NEVER show success without a transaction hash
1788
+ if not tx_hash:
1789
+ error_msg = "Transaction failed - no transaction hash returned. The transaction may have been rejected by the chain."
1790
+ logger.error(f"Transfer ownership failed: {error_msg}")
1791
+ if handle_subnet_error(error_msg, subnet_id, "transfer ownership", client):
1792
+ return
1793
+
1794
+ # Try to resolve block number if missing but we have block hash
1795
+ if not block_number and block_hash:
1796
+ try:
1797
+ resolved = client.rpc.chain.get_block_number(block_hash=block_hash)
1798
+ if resolved is not None:
1799
+ block_number = resolved
1800
+ except Exception:
1801
+ pass # Ignore if we can't resolve
1802
+
1803
+ # Display detailed success message with transaction details
1804
+ from ...ui.components import HTCLIPanel
1805
+ from ...ui.display import HTCLIConsole
1806
+
1807
+ console = HTCLIConsole()
1808
+ console.print() # Blank line after spinner
1809
+
1810
+ block_display = block_number if block_number is not None else "Pending"
1811
+ block_hash_display = block_hash or "N/A"
1812
+ tx_hash_display = tx_hash or "N/A"
1813
+
1814
+ success_msg = f"""[htcli.success]✅ Subnet {subnet_id} ownership transfer initiated successfully![/htcli.success]
1815
+
1816
+ [htcli.value]Transaction Hash:[/htcli.value] {tx_hash_display}
1817
+ [htcli.value]Block Number:[/htcli.value] {block_display}
1818
+ [htcli.value]Block Hash:[/htcli.value] {block_hash_display}
1819
+
1820
+ [htcli.info]💡 The new owner must accept the transfer using: htcli subnet accept --subnet-id {subnet_id}[/htcli.info]
1821
+ """
1822
+
1823
+ panel = HTCLIPanel(
1824
+ success_msg,
1825
+ title="✅ Transfer Initiated",
1826
+ border_style="htcli.success",
1827
+ highlight=True,
1828
+ )
1829
+ panel.render()
1830
+ except Exception as e:
1831
+ error_msg = str(e)
1832
+ if not handle_subnet_error(error_msg, subnet_id, "transfer ownership"):
1833
+ handle_and_display_error(e)
1834
+
1835
+
1836
+ def owner_accept_handler(subnet_id: Optional[int], coldkey: Optional[str] = None):
1837
+ try:
1838
+ if subnet_id is None:
1839
+ subnet_id = prompt_owner_accept()
1840
+ # Get signing wallet using comprehensive wallet retrieval mechanism
1841
+ if coldkey:
1842
+ from ...utils.wallet.core import resolve_coldkey_and_get_keypair
1843
+
1844
+ wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
1845
+ else:
1846
+ wallet_name, keypair = retrieve_wallet_with_validation(
1847
+ wallet_type="coldkey",
1848
+ purpose="sign the ownership acceptance transaction",
1849
+ only_existing_wallets=True,
1850
+ )
1851
+
1852
+ client = get_client()
1853
+ if not client.connect():
1854
+ raise RuntimeError("Could not connect to blockchain")
1855
+
1856
+ from ...models.requests.subnet import SubnetOwnershipAcceptRequest
1857
+
1858
+ request = SubnetOwnershipAcceptRequest(subnet_id=subnet_id)
1859
+
1860
+ # Use the same simple pattern as other handlers
1861
+ # The spinner will handle any output suppression needed
1862
+ with HTCLILoadingContext(
1863
+ f"Submitting accept ownership transaction for subnet {subnet_id}..."
1864
+ ):
1865
+ result = client.extrinsics.subnet.accept_subnet_ownership(
1866
+ request, keypair=keypair
1867
+ )
1868
+
1869
+ # Debug: Log what we received
1870
+ from ...utils.logging import get_logger
1871
+
1872
+ logger = get_logger(__name__)
1873
+ logger.debug(f"Accept ownership result: {result}")
1874
+ logger.debug(
1875
+ f"Result keys: {list(result.keys()) if isinstance(result, dict) else 'not a dict'}"
1876
+ )
1877
+
1878
+ # Extract transaction details first
1879
+ tx_hash = result.get("transaction_hash") or result.get("extrinsic_hash")
1880
+ block_number = result.get("block_number")
1881
+ block_hash = result.get("block_hash")
1882
+
1883
+ logger.debug(
1884
+ f"Extracted - tx_hash: {tx_hash}, block_number: {block_number}, block_hash: {block_hash}"
1885
+ )
1886
+
1887
+ # CRITICAL: Check for errors in result OR missing transaction details
1888
+ # If success is False OR we don't have a transaction hash, treat as failure
1889
+ if isinstance(result, dict) and (
1890
+ not result.get("success", False) or not tx_hash
1891
+ ):
1892
+ error_msg = result.get(
1893
+ "error", "Accept failed - no transaction hash returned"
1894
+ )
1895
+ if not tx_hash and result.get("success", False):
1896
+ error_msg = "Transaction submitted but no transaction hash returned. The transaction may have failed."
1897
+
1898
+ logger.error(f"Accept ownership failed: {error_msg}")
1899
+ if handle_subnet_error(error_msg, subnet_id, "accept ownership", client):
1900
+ return
1901
+ # If handle_subnet_error didn't handle it, raise an exception to prevent showing success
1902
+ raise RuntimeError(error_msg)
1903
+
1904
+ # CRITICAL: Double-check we have transaction hash before showing success
1905
+ # This is a safety check - we should NEVER show success without a transaction hash
1906
+ if not tx_hash:
1907
+ error_msg = "Transaction failed - no transaction hash returned. The transaction may have been rejected by the chain."
1908
+ logger.error(f"Accept ownership failed: {error_msg}")
1909
+ if handle_subnet_error(error_msg, subnet_id, "accept ownership", client):
1910
+ return
1911
+ raise RuntimeError(error_msg)
1912
+
1913
+ # Try to resolve block number if missing but we have block hash
1914
+ if not block_number and block_hash:
1915
+ try:
1916
+ resolved = client.rpc.chain.get_block_number(block_hash=block_hash)
1917
+ if resolved is not None:
1918
+ block_number = resolved
1919
+ except Exception:
1920
+ pass # Ignore if we can't resolve
1921
+
1922
+ # Display detailed success message with transaction details
1923
+ from ...ui.components import HTCLIPanel
1924
+ from ...ui.display import HTCLIConsole
1925
+
1926
+ console = HTCLIConsole()
1927
+ console.print() # Blank line after spinner
1928
+
1929
+ block_display = block_number if block_number is not None else "Pending"
1930
+ block_hash_display = block_hash or "N/A"
1931
+ tx_hash_display = tx_hash or "N/A"
1932
+
1933
+ success_msg = f"""[htcli.success]✅ Subnet {subnet_id} ownership accepted successfully![/htcli.success]
1934
+
1935
+ [htcli.value]Transaction Hash:[/htcli.value] {tx_hash_display}
1936
+ [htcli.value]Block Number:[/htcli.value] {block_display}
1937
+ [htcli.value]Block Hash:[/htcli.value] {block_hash_display}
1938
+
1939
+ [htcli.info]💡 You are now the owner of subnet {subnet_id}.[/htcli.info]
1940
+ """
1941
+
1942
+ panel = HTCLIPanel(
1943
+ success_msg,
1944
+ title="✅ Ownership Accepted",
1945
+ border_style="htcli.success",
1946
+ highlight=True,
1947
+ )
1948
+ panel.render()
1949
+ except Exception as e:
1950
+ error_msg = str(e)
1951
+ if not handle_subnet_error(error_msg, subnet_id, "accept ownership"):
1952
+ handle_and_display_error(e)
1953
+
1954
+
1955
+ def owner_remove_handler(subnet_id: Optional[int], coldkey: Optional[str] = None):
1956
+ """Handle subnet removal with proper error handling and confirmation."""
1957
+ try:
1958
+ if subnet_id is None:
1959
+ subnet_id = prompt_owner_remove()
1960
+
1961
+ if subnet_id < 0:
1962
+ raise ValueError("Subnet ID must be a non-negative integer")
1963
+
1964
+ # Get signing wallet using comprehensive wallet retrieval mechanism
1965
+ if coldkey:
1966
+ from ...utils.wallet.core import resolve_coldkey_and_get_keypair
1967
+
1968
+ wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
1969
+ else:
1970
+ wallet_name, keypair = retrieve_wallet_with_validation(
1971
+ wallet_type="coldkey",
1972
+ purpose="sign the transaction",
1973
+ only_existing_wallets=True,
1974
+ )
1975
+
1976
+ client = get_client()
1977
+ if not client.connect():
1978
+ raise RuntimeError("Could not connect to blockchain")
1979
+
1980
+ from ...models.requests.subnet import SubnetRemoveRequest
1981
+
1982
+ request = SubnetRemoveRequest(subnet_id=subnet_id)
1983
+
1984
+ with HTCLILoadingContext(f"Removing subnet {subnet_id}..."):
1985
+ result = client.extrinsics.subnet.remove_subnet(request, keypair=keypair)
1986
+
1987
+ # Check for errors in result
1988
+ if isinstance(result, dict) and not result.get("success", False):
1989
+ error_msg = result.get("error", "Remove failed")
1990
+ if handle_subnet_error(error_msg, subnet_id, "remove", client):
1991
+ return
1992
+
1993
+ display_generic_success(result)
1994
+ except ValueError as e:
1995
+ # Handle validation errors and user cancellation
1996
+ if "cancelled" in str(e).lower():
1997
+ from ...ui.display import HTCLIConsole
1998
+
1999
+ console = HTCLIConsole()
2000
+ console.print("[htcli.warning]Subnet removal cancelled by user.[/]")
2001
+ else:
2002
+ from ...ui.display import print_error
2003
+
2004
+ print_error(f"Invalid subnet ID: {str(e)}")
2005
+ except Exception as e:
2006
+ error_msg = str(e)
2007
+ # Use specific subnet error handling
2008
+ if not handle_subnet_error(error_msg, subnet_id, "remove"):
2009
+ if "subnet" in error_msg.lower() or "remove" in error_msg.lower():
2010
+ error = handle_substrate_error(e)
2011
+ if not isinstance(error, SubnetError):
2012
+ error = SubnetError(
2013
+ f"Failed to remove subnet {subnet_id}: {error_msg}"
2014
+ )
2015
+ error.display()
2016
+ else:
2017
+ handle_and_display_error(e)
2018
+
2019
+
2020
+ def get_node_status_handler(subnet_id: Optional[int], node_id: Optional[int]):
2021
+ """Handle getting node status with proper error handling."""
2022
+ try:
2023
+ if subnet_id is None:
2024
+ from .prompts import prompt_get_subnet
2025
+
2026
+ subnet_id = prompt_get_subnet()
2027
+
2028
+ if node_id is None:
2029
+ from ...ui.prompts import integer_prompt
2030
+
2031
+ node_id = integer_prompt(
2032
+ "Enter the node ID within the subnet",
2033
+ min_value=0,
2034
+ )
2035
+
2036
+ with HTCLILoadingContext(
2037
+ f"Fetching node {node_id} status from subnet {subnet_id}..."
2038
+ ):
2039
+ client = get_client()
2040
+ result = client.rpc.node.get_subnet_node_info(subnet_id, node_id)
2041
+
2042
+ # Display the node status information
2043
+ if result:
2044
+ from ...ui.display import print_info, print_success
2045
+
2046
+ print_success(f"Node {node_id} status in subnet {subnet_id}")
2047
+ classification = getattr(result, "classification", {}) or {}
2048
+ status = classification.get("node_class", "Unknown")
2049
+ print_info(f"Status: {status}")
2050
+ print_info(f"Stake: {getattr(result, 'stake_balance', 0)}")
2051
+ print_info(f"Reputation: {getattr(result, 'subnet_node_reputation', 0)}")
2052
+ else:
2053
+ from ...ui.display import print_error
2054
+
2055
+ print_error(f"Node {node_id} not found in subnet {subnet_id}")
2056
+
2057
+ except Exception as e:
2058
+ handle_and_display_error(e)
2059
+
2060
+
2061
+ def get_node_staking_handler(subnet_id: Optional[int], node_id: Optional[int]):
2062
+ """Handle getting node staking information with proper error handling."""
2063
+ try:
2064
+ if subnet_id is None:
2065
+ from .prompts import prompt_get_subnet
2066
+
2067
+ subnet_id = prompt_get_subnet()
2068
+
2069
+ if node_id is None:
2070
+ from ...ui.prompts import integer_prompt
2071
+
2072
+ node_id = integer_prompt(
2073
+ "Enter the node ID within the subnet",
2074
+ min_value=0,
2075
+ )
2076
+
2077
+ client = get_client()
2078
+ with HTCLILoadingContext(
2079
+ f"Fetching staking info for node {node_id} in subnet {subnet_id}..."
2080
+ ):
2081
+ result = client.rpc.node.get_node_staking_info(subnet_id, node_id)
2082
+
2083
+ # Display the node staking information
2084
+ if result.get("success"):
2085
+ staking_data = result.get("data", {})
2086
+ from ...ui.display import print_info, print_success
2087
+
2088
+ print_success(f"Node {node_id} staking info in subnet {subnet_id}")
2089
+ print_info(f"Node Stake: {staking_data.get('node_stake', 0)}")
2090
+ print_info(
2091
+ f"Delegate Stake Shares: {staking_data.get('delegate_stake_shares', 0)}"
2092
+ )
2093
+ print_info(f"Reward Rate: {staking_data.get('reward_rate', 0)}")
2094
+ else:
2095
+ from ...ui.display import print_error
2096
+
2097
+ print_error(
2098
+ f"Failed to get node staking info: {result.get('message', 'Unknown error')}"
2099
+ )
2100
+
2101
+ except Exception as e:
2102
+ handle_and_display_error(e)
2103
+
2104
+
2105
+ # New RPC-based handlers
2106
+ def get_subnet_info_handler(subnet_id: Optional[int], all_subnets: bool = False):
2107
+ """Handle getting subnet information using RPC."""
2108
+ try:
2109
+ if all_subnets and subnet_id is not None:
2110
+ print_error("Cannot use --all with --subnet-id. Please choose one.")
2111
+ return
2112
+
2113
+ if subnet_id is not None and subnet_id < 0:
2114
+ display_subnet_info_rpc(None)
2115
+ return
2116
+
2117
+ client = get_client()
2118
+
2119
+ if all_subnets:
2120
+ with HTCLILoadingContext("Fetching information for all subnets..."):
2121
+ subnet_models = client.rpc.subnet.get_all_subnets_info()
2122
+
2123
+ display_subnets_overview(subnet_models or [])
2124
+ return
2125
+
2126
+ if subnet_id is None:
2127
+ from ...utils.prompts import integer_prompt
2128
+
2129
+ subnet_id = integer_prompt("Enter subnet ID")
2130
+
2131
+ with HTCLILoadingContext(f"Fetching subnet {subnet_id} information..."):
2132
+ subnet_model = client.rpc.subnet.get_subnet_info(subnet_id)
2133
+
2134
+ # Convert Pydantic model to dict for display
2135
+ if subnet_model:
2136
+ # Use Pydantic's model_dump() to get all fields as dict
2137
+ subnet_dict = subnet_model.model_dump()
2138
+
2139
+ # Convert bytes fields to strings for display
2140
+ if isinstance(subnet_dict.get("name"), bytes):
2141
+ decoded_name = subnet_dict["name"].decode("utf-8")
2142
+ subnet_dict["name"] = decoded_name if decoded_name else "Not set"
2143
+ if isinstance(subnet_dict.get("description"), bytes):
2144
+ decoded_desc = subnet_dict["description"].decode("utf-8")
2145
+ subnet_dict["description"] = decoded_desc if decoded_desc else "Not set"
2146
+ if isinstance(subnet_dict.get("repo"), bytes):
2147
+ decoded_repo = subnet_dict["repo"].decode("utf-8")
2148
+ subnet_dict["repo"] = decoded_repo if decoded_repo else "Not set"
2149
+ if isinstance(subnet_dict.get("tags"), bytes):
2150
+ decoded_tags = subnet_dict["tags"].decode("utf-8")
2151
+ subnet_dict["tags"] = decoded_tags if decoded_tags else "Not set"
2152
+
2153
+ # Handle empty sets for initial_coldkeys
2154
+ if (
2155
+ isinstance(subnet_dict.get("initial_coldkeys"), set)
2156
+ and not subnet_dict["initial_coldkeys"]
2157
+ ):
2158
+ subnet_dict["initial_coldkeys"] = (
2159
+ None # Will display as "None" or can be handled by display function
2160
+ )
2161
+
2162
+ # Convert state enum to string
2163
+ if hasattr(subnet_dict.get("state"), "value"):
2164
+ subnet_dict["state"] = subnet_dict["state"].value
2165
+ elif hasattr(subnet_dict.get("state"), "name"):
2166
+ subnet_dict["state"] = subnet_dict["state"].name
2167
+
2168
+ display_subnet_info_rpc(subnet_dict)
2169
+ else:
2170
+ display_subnet_info_rpc(None)
2171
+
2172
+ except Exception as e:
2173
+ handle_and_display_error(e)
2174
+
2175
+
2176
+ def get_subnet_nodes_handler(subnet_id: Optional[int], coldkey: Optional[str] = None):
2177
+ """Handle getting subnet nodes using RPC. Can filter by subnet_id or coldkey."""
2178
+ try:
2179
+ if subnet_id is not None and subnet_id < 0:
2180
+ print_info(f"No nodes found for subnet {subnet_id}")
2181
+ return
2182
+
2183
+ client = get_client()
2184
+
2185
+ # Prioritize subnet_id when both are provided to ensure consistency
2186
+ # This ensures we get all nodes for the subnet, then filter by coldkey
2187
+ if subnet_id is not None:
2188
+ # Use subnet_id to get nodes
2189
+ with HTCLILoadingContext(f"Fetching nodes for subnet {subnet_id}..."):
2190
+ all_nodes = client.rpc.node.get_subnet_nodes_info(subnet_id)
2191
+
2192
+ # If coldkey is also provided, filter by coldkey
2193
+ if coldkey:
2194
+ # Resolve coldkey if it's a wallet name
2195
+ if not (coldkey.startswith("0x") or len(coldkey) in (48, 49)):
2196
+ # Treat as wallet name: resolve to its address if exists
2197
+ try:
2198
+ from ...utils.wallet.crypto import get_wallet_info_by_name
2199
+
2200
+ info = get_wallet_info_by_name(coldkey, is_hotkey=False)
2201
+ # Prefer EVM address, fallback to ss58_address
2202
+ coldkey = (
2203
+ info.get("evm_address")
2204
+ or info.get("address")
2205
+ or info.get("ss58_address")
2206
+ or coldkey
2207
+ )
2208
+ except FileNotFoundError:
2209
+ # Wallet name not found - show error and exit
2210
+ print_error(f"Wallet '{coldkey}' not found")
2211
+ return
2212
+ except Exception:
2213
+ # Leave as provided; RPC layer will handle format checks
2214
+ pass
2215
+
2216
+ # Normalize Ethereum addresses to lowercase (addresses are case-insensitive)
2217
+ if coldkey and coldkey.startswith("0x"):
2218
+ coldkey = coldkey.lower()
2219
+
2220
+ # Filter nodes by coldkey
2221
+ nodes_models = [
2222
+ node
2223
+ for node in all_nodes
2224
+ if node.coldkey and node.coldkey.lower() == coldkey.lower()
2225
+ ]
2226
+ logger.debug(
2227
+ f"After filtering by coldkey {coldkey}: {len(nodes_models)} nodes"
2228
+ )
2229
+ else:
2230
+ nodes_models = all_nodes
2231
+
2232
+ elif coldkey:
2233
+ # Only coldkey provided (no subnet_id) - use coldkey RPC method
2234
+ # Resolve coldkey and fetch nodes
2235
+ subnet_id = None # Ensure subnet_id is defined for display logic
2236
+ with HTCLILoadingContext("Resolving coldkey and fetching nodes..."):
2237
+ # If coldkey looks like a wallet name, resolve to address
2238
+ if not (coldkey.startswith("0x") or len(coldkey) in (48, 49)):
2239
+ # Treat as wallet name: resolve to its address if exists
2240
+ try:
2241
+ from ...utils.wallet.crypto import get_wallet_info_by_name
2242
+
2243
+ info = get_wallet_info_by_name(coldkey, is_hotkey=False)
2244
+ # Prefer EVM address, fallback to ss58_address
2245
+ coldkey = (
2246
+ info.get("evm_address")
2247
+ or info.get("address")
2248
+ or info.get("ss58_address")
2249
+ or coldkey
2250
+ )
2251
+ except FileNotFoundError:
2252
+ # Wallet name not found - show error and exit
2253
+ print_error(f"Wallet '{coldkey}' not found")
2254
+ return
2255
+ except Exception:
2256
+ # Leave as provided; RPC layer will handle format checks
2257
+ pass
2258
+
2259
+ # Normalize Ethereum addresses to lowercase (addresses are case-insensitive)
2260
+ if coldkey and coldkey.startswith("0x"):
2261
+ coldkey = coldkey.lower()
2262
+
2263
+ # Get nodes for this coldkey using RPC method
2264
+ nodes_models = client.rpc.node.get_coldkey_subnet_nodes_info(coldkey)
2265
+ logger.debug(
2266
+ f"Retrieved {len(nodes_models)} total nodes for coldkey {coldkey}"
2267
+ )
2268
+ if nodes_models:
2269
+ logger.debug(
2270
+ f"Node subnet IDs: {[node.subnet_id for node in nodes_models]}"
2271
+ )
2272
+
2273
+ else:
2274
+ # Neither provided - prompt for subnet_id
2275
+ from ...utils.prompts import integer_prompt
2276
+
2277
+ subnet_id = integer_prompt("Enter subnet ID")
2278
+ with HTCLILoadingContext(f"Fetching nodes for subnet {subnet_id}..."):
2279
+ nodes_models = client.rpc.node.get_subnet_nodes_info(subnet_id)
2280
+
2281
+ # Convert Pydantic models to dicts for display
2282
+ nodes_data = []
2283
+ for node in nodes_models:
2284
+ try:
2285
+ node_dict = {
2286
+ "id": node.subnet_node_id,
2287
+ "subnet_id": node.subnet_id,
2288
+ "coldkey": node.coldkey,
2289
+ "hotkey": node.hotkey,
2290
+ "stake_balance": node.stake_balance,
2291
+ "node_delegate_stake_balance": node.node_delegate_stake_balance,
2292
+ "status": (
2293
+ str(node.classification.get("node_class", "Unknown"))
2294
+ if isinstance(node.classification, dict)
2295
+ else str(node.classification)
2296
+ ),
2297
+ }
2298
+ nodes_data.append(node_dict)
2299
+ logger.debug(
2300
+ f"Added node {node.subnet_node_id} (subnet {node.subnet_id}, coldkey {node.coldkey})"
2301
+ )
2302
+ except Exception as e:
2303
+ logger.warning(
2304
+ f"Failed to convert node {getattr(node, 'subnet_node_id', 'unknown')} to dict: {e}"
2305
+ )
2306
+ continue
2307
+
2308
+ # Display nodes (empty list if none)
2309
+ if not nodes_data:
2310
+ if coldkey:
2311
+ print_info(f"No nodes found for coldkey: {coldkey}")
2312
+ else:
2313
+ print_info(f"No nodes found for subnet {subnet_id}")
2314
+ return
2315
+
2316
+ # Use subnet_id if provided, otherwise None (for coldkey listing)
2317
+ display_subnet_nodes_rpc(nodes_data, subnet_id if subnet_id else None)
2318
+
2319
+ except Exception as e:
2320
+ handle_and_display_node_error(e, operation="list")
2321
+
2322
+
2323
+ def get_bootnodes_handler(subnet_id: Optional[int]):
2324
+ """Handle getting subnet bootnodes using RPC."""
2325
+ try:
2326
+ if subnet_id is None:
2327
+ from ...utils.prompts import integer_prompt
2328
+
2329
+ subnet_id = integer_prompt("Enter subnet ID")
2330
+
2331
+ if subnet_id < 0:
2332
+
2333
+ class BootnodesData:
2334
+ def __init__(self):
2335
+ self.bootnodes = []
2336
+ self.node_bootnodes = []
2337
+
2338
+ display_bootnodes_rpc(BootnodesData(), subnet_id)
2339
+ return
2340
+
2341
+ client = get_client()
2342
+
2343
+ with HTCLILoadingContext(f"Fetching bootnodes for subnet {subnet_id}..."):
2344
+ bootnodes_dict = client.rpc.subnet.get_subnet_bootnodes(subnet_id)
2345
+
2346
+ # Helper to decode bytes to string
2347
+ def decode_list(items):
2348
+ decoded_items = []
2349
+ for item in items:
2350
+ if isinstance(item, bytes):
2351
+ try:
2352
+ decoded_items.append(item.decode("utf-8"))
2353
+ except Exception:
2354
+ decoded_items.append(str(item))
2355
+ else:
2356
+ decoded_items.append(str(item))
2357
+ return decoded_items
2358
+
2359
+ # Wrap bootnodes in object for display (display expects .bootnodes and .node_bootnodes)
2360
+ class BootnodesData:
2361
+ def __init__(self, bootnodes, node_bootnodes):
2362
+ self.bootnodes = decode_list(bootnodes)
2363
+ self.node_bootnodes = decode_list(node_bootnodes)
2364
+
2365
+ # Extract lists from the dictionary returned by get_subnet_bootnodes
2366
+ bootnodes_data = BootnodesData(
2367
+ bootnodes_dict.get("bootnodes", []),
2368
+ bootnodes_dict.get("node_bootnodes", []),
2369
+ )
2370
+
2371
+ # Display bootnodes
2372
+ display_bootnodes_rpc(bootnodes_data, subnet_id)
2373
+
2374
+ except Exception as e:
2375
+ handle_and_display_error(e)
2376
+
2377
+
2378
+ def owner_set_emergency_validator_set_handler(
2379
+ subnet_id: Optional[int], node_ids: Optional[str], coldkey: Optional[str] = None
2380
+ ):
2381
+ """Handle setting emergency validator set."""
2382
+ try:
2383
+ from .prompts import prompt_set_emergency_validator_set
2384
+
2385
+ if subnet_id is None or node_ids is None:
2386
+ subnet_id, node_ids = prompt_set_emergency_validator_set(
2387
+ subnet_id, node_ids
2388
+ )
2389
+
2390
+ # Parse node IDs from comma-separated string
2391
+ node_ids_list = [int(nid.strip()) for nid in node_ids.split(",") if nid.strip()]
2392
+
2393
+ # Get signing wallet
2394
+ if coldkey:
2395
+ from ...utils.wallet.core import resolve_coldkey_and_get_keypair
2396
+
2397
+ wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
2398
+ else:
2399
+ wallet_name, keypair = retrieve_wallet_with_validation(
2400
+ wallet_type="coldkey",
2401
+ purpose="sign the transaction",
2402
+ only_existing_wallets=True,
2403
+ )
2404
+
2405
+ client = get_client()
2406
+ if not client.connect():
2407
+ raise RuntimeError("Could not connect to blockchain")
2408
+
2409
+ with HTCLILoadingContext(
2410
+ f"Setting emergency validator set for subnet {subnet_id}..."
2411
+ ):
2412
+ result = client.extrinsics.subnet.owner_set_emergency_validator_set(
2413
+ subnet_id=subnet_id, subnet_node_ids=node_ids_list, keypair=keypair
2414
+ )
2415
+
2416
+ if isinstance(result, dict) and not result.get("success", False):
2417
+ error_msg = result.get("error", "Operation failed")
2418
+ if handle_subnet_error(
2419
+ error_msg, subnet_id, "set emergency validator set", client
2420
+ ):
2421
+ return
2422
+
2423
+ from .display import display_emergency_validator_set_result
2424
+
2425
+ display_emergency_validator_set_result(
2426
+ result, subnet_id, node_ids_list, action="set"
2427
+ )
2428
+
2429
+ except Exception as e:
2430
+ error_msg = str(e)
2431
+ if not handle_subnet_error(error_msg, subnet_id, "set emergency validator set"):
2432
+ handle_and_display_error(e)
2433
+
2434
+
2435
+ def owner_clear_emergency_validator_set_handler(
2436
+ subnet_id: Optional[int], coldkey: Optional[str] = None
2437
+ ):
2438
+ """Handle clearing emergency validator set."""
2439
+ try:
2440
+ from .prompts import prompt_clear_emergency_validator_set
2441
+
2442
+ if subnet_id is None:
2443
+ subnet_id = prompt_clear_emergency_validator_set()
2444
+
2445
+ # Get signing wallet
2446
+ if coldkey:
2447
+ from ...utils.wallet.core import resolve_coldkey_and_get_keypair
2448
+
2449
+ wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
2450
+ else:
2451
+ wallet_name, keypair = retrieve_wallet_with_validation(
2452
+ wallet_type="coldkey",
2453
+ purpose="sign the transaction",
2454
+ only_existing_wallets=True,
2455
+ )
2456
+
2457
+ client = get_client()
2458
+ if not client.connect():
2459
+ raise RuntimeError("Could not connect to blockchain")
2460
+
2461
+ # Clear by setting empty list
2462
+ with HTCLILoadingContext(
2463
+ f"Clearing emergency validator set for subnet {subnet_id}..."
2464
+ ):
2465
+ result = client.extrinsics.subnet.owner_set_emergency_validator_set(
2466
+ subnet_id=subnet_id, subnet_node_ids=[], keypair=keypair
2467
+ )
2468
+
2469
+ if isinstance(result, dict) and not result.get("success", False):
2470
+ error_msg = result.get("error", "Operation failed")
2471
+ if handle_subnet_error(
2472
+ error_msg, subnet_id, "clear emergency validator set", client
2473
+ ):
2474
+ return
2475
+
2476
+ from .display import display_emergency_validator_set_result
2477
+
2478
+ display_emergency_validator_set_result(result, subnet_id, [], action="clear")
2479
+
2480
+ except Exception as e:
2481
+ error_msg = str(e)
2482
+ if not handle_subnet_error(
2483
+ error_msg, subnet_id, "clear emergency validator set"
2484
+ ):
2485
+ handle_and_display_error(e)
2486
+
2487
+
2488
+ def owner_add_bootnode_access_handler(
2489
+ subnet_id: Optional[int], account: Optional[str], coldkey: Optional[str] = None
2490
+ ):
2491
+ """Handle adding bootnode access."""
2492
+ try:
2493
+ from .prompts import prompt_bootnode_access
2494
+ from ...utils.blockchain.validation import validate_address
2495
+ from ...utils.wallet.crypto import get_wallet_info_by_name
2496
+ from ...utils.wallet.crypto import public_key_to_evm_address
2497
+
2498
+ if subnet_id is None or account is None:
2499
+ subnet_id, account = prompt_bootnode_access(
2500
+ subnet_id, account, action="add"
2501
+ )
2502
+
2503
+ # Resolve account: can be either EVM address or wallet name
2504
+ original_account = account
2505
+ if account.startswith("0x"):
2506
+ # Already an EVM address - normalize to lowercase
2507
+ account = account.lower()
2508
+ else:
2509
+ # Try to resolve as wallet name
2510
+ try:
2511
+ wallet_info = get_wallet_info_by_name(
2512
+ account,
2513
+ is_hotkey=False, # Looking for coldkey wallets
2514
+ )
2515
+
2516
+ if wallet_info:
2517
+ # Try to get EVM address from wallet info
2518
+ resolved_address = wallet_info.get(
2519
+ "evm_address"
2520
+ ) or wallet_info.get("address")
2521
+
2522
+ # If we have an address, check if it's EVM format
2523
+ if resolved_address and validate_address(resolved_address):
2524
+ account = resolved_address.lower()
2525
+ else:
2526
+ # Try to derive from public key
2527
+ public_key_hex = wallet_info.get("public_key")
2528
+ if public_key_hex:
2529
+ try:
2530
+ resolved_address = public_key_to_evm_address(
2531
+ bytes.fromhex(public_key_hex)
2532
+ )
2533
+ if validate_address(resolved_address):
2534
+ account = resolved_address.lower()
2535
+ else:
2536
+ raise ValueError(
2537
+ f"Wallet '{original_account}' does not have a valid EVM address. "
2538
+ "Please use an EVM address (0x...) directly."
2539
+ )
2540
+ except Exception as e:
2541
+ raise ValueError(
2542
+ f"Failed to derive EVM address from wallet '{original_account}': {str(e)}"
2543
+ ) from e
2544
+ else:
2545
+ raise ValueError(
2546
+ f"Wallet '{original_account}' does not have a valid EVM address. "
2547
+ "Please provide an EVM address (0x...) directly."
2548
+ )
2549
+ else:
2550
+ raise ValueError(f"Wallet '{account}' not found")
2551
+ except FileNotFoundError:
2552
+ raise ValueError(
2553
+ f"Wallet '{account}' not found. "
2554
+ "Please provide either a wallet name or a valid EVM address (0x...)."
2555
+ )
2556
+ except ValueError:
2557
+ # Re-raise ValueError as-is
2558
+ raise
2559
+ except Exception as e:
2560
+ # For other errors, provide helpful message
2561
+ raise ValueError(
2562
+ f"Failed to resolve account '{account}': {str(e)}. "
2563
+ "Please provide either a wallet name or a valid EVM address (0x...)."
2564
+ )
2565
+
2566
+ # Get signing wallet
2567
+ if coldkey:
2568
+ from ...utils.wallet.core import resolve_coldkey_and_get_keypair
2569
+
2570
+ wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
2571
+ else:
2572
+ wallet_name, keypair = retrieve_wallet_with_validation(
2573
+ wallet_type="coldkey",
2574
+ purpose="sign the transaction",
2575
+ only_existing_wallets=True,
2576
+ )
2577
+
2578
+ client = get_client()
2579
+ if not client.connect():
2580
+ raise RuntimeError("Could not connect to blockchain")
2581
+
2582
+ with HTCLILoadingContext(f"Adding bootnode access for subnet {subnet_id}..."):
2583
+ result = client.extrinsics.subnet.owner_add_bootnode_access(
2584
+ subnet_id=subnet_id, new_account=account, keypair=keypair
2585
+ )
2586
+
2587
+ if isinstance(result, dict) and not result.get("success", False):
2588
+ error_msg = result.get("error", "Operation failed")
2589
+ if handle_subnet_error(error_msg, subnet_id, "add bootnode access", client):
2590
+ return
2591
+
2592
+ from .display import display_bootnode_access_result
2593
+
2594
+ display_bootnode_access_result(result, subnet_id, account, action="add")
2595
+
2596
+ except Exception as e:
2597
+ error_msg = str(e)
2598
+ if not handle_subnet_error(error_msg, subnet_id, "add bootnode access"):
2599
+ handle_and_display_error(e)
2600
+
2601
+
2602
+ def owner_remove_bootnode_access_handler(
2603
+ subnet_id: Optional[int], account: Optional[str], coldkey: Optional[str] = None
2604
+ ):
2605
+ """Handle removing bootnode access."""
2606
+ try:
2607
+ from .prompts import prompt_bootnode_access
2608
+ from ...utils.blockchain.validation import validate_address
2609
+ from ...utils.wallet.crypto import get_wallet_info_by_name
2610
+ from ...utils.wallet.crypto import public_key_to_evm_address
2611
+
2612
+ if subnet_id is None or account is None:
2613
+ subnet_id, account = prompt_bootnode_access(
2614
+ subnet_id, account, action="remove"
2615
+ )
2616
+
2617
+ # Resolve account: can be either EVM address or wallet name
2618
+ original_account = account
2619
+ if account.startswith("0x"):
2620
+ # Already an EVM address - normalize to lowercase
2621
+ account = account.lower()
2622
+ else:
2623
+ # Try to resolve as wallet name
2624
+ try:
2625
+ wallet_info = get_wallet_info_by_name(
2626
+ account,
2627
+ is_hotkey=False, # Looking for coldkey wallets
2628
+ )
2629
+
2630
+ if wallet_info:
2631
+ # Try to get EVM address from wallet info
2632
+ resolved_address = wallet_info.get(
2633
+ "evm_address"
2634
+ ) or wallet_info.get("address")
2635
+
2636
+ # If we have an address, check if it's EVM format
2637
+ if resolved_address and validate_address(resolved_address):
2638
+ account = resolved_address.lower()
2639
+ else:
2640
+ # Try to derive from public key
2641
+ public_key_hex = wallet_info.get("public_key")
2642
+ if public_key_hex:
2643
+ try:
2644
+ resolved_address = public_key_to_evm_address(
2645
+ bytes.fromhex(public_key_hex)
2646
+ )
2647
+ if validate_address(resolved_address):
2648
+ account = resolved_address.lower()
2649
+ else:
2650
+ raise ValueError(
2651
+ f"Wallet '{original_account}' does not have a valid EVM address. "
2652
+ "Please use an EVM address (0x...) directly."
2653
+ )
2654
+ except Exception as e:
2655
+ raise ValueError(
2656
+ f"Failed to derive EVM address from wallet '{original_account}': {str(e)}"
2657
+ ) from e
2658
+ else:
2659
+ raise ValueError(
2660
+ f"Wallet '{original_account}' does not have a valid EVM address. "
2661
+ "Please use an EVM address (0x...) directly."
2662
+ )
2663
+ else:
2664
+ raise ValueError(f"Wallet '{account}' not found")
2665
+ except FileNotFoundError:
2666
+ raise ValueError(
2667
+ f"Wallet '{account}' not found. "
2668
+ "Please provide either a wallet name or a valid EVM address (0x...)."
2669
+ )
2670
+ except ValueError:
2671
+ # Re-raise ValueError as-is
2672
+ raise
2673
+ except Exception as e:
2674
+ # For other errors, provide helpful message
2675
+ raise ValueError(
2676
+ f"Failed to resolve account '{account}': {str(e)}. "
2677
+ "Please provide either a wallet name or a valid EVM address (0x...)."
2678
+ )
2679
+
2680
+ # Get signing wallet
2681
+ if coldkey:
2682
+ from ...utils.wallet.core import resolve_coldkey_and_get_keypair
2683
+
2684
+ wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
2685
+ else:
2686
+ wallet_name, keypair = retrieve_wallet_with_validation(
2687
+ wallet_type="coldkey",
2688
+ purpose="sign the transaction",
2689
+ only_existing_wallets=True,
2690
+ )
2691
+
2692
+ client = get_client()
2693
+ if not client.connect():
2694
+ raise RuntimeError("Could not connect to blockchain")
2695
+
2696
+ with HTCLILoadingContext(f"Removing bootnode access for subnet {subnet_id}..."):
2697
+ result = client.extrinsics.subnet.owner_remove_bootnode_access(
2698
+ subnet_id=subnet_id, remove_account=account, keypair=keypair
2699
+ )
2700
+
2701
+ if isinstance(result, dict) and not result.get("success", False):
2702
+ error_msg = result.get("error", "Operation failed")
2703
+ if handle_subnet_error(
2704
+ error_msg, subnet_id, "remove bootnode access", client
2705
+ ):
2706
+ return
2707
+
2708
+ from .display import display_bootnode_access_result
2709
+
2710
+ display_bootnode_access_result(result, subnet_id, account, action="remove")
2711
+
2712
+ except Exception as e:
2713
+ error_msg = str(e)
2714
+ if not handle_subnet_error(error_msg, subnet_id, "remove bootnode access"):
2715
+ handle_and_display_error(e)
2716
+
2717
+
2718
+ def owner_add_bootnode_handler(
2719
+ subnet_id: Optional[int],
2720
+ bootnode_address: Optional[str],
2721
+ coldkey: Optional[str] = None,
2722
+ ):
2723
+ """Handle adding a bootnode address."""
2724
+ try:
2725
+ from ...ui.prompts import integer_prompt, text_prompt
2726
+ from ...utils.wallet.core import resolve_coldkey_and_get_keypair
2727
+
2728
+ if subnet_id is None:
2729
+ subnet_id = integer_prompt("Enter the Subnet ID (UID)", min_value=0)
2730
+
2731
+ if bootnode_address is None:
2732
+ bootnode_address = text_prompt(
2733
+ "Enter the bootnode address (multiaddr)", required=True
2734
+ )
2735
+
2736
+ import re
2737
+
2738
+ # Regex for multiaddr: /ip4/IP/tcp/PORT/p2p/PEER_ID
2739
+ multiaddr_pattern = r"^/(ip4|ip6)/([^/]+)/(tcp|udp)/(\d+)/p2p/([a-zA-Z0-9]+)$"
2740
+ if not re.match(multiaddr_pattern, bootnode_address):
2741
+ from ...ui.display import print_error
2742
+
2743
+ print_error("Invalid bootnode address format.")
2744
+ print_error("Expected format: /ip4/<IP>/tcp/<PORT>/p2p/<PEER_ID>")
2745
+ print_error("Example: /ip4/127.0.0.1/tcp/30333/p2p/12D3KooW...")
2746
+ return
2747
+
2748
+ # Get keypair
2749
+ if coldkey:
2750
+ wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
2751
+ else:
2752
+ wallet_name, keypair = retrieve_wallet_with_validation(
2753
+ wallet_type="coldkey",
2754
+ purpose="sign the transaction",
2755
+ only_existing_wallets=True,
2756
+ )
2757
+
2758
+ client = get_client()
2759
+ if not client.connect():
2760
+ raise RuntimeError("Could not connect to blockchain")
2761
+
2762
+ with HTCLILoadingContext(f"Adding bootnode to subnet {subnet_id}..."):
2763
+ # Ensure it is a list for the client method
2764
+ result = client.extrinsics.subnet.update_bootnodes(
2765
+ subnet_id=subnet_id,
2766
+ add_bootnodes=[bootnode_address],
2767
+ remove_bootnodes=[],
2768
+ keypair=keypair,
2769
+ )
2770
+
2771
+ if isinstance(result, dict) and not result.get("success", False):
2772
+ error_msg = result.get("error", "Operation failed")
2773
+ if handle_subnet_error(error_msg, subnet_id, "add bootnode", client):
2774
+ return
2775
+
2776
+ from ...ui.display import print_success
2777
+
2778
+ print_success(f"Successfully added bootnode to subnet {subnet_id}")
2779
+ if result.get("message"):
2780
+ print_info(result["message"])
2781
+
2782
+ except Exception as e:
2783
+ error_msg = str(e)
2784
+ if not handle_subnet_error(error_msg, subnet_id, "add bootnode"):
2785
+ handle_and_display_error(e)
2786
+
2787
+
2788
+ def owner_remove_bootnode_handler(
2789
+ subnet_id: Optional[int],
2790
+ bootnode_address: Optional[str],
2791
+ coldkey: Optional[str] = None,
2792
+ ):
2793
+ """Handle removing a bootnode address."""
2794
+ try:
2795
+ from ...ui.prompts import integer_prompt, text_prompt
2796
+ from ...utils.wallet.core import resolve_coldkey_and_get_keypair
2797
+
2798
+ if subnet_id is None:
2799
+ subnet_id = integer_prompt("Enter the Subnet ID (UID)", min_value=0)
2800
+
2801
+ if bootnode_address is None:
2802
+ bootnode_address = text_prompt(
2803
+ "Enter the bootnode address (multiaddr) to remove", required=True
2804
+ )
2805
+
2806
+ import re
2807
+
2808
+ # Regex for multiaddr: /ip4/IP/tcp/PORT/p2p/PEER_ID
2809
+ multiaddr_pattern = r"^/(ip4|ip6)/([^/]+)/(tcp|udp)/(\d+)/p2p/([a-zA-Z0-9]+)$"
2810
+ if not re.match(multiaddr_pattern, bootnode_address):
2811
+ from ...ui.display import print_error
2812
+
2813
+ print_error("Invalid bootnode address format.")
2814
+ print_error("Expected format: /ip4/<IP>/tcp/<PORT>/p2p/<PEER_ID>")
2815
+ print_error("Example: /ip4/127.0.0.1/tcp/30333/p2p/12D3KooW...")
2816
+ return
2817
+
2818
+ # Get keypair
2819
+ if coldkey:
2820
+ wallet_name, keypair = resolve_coldkey_and_get_keypair(coldkey)
2821
+ else:
2822
+ wallet_name, keypair = retrieve_wallet_with_validation(
2823
+ wallet_type="coldkey",
2824
+ purpose="sign the transaction",
2825
+ only_existing_wallets=True,
2826
+ )
2827
+
2828
+ client = get_client()
2829
+ if not client.connect():
2830
+ raise RuntimeError("Could not connect to blockchain")
2831
+
2832
+ with HTCLILoadingContext(f"Removing bootnode from subnet {subnet_id}..."):
2833
+ # Ensure it is a list for the client method
2834
+ result = client.extrinsics.subnet.update_bootnodes(
2835
+ subnet_id=subnet_id,
2836
+ add_bootnodes=[],
2837
+ remove_bootnodes=[bootnode_address],
2838
+ keypair=keypair,
2839
+ )
2840
+
2841
+ if isinstance(result, dict) and not result.get("success", False):
2842
+ error_msg = result.get("error", "Operation failed")
2843
+ if handle_subnet_error(error_msg, subnet_id, "remove bootnode", client):
2844
+ return
2845
+
2846
+ from ...ui.display import print_success
2847
+
2848
+ print_success(f"Successfully removed bootnode from subnet {subnet_id}")
2849
+ if result.get("message"):
2850
+ print_info(result["message"])
2851
+
2852
+ except Exception as e:
2853
+ error_msg = str(e)
2854
+ if not handle_subnet_error(error_msg, subnet_id, "remove bootnode"):
2855
+ handle_and_display_error(e)