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,749 @@
1
+ """
2
+ Node display functions for RPC-based operations and extrinsic operations.
3
+ """
4
+
5
+ from typing import Any, Optional
6
+
7
+ from ...ui.colors import amount, info, success
8
+ from ...ui.components import HTCLIPanel, create_node_table
9
+ from ...ui.display import HTCLIConsole
10
+
11
+ console = HTCLIConsole()
12
+
13
+
14
+ def display_all_nodes_rpc(nodes_data: list[Any]):
15
+ """Display all nodes from RPC response."""
16
+ if not nodes_data:
17
+ console.print(info("No nodes found across all subnets"))
18
+ return
19
+
20
+ # Create table for all nodes
21
+ table = create_node_table(nodes_data)
22
+ console.print(table)
23
+
24
+
25
+ def display_node_info_rpc(node_data: Any, subnet_id: int, node_id: int):
26
+ """Display specific node information from RPC response."""
27
+ if not node_data:
28
+ console.print(info(f"Node {node_id} not found in subnet {subnet_id}"))
29
+ return
30
+
31
+ from ...ui.components import HTCLIPanel
32
+
33
+ # Helper functions for formatting
34
+ def format_balance(balance: int) -> str:
35
+ """Convert raw balance to TENSOR."""
36
+ return f"{balance / 1e18:,.4f} TENSOR"
37
+
38
+ def format_peer_id(peer_id) -> str:
39
+ """Format peer ID - show only if not empty."""
40
+ # Handle None, empty bytes, or empty list
41
+ if not peer_id:
42
+ return "[dim]Not set[/dim]"
43
+ if isinstance(peer_id, bytes):
44
+ if len(peer_id) == 0:
45
+ return "[dim]Not set[/dim]"
46
+ # Try to decode using the peer_id utility function
47
+ try:
48
+ from ...utils.blockchain.peer_id import decode_peer_id
49
+ decoded = decode_peer_id(peer_id)
50
+ return decoded if decoded else "[dim]Empty[/dim]"
51
+ except Exception:
52
+ # Fallback to UTF-8 decode if decode_peer_id fails
53
+ try:
54
+ decoded = peer_id.decode('utf-8', errors='ignore')
55
+ return decoded if decoded else "[dim]Empty[/dim]"
56
+ except Exception:
57
+ return "[dim]Invalid[/dim]"
58
+ # Handle string peer IDs directly
59
+ if isinstance(peer_id, str):
60
+ return peer_id if peer_id else "[dim]Not set[/dim]"
61
+ # Handle list (shouldn't happen if to_bytes is working, but just in case)
62
+ if isinstance(peer_id, list):
63
+ if len(peer_id) == 0:
64
+ return "[dim]Not set[/dim]"
65
+ try:
66
+ return format_peer_id(bytes(peer_id))
67
+ except Exception:
68
+ return "[dim]Invalid[/dim]"
69
+ return str(peer_id)
70
+
71
+ def format_identity(identity: dict) -> str:
72
+ """Format identity dict - show only non-empty fields."""
73
+ if not identity:
74
+ return "[dim]No identity set[/dim]"
75
+
76
+ # Filter out empty values
77
+ non_empty = {k: v for k, v in identity.items() if v and v.strip()}
78
+
79
+ if not non_empty:
80
+ return "[dim]No identity information[/dim]"
81
+
82
+ lines = []
83
+ for key, value in non_empty.items():
84
+ formatted_key = key.replace('_', ' ').title()
85
+ lines.append(f" • {formatted_key}: {value}")
86
+
87
+ return "\n".join(lines)
88
+
89
+ def format_classification(classification: dict) -> str:
90
+ """Format classification information."""
91
+ if not classification:
92
+ return "[dim]Unknown[/dim]"
93
+
94
+ node_class = classification.get('node_class', 'Unknown')
95
+ start_epoch = classification.get('start_epoch', 'N/A')
96
+
97
+ # Color code based on class
98
+ if node_class == 'Active':
99
+ return f"[green]✓ Active[/green] (since epoch {start_epoch})"
100
+ elif node_class == 'Deactivated':
101
+ return f"[yellow]⚠ Deactivated[/yellow] (since epoch {start_epoch})"
102
+ elif node_class == 'Included':
103
+ return f"[cyan]◉ Included[/cyan] (since epoch {start_epoch})"
104
+ elif node_class == 'Queue':
105
+ return f"[blue]⋯ Queued[/blue] (since epoch {start_epoch})"
106
+ else:
107
+ return f"{node_class} (epoch {start_epoch})"
108
+
109
+ def format_reputation(reputation: dict) -> str:
110
+ """Format reputation information."""
111
+ if not reputation:
112
+ return "[dim]No reputation data[/dim]"
113
+
114
+ score = reputation.get('score', 0) / 1e12 # Assuming score is scaled
115
+ lifetime_nodes = reputation.get('lifetime_node_count', 0)
116
+ total_active = reputation.get('total_active_nodes', 0)
117
+ increases = reputation.get('total_increases', 0)
118
+ decreases = reputation.get('total_decreases', 0)
119
+ avg_attestation = reputation.get('average_attestation', 0)
120
+
121
+ return f""" • Score: {score:,.2f}
122
+ • Lifetime Nodes: {lifetime_nodes}
123
+ • Active Nodes: {total_active}
124
+ • Increases: {increases} | Decreases: {decreases}
125
+ • Avg Attestation: {avg_attestation}"""
126
+
127
+ def format_bytes_field(value) -> str:
128
+ """Best-effort formatting for optional bytes/opaque fields."""
129
+ if value is None:
130
+ return "[dim]Not set[/dim]"
131
+ if isinstance(value, (bytes, bytearray)):
132
+ try:
133
+ decoded = value.decode("utf-8", errors="ignore")
134
+ return decoded if decoded else f"0x{value.hex()}"
135
+ except Exception:
136
+ return f"0x{value.hex()}"
137
+ return str(value)
138
+
139
+ # Format balances
140
+ stake_balance = format_balance(node_data.stake_balance)
141
+ delegate_stake = format_balance(node_data.node_delegate_stake_balance)
142
+
143
+ # Format classification
144
+ classification_display = format_classification(node_data.classification)
145
+
146
+ # Format identity
147
+ identity_display = format_identity(node_data.identity)
148
+
149
+ # Format reputation (prefer coldkey_reputation if available)
150
+ raw_reputation = getattr(node_data, "coldkey_reputation", None)
151
+ if not raw_reputation:
152
+ raw_reputation = getattr(node_data, "reputation", None)
153
+ reputation_display = format_reputation(raw_reputation)
154
+
155
+ # Build the display - show all relevant fields
156
+ node_info = f"""[htcli.accent]Basic Information[/htcli.accent]
157
+ [htcli.value]Subnet ID:[/htcli.value] {subnet_id}
158
+ [htcli.value]Node ID:[/htcli.value] {node_id}
159
+ [htcli.value]Status:[/htcli.value] {classification_display}
160
+
161
+ [htcli.accent]Wallet Addresses[/htcli.accent]
162
+ [htcli.value]Coldkey:[/htcli.value] {node_data.coldkey}
163
+ [htcli.value]Hotkey:[/htcli.value] {node_data.hotkey}
164
+
165
+ [htcli.accent]Network Configuration[/htcli.accent]
166
+ [htcli.value]Peer ID:[/htcli.value] {format_peer_id(node_data.peer_id)}
167
+ [htcli.value]Bootnode Peer ID:[/htcli.value] {format_peer_id(node_data.bootnode_peer_id)}
168
+ [htcli.value]Client Peer ID:[/htcli.value] {format_peer_id(node_data.client_peer_id)}
169
+ [htcli.value]Is Bootnode:[/htcli.value] {"Yes" if node_data.bootnode else "No"}
170
+
171
+ [htcli.accent]Economics[/htcli.accent]
172
+ [htcli.value]Node Stake:[/htcli.value] {amount(stake_balance)}
173
+ [htcli.value]Total Delegate Stake:[/htcli.value] {amount(delegate_stake)}
174
+ [htcli.value]Delegate Reward Rate:[/htcli.value] {node_data.delegate_reward_rate}%
175
+ [htcli.value]Penalties:[/htcli.value] {getattr(node_data, 'penalties', 0)}
176
+ [htcli.value]Total Node Delegate Stake Shares:[/htcli.value] {getattr(node_data, 'total_node_delegate_stake_shares', 0)}
177
+ [htcli.value]Last Delegate Reward Rate Update Epoch:[/htcli.value] {getattr(node_data, 'last_delegate_reward_rate_update', 0)}
178
+
179
+ [htcli.accent]Activity & Scheduling[/htcli.accent]
180
+ [htcli.value]Node Slot Index:[/htcli.value] {getattr(node_data, 'node_slot_index', '[dim]N/A[/dim]')}
181
+ [htcli.value]Consecutive Idle Epochs:[/htcli.value] {getattr(node_data, 'consecutive_idle_epochs', 0)}
182
+ [htcli.value]Consecutive Included Epochs:[/htcli.value] {getattr(node_data, 'consecutive_included_epochs', 0)}
183
+
184
+ [htcli.accent]Custom Parameters[/htcli.accent]
185
+ [htcli.value]Unique:[/htcli.value] {format_bytes_field(getattr(node_data, 'unique', None))}
186
+ [htcli.value]Non-Unique:[/htcli.value] {format_bytes_field(getattr(node_data, 'non_unique', None))}
187
+
188
+ [htcli.accent]Identity[/htcli.accent]
189
+ {identity_display}
190
+
191
+ [htcli.accent]Reputation[/htcli.accent]
192
+ {reputation_display}
193
+ [htcli.value]Subnet Node Reputation:[/htcli.value] {getattr(node_data, 'subnet_node_reputation', 0)}
194
+ """
195
+
196
+ panel = HTCLIPanel(
197
+ node_info,
198
+ title=f"🔍 Node {node_id} Details",
199
+ border_style="htcli.info",
200
+ highlight=True,
201
+ )
202
+ panel.render(console.console)
203
+
204
+
205
+ def display_overwatch_commits_rpc(
206
+ commits_data: list[Any], epoch: int, overwatch_node_id: int
207
+ ):
208
+ """Display overwatch commits from RPC response."""
209
+ if not commits_data:
210
+ console.print(
211
+ info(
212
+ f"No overwatch commits found for epoch {epoch}, node {overwatch_node_id}"
213
+ )
214
+ )
215
+ return
216
+
217
+ from ...ui.components import HTCLIPanel
218
+
219
+ # Display overwatch commits
220
+ commits_info = f"""
221
+ [htcli.accent]Overwatch Commits[/htcli.accent]
222
+
223
+ [htcli.value]Epoch:[/htcli.value] {epoch}
224
+ [htcli.value]Overwatch Node ID:[/htcli.value] {overwatch_node_id}
225
+ [htcli.value]Total Commits:[/htcli.value] {len(commits_data)}
226
+
227
+ [htcli.accent]Commits:[/htcli.accent]
228
+ {chr(10).join(f"• Subnet {commit['subnet_id']}: {commit['commit_hash']}" for commit in commits_data)}
229
+ """
230
+
231
+ panel = HTCLIPanel(
232
+ commits_info,
233
+ title="🔐 Overwatch Commits",
234
+ border_style="htcli.info",
235
+ highlight=True,
236
+ )
237
+ panel.render(console.console)
238
+
239
+
240
+ def display_overwatch_reveals_rpc(
241
+ reveals_data: list[Any], epoch: int, overwatch_node_id: int
242
+ ):
243
+ """Display overwatch reveals from RPC response."""
244
+ if not reveals_data:
245
+ console.print(
246
+ info(
247
+ f"No overwatch reveals found for epoch {epoch}, node {overwatch_node_id}"
248
+ )
249
+ )
250
+ return
251
+
252
+ from ...ui.components import HTCLIPanel
253
+
254
+ # Display overwatch reveals
255
+ reveals_info = f"""
256
+ [htcli.accent]Overwatch Reveals[/htcli.accent]
257
+
258
+ [htcli.value]Epoch:[/htcli.value] {epoch}
259
+ [htcli.value]Overwatch Node ID:[/htcli.value] {overwatch_node_id}
260
+ [htcli.value]Total Reveals:[/htcli.value] {len(reveals_data)}
261
+
262
+ [htcli.accent]Reveals:[/htcli.accent]
263
+ {chr(10).join(f"• Subnet {reveal['subnet_id']}: Weight {reveal['weight']}" for reveal in reveals_data)}
264
+ """
265
+
266
+ panel = HTCLIPanel(
267
+ reveals_info,
268
+ title="🔓 Overwatch Reveals",
269
+ border_style="htcli.info",
270
+ highlight=True,
271
+ )
272
+ panel.render(console.console)
273
+
274
+
275
+ # ============================================================================
276
+ # EXTRINSIC DISPLAY FUNCTIONS - Write Operations
277
+ # ============================================================================
278
+
279
+
280
+ def display_node_register_result(response):
281
+ """Display node registration result."""
282
+ if hasattr(response, "success") and response.success:
283
+ stake_balance_tensor = (
284
+ (response.stake_balance / 1e18)
285
+ if hasattr(response, "stake_balance") and response.stake_balance
286
+ else 0
287
+ )
288
+
289
+ # Try to get block number if it's missing but we have block_hash
290
+ block_number = response.block_number if hasattr(response, "block_number") else None
291
+ if not block_number and hasattr(response, "block_hash") and response.block_hash:
292
+ try:
293
+ from ...dependencies import get_client
294
+ client = get_client()
295
+ if client and client.rpc and client.rpc.chain:
296
+ block_number = client.rpc.chain.get_block_number(response.block_hash)
297
+ except Exception:
298
+ pass
299
+
300
+ block_hash = getattr(response, "block_hash", None)
301
+ content = f"""[htcli.success]✅ Node Registered Successfully![/htcli.success]
302
+
303
+ [htcli.value]Subnet ID:[/htcli.value] {response.subnet_id if hasattr(response, "subnet_id") else "N/A"}
304
+ [htcli.value]Validator ID:[/htcli.value] {response.validator_id if hasattr(response, "validator_id") else "N/A"}
305
+ [htcli.value]Node ID:[/htcli.value] {response.subnet_node_id if hasattr(response, "subnet_node_id") else "N/A"}
306
+ [htcli.value]Hotkey:[/htcli.value] {response.hotkey if hasattr(response, "hotkey") else "N/A"}
307
+ [htcli.value]Initial Stake:[/htcli.value] {amount(f"{stake_balance_tensor:,.4f} TENSOR")}
308
+ [htcli.value]Transaction Hash:[/htcli.value] {response.transaction_hash or "N/A"}
309
+ [htcli.value]Block Number:[/htcli.value] {block_number or "N/A"}
310
+ """
311
+ if block_hash:
312
+ content += f"[htcli.value]Block Hash:[/htcli.value] {block_hash}\n"
313
+ content += "\n[htcli.info]💡 Your node is now registered! Subnet may need additional nodes to activate.[/htcli.info]\n"
314
+
315
+ panel = HTCLIPanel(
316
+ content,
317
+ title="🚀 Node Registration Complete",
318
+ border_style="htcli.success",
319
+ highlight=True,
320
+ )
321
+ panel.render(console.console)
322
+ else:
323
+ error_msg = response.error if hasattr(response, "error") else "Unknown error"
324
+
325
+ if "ColdkeyRegistrationWhitelist" in error_msg:
326
+ panel = HTCLIPanel(
327
+ """Only whitelisted coldkeys can register nodes while the subnet is still in its registration period.
328
+
329
+ 💡 What you can do:
330
+ • Use one of the coldkeys supplied in --initial-coldkeys when the subnet was created
331
+ • Check each coldkey's remaining slots via `htcli subnet info --subnet-id <id>`
332
+ • Wait until the subnet activates—once active, the whitelist is removed
333
+ """,
334
+ title="⚠️ Coldkey Not On Registration Whitelist",
335
+ border_style="htcli.error",
336
+ highlight=True,
337
+ )
338
+ panel.render(console.console)
339
+ return
340
+
341
+ # Parse error message - it might be a dict or string
342
+ error_text = str(error_msg)
343
+ if isinstance(error_msg, dict):
344
+ # Extract message and data from error dict
345
+ error_text = error_msg.get("message", "Transaction failed")
346
+ error_data = error_msg.get("data", "")
347
+ if error_data:
348
+ error_text = f"{error_text}: {error_data}"
349
+
350
+ # Handle specific error types with helpful messages
351
+ error_lower = error_text.lower()
352
+
353
+ if "maxregisterednodes" in error_lower or "max registered nodes" in error_lower:
354
+ panel = HTCLIPanel(
355
+ """The subnet has reached its maximum number of registered nodes.
356
+
357
+ 💡 What this means:
358
+ • Each subnet has a MaxRegisteredNodes limit controlling how many nodes can be registered
359
+ • This limit may also include per-coldkey registration slots from the subnet's initial coldkeys
360
+
361
+ 💡 What you can do:
362
+ • Check current node count and limits: htcli subnet info --subnet-id <id>
363
+ • If you are the subnet owner, increase the max registered nodes via subnet configuration/governance
364
+ • Or remove an existing node first: htcli node remove --subnet-id <id> --node-id <node-id>
365
+ """,
366
+ title="⚠️ Subnet Node Capacity Reached",
367
+ border_style="htcli.error",
368
+ highlight=True,
369
+ )
370
+ panel.render(console.console)
371
+ return
372
+
373
+ if "inability to pay" in error_lower or "account balance too low" in error_lower or "insufficient" in error_lower:
374
+ panel = HTCLIPanel(
375
+ """Your account balance is too low to complete the transaction.
376
+
377
+ 💡 How to fix this:
378
+ • Check your balance: htcli wallet balance --wallet <wallet-name>
379
+ • Ensure you have enough funds for:
380
+ - The stake amount you specified
381
+ - Transaction fees (typically 0.001-0.01 TENSOR)
382
+ • Add more funds to your wallet if needed
383
+ """,
384
+ title="⚠️ Insufficient Balance",
385
+ border_style="htcli.error",
386
+ highlight=True,
387
+ )
388
+ panel.render(console.console)
389
+ return
390
+
391
+ if "maxstakereached" in error_lower or "max stake reached" in error_lower:
392
+ panel = HTCLIPanel(
393
+ """The stake amount you specified exceeds the subnet's maximum stake limit.
394
+
395
+ 💡 How to fix this:
396
+ • Check the subnet's maximum stake: htcli subnet info --subnet-id <id>
397
+ • Reduce your stake amount to be within the subnet's limits
398
+ • The stake amount must be less than or equal to the subnet's max_stake value
399
+ """,
400
+ title="⚠️ Maximum Stake Limit Reached",
401
+ border_style="htcli.error",
402
+ highlight=True,
403
+ )
404
+ panel.render(console.console)
405
+ return
406
+
407
+ if "hotkeyhasowner" in error_lower or "hotkey has owner" in error_lower:
408
+ panel = HTCLIPanel(
409
+ """This hotkey is already registered to a node.
410
+
411
+ 💡 What this means:
412
+ • Hotkey needs to be unique across the network, not within that subnet
413
+ • The hotkey you're trying to use is already associated with an existing node somewhere on the network
414
+
415
+ 💡 What you can do:
416
+ • Use a different hotkey wallet for this registration
417
+ • Check existing nodes: htcli node list --subnet-id <id>
418
+ • If you need to use this hotkey, you may need to remove the existing node first
419
+ """,
420
+ title="⚠️ Hotkey Already Registered",
421
+ border_style="htcli.error",
422
+ highlight=True,
423
+ )
424
+ panel.render(console.console)
425
+ return
426
+
427
+ if "invalid transaction" in error_lower:
428
+ panel = HTCLIPanel(
429
+ f"""{error_text}
430
+
431
+ 💡 What you can do:
432
+ • Verify all parameters are correct
433
+ • Check your wallet balance
434
+ • Ensure the subnet is accepting registrations
435
+ • Review command help: htcli node register --help
436
+ """,
437
+ title="⚠️ Transaction Failed",
438
+ border_style="htcli.error",
439
+ highlight=True,
440
+ )
441
+ panel.render(console.console)
442
+ return
443
+
444
+ # Generic error display
445
+ panel = HTCLIPanel(
446
+ f"""{error_text}
447
+
448
+ 💡 Troubleshooting:
449
+ • Verify node parameters are valid
450
+ • Check that the subnet exists and accepts registrations
451
+ • Ensure you have sufficient balance for stake and fees
452
+ • Review command help: htcli node register --help
453
+ """,
454
+ title="⚠️ Node Registration Failed",
455
+ border_style="htcli.error",
456
+ highlight=True,
457
+ )
458
+ panel.render(console.console)
459
+
460
+
461
+ def display_node_bootnode_peer_update(
462
+ response, subnet_id: int, node_id: int, old_peer_id: Optional[str], new_peer_id: str
463
+ ):
464
+ """Display bootnode peer ID update result."""
465
+ if hasattr(response, "success") and response.success:
466
+ old_display = old_peer_id[:50] + "..." if old_peer_id and len(old_peer_id) > 50 else (old_peer_id or "N/A")
467
+ new_display = new_peer_id[:50] + "..." if len(new_peer_id) > 50 else new_peer_id
468
+
469
+ tx_hash = getattr(response, "transaction_hash", None) or getattr(response, "extrinsic_hash", None)
470
+ block_number = getattr(response, "block_number", None)
471
+ block_hash = getattr(response, "block_hash", None)
472
+
473
+ content = f"""[htcli.success]✅ Bootnode Peer ID Updated Successfully![/htcli.success]
474
+
475
+ [htcli.value]Subnet ID:[/htcli.value] {subnet_id}
476
+ [htcli.value]Node ID:[/htcli.value] {node_id}
477
+ [htcli.value]Old Bootnode Peer ID:[/htcli.value] {old_display}
478
+ [htcli.value]New Bootnode Peer ID:[/htcli.value] {new_display}
479
+ [htcli.value]Transaction Hash:[/htcli.value] {tx_hash or "N/A"}
480
+ [htcli.value]Block Number:[/htcli.value] {block_number or "N/A"}
481
+ """
482
+ if block_hash:
483
+ content += f"[htcli.value]Block Hash:[/htcli.value] {block_hash}\n"
484
+ content += "\n[htcli.info]💡 The node's bootnode peer ID has been updated on-chain.[/htcli.info]\n"
485
+
486
+ panel = HTCLIPanel(
487
+ content,
488
+ title="🔄 Bootnode Peer ID Update Complete",
489
+ border_style="htcli.success",
490
+ highlight=True,
491
+ )
492
+ panel.render(console.console)
493
+ else:
494
+ error_msg = response.error if hasattr(response, "error") else "Unknown error"
495
+ console.print(f"[htcli.error]❌ Failed to update bootnode peer ID: {error_msg}[/]")
496
+
497
+
498
+ def display_node_client_peer_update(
499
+ response, subnet_id: int, node_id: int, old_peer_id: Optional[str], new_peer_id: str
500
+ ):
501
+ """Display client peer ID update result."""
502
+ if hasattr(response, "success") and response.success:
503
+ old_display = old_peer_id[:50] + "..." if old_peer_id and len(old_peer_id) > 50 else (old_peer_id or "N/A")
504
+ new_display = new_peer_id[:50] + "..." if len(new_peer_id) > 50 else new_peer_id
505
+
506
+ tx_hash = getattr(response, "transaction_hash", None) or getattr(response, "extrinsic_hash", None)
507
+ block_number = getattr(response, "block_number", None)
508
+ block_hash = getattr(response, "block_hash", None)
509
+
510
+ content = f"""[htcli.success]✅ Client Peer ID Updated Successfully![/htcli.success]
511
+
512
+ [htcli.value]Subnet ID:[/htcli.value] {subnet_id}
513
+ [htcli.value]Node ID:[/htcli.value] {node_id}
514
+ [htcli.value]Old Client Peer ID:[/htcli.value] {old_display}
515
+ [htcli.value]New Client Peer ID:[/htcli.value] {new_display}
516
+ [htcli.value]Transaction Hash:[/htcli.value] {tx_hash or "N/A"}
517
+ [htcli.value]Block Number:[/htcli.value] {block_number or "N/A"}
518
+ """
519
+ if block_hash:
520
+ content += f"[htcli.value]Block Hash:[/htcli.value] {block_hash}\n"
521
+ content += "\n[htcli.info]💡 The node's client peer ID has been updated on-chain.[/htcli.info]\n"
522
+
523
+ panel = HTCLIPanel(
524
+ content,
525
+ title="🔄 Client Peer ID Update Complete",
526
+ border_style="htcli.success",
527
+ highlight=True,
528
+ )
529
+ panel.render(console.console)
530
+ else:
531
+ error_msg = response.error if hasattr(response, "error") else "Unknown error"
532
+ console.print(f"[htcli.error]❌ Failed to update client peer ID: {error_msg}[/]")
533
+
534
+
535
+ def display_node_remove_result(
536
+ result: Any,
537
+ subnet_id: int,
538
+ node_id: int,
539
+ ):
540
+ """Display subnet node removal result."""
541
+ success_flag = (
542
+ result.get("success", False) if isinstance(result, dict) else getattr(result, "success", False)
543
+ )
544
+
545
+ if success_flag:
546
+ tx_hash = (
547
+ result.get("transaction_hash")
548
+ or result.get("extrinsic_hash")
549
+ if isinstance(result, dict)
550
+ else getattr(result, "transaction_hash", None)
551
+ or getattr(result, "extrinsic_hash", None)
552
+ )
553
+ block_number = (
554
+ result.get("block_number") if isinstance(result, dict) else getattr(result, "block_number", None)
555
+ )
556
+ block_hash = (
557
+ result.get("block_hash") if isinstance(result, dict) else getattr(result, "block_hash", None)
558
+ )
559
+
560
+ content = f"""[htcli.success]✅ Node Removed Successfully[/htcli.success]
561
+
562
+ [htcli.value]Subnet ID:[/htcli.value] {subnet_id}
563
+ [htcli.value]Node ID:[/htcli.value] {node_id}
564
+ [htcli.value]Transaction Hash:[/htcli.value] {tx_hash or "N/A"}
565
+ [htcli.value]Block Number:[/htcli.value] {block_number or "N/A"}
566
+ """
567
+ if block_hash:
568
+ content += f"[htcli.value]Block Hash:[/htcli.value] {block_hash}\n"
569
+
570
+ panel = HTCLIPanel(
571
+ content,
572
+ title="🗑️ Node Removal Complete",
573
+ border_style="htcli.success",
574
+ highlight=True,
575
+ )
576
+ panel.render(console.console)
577
+ else:
578
+ error_msg = result.get("error", "Unknown error") if isinstance(result, dict) else getattr(
579
+ result, "error", "Unknown error"
580
+ )
581
+ console.print(f"[htcli.error]❌ Failed to remove node: {error_msg}[/]")
582
+
583
+
584
+ def display_node_lifecycle_result(
585
+ result: Any,
586
+ subnet_id: int,
587
+ node_id: int,
588
+ action: str,
589
+ ):
590
+ """Display subnet node activation/deactivation result."""
591
+ success_flag = (
592
+ result.get("success", False)
593
+ if isinstance(result, dict)
594
+ else getattr(result, "success", False)
595
+ )
596
+ action_title = action.title()
597
+
598
+ if success_flag:
599
+ tx_hash = (
600
+ result.get("transaction_hash") or result.get("extrinsic_hash")
601
+ if isinstance(result, dict)
602
+ else getattr(result, "transaction_hash", None)
603
+ or getattr(result, "extrinsic_hash", None)
604
+ )
605
+ block_number = (
606
+ result.get("block_number")
607
+ if isinstance(result, dict)
608
+ else getattr(result, "block_number", None)
609
+ )
610
+ block_hash = (
611
+ result.get("block_hash")
612
+ if isinstance(result, dict)
613
+ else getattr(result, "block_hash", None)
614
+ )
615
+
616
+ content = f"""[htcli.success]✅ Node {action_title}d Successfully[/htcli.success]
617
+
618
+ [htcli.value]Subnet ID:[/htcli.value] {subnet_id}
619
+ [htcli.value]Node ID:[/htcli.value] {node_id}
620
+ [htcli.value]Transaction Hash:[/htcli.value] {tx_hash or "N/A"}
621
+ [htcli.value]Block Number:[/htcli.value] {block_number or "N/A"}
622
+ """
623
+ if block_hash:
624
+ content += f"[htcli.value]Block Hash:[/htcli.value] {block_hash}\n"
625
+
626
+ panel = HTCLIPanel(
627
+ content,
628
+ title=f"Node {action_title} Complete",
629
+ border_style="htcli.success",
630
+ highlight=True,
631
+ )
632
+ panel.render(console.console)
633
+ else:
634
+ error_msg = (
635
+ result.get("error", "Unknown error")
636
+ if isinstance(result, dict)
637
+ else getattr(result, "error", "Unknown error")
638
+ )
639
+ console.print(f"[htcli.error]❌ Failed to {action} node: {error_msg}[/]")
640
+
641
+
642
+ def display_node_update_summary(
643
+ subnet_id: int,
644
+ node_id: int,
645
+ updates: list[dict[str, Any]],
646
+ client: Optional[Any] = None,
647
+ ):
648
+ """
649
+ Display a consolidated summary of multiple node updates in a single panel.
650
+
651
+ Each entry in `updates` should be a dict with:
652
+ - field: str
653
+ - old: Any
654
+ - new: Any
655
+ - result: dict or response object
656
+ """
657
+ if not updates:
658
+ return
659
+
660
+ def format_field_name(field: str) -> str:
661
+ return field.replace("_", " ").title()
662
+
663
+ def get_transaction_hash(result: Any) -> Optional[str]:
664
+ if isinstance(result, dict):
665
+ return result.get("transaction_hash") or result.get("extrinsic_hash")
666
+ return (
667
+ getattr(result, "transaction_hash", None)
668
+ or getattr(result, "extrinsic_hash", None)
669
+ )
670
+
671
+ def get_block_hash(result: Any) -> Optional[str]:
672
+ if isinstance(result, dict):
673
+ return result.get("block_hash")
674
+ return getattr(result, "block_hash", None)
675
+
676
+ def get_block_number(result: Any) -> Optional[int]:
677
+ if isinstance(result, dict):
678
+ return result.get("block_number")
679
+ return getattr(result, "block_number", None)
680
+
681
+ def is_success(result: Any) -> bool:
682
+ if isinstance(result, dict):
683
+ return bool(result.get("success", False))
684
+ return bool(getattr(result, "success", False))
685
+
686
+ lines: list[str] = [
687
+ success(
688
+ f"✅ Node {node_id} on subnet {subnet_id} updated successfully!"
689
+ )
690
+ ]
691
+ lines.append("") # blank line
692
+
693
+ for update in updates:
694
+ field = update.get("field")
695
+ old = update.get("old")
696
+ new = update.get("new")
697
+ result = update.get("result")
698
+
699
+ field_display = format_field_name(str(field))
700
+ status_text = "[green]Success[/green]" if is_success(result) else "[htcli.error]Failed[/htcli.error]"
701
+ lines.append(f"[htcli.value]{field_display}:[/] {status_text}")
702
+
703
+ if old is not None or new is not None:
704
+ old_disp = str(old) if old is not None else "[dim]N/A[/dim]"
705
+ new_disp = str(new) if new is not None else "[dim]N/A[/dim]"
706
+ lines.append(f" [htcli.subtitle]Old:[/] {old_disp}")
707
+ lines.append(f" [htcli.subtitle]New:[/] {new_disp}")
708
+
709
+ tx_hash = get_transaction_hash(result)
710
+ block_hash = get_block_hash(result)
711
+ block_number = get_block_number(result)
712
+
713
+ if not block_number and block_hash and client and getattr(client, "rpc", None) and getattr(
714
+ client.rpc, "chain", None
715
+ ):
716
+ try:
717
+ block_number = client.rpc.chain.get_block_number(block_hash=block_hash)
718
+ except Exception:
719
+ pass
720
+
721
+ if tx_hash:
722
+ lines.append(
723
+ f" [htcli.subtitle]Transaction Hash:[/] [htcli.hash]{tx_hash}[/]"
724
+ )
725
+ if block_number:
726
+ lines.append(
727
+ f" [htcli.subtitle]Block Number:[/] [htcli.value]{block_number}[/]"
728
+ )
729
+ if block_hash:
730
+ lines.append(
731
+ f" [htcli.subtitle]Block Hash:[/] [htcli.hash]{block_hash}[/]"
732
+ )
733
+ lines.append(
734
+ )
735
+
736
+ lines.append("") # space between updates
737
+
738
+ if lines and lines[-1] == "":
739
+ lines.pop()
740
+
741
+ content = "\n".join(lines)
742
+
743
+ panel = HTCLIPanel(
744
+ content,
745
+ title="✅ Node Updates Complete",
746
+ border_style="htcli.success",
747
+ highlight=True,
748
+ )
749
+ panel.render(console.console)