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,801 @@
1
+ from collections import Counter
2
+ from typing import Any, Optional
3
+
4
+ from ...ui.colors import error, info, success
5
+ from ...ui.components import HTCLIPanel, create_subnet_details_tree, create_subnet_table
6
+ from ...ui.display import HTCLIConsole
7
+
8
+ console = HTCLIConsole()
9
+
10
+
11
+ def display_subnet_list(subnets: list[dict[str, Any]]):
12
+ """Displays a list of subnets in a table with bright colors and comprehensive information."""
13
+ if not subnets:
14
+ console.print(info("ℹ️ No subnets found."))
15
+ return
16
+
17
+ # Use the specialized subnet table function from HTCLI components
18
+ table = create_subnet_table(subnets)
19
+ console.print() # Empty line before table
20
+ table.render()
21
+ console.print() # Empty line after table
22
+
23
+
24
+ def display_subnets_overview(subnets: list[Any]):
25
+ """Display a network-wide overview of all subnets in a comprehensive table layout."""
26
+ if not subnets:
27
+ console.print(info("ℹ️ No subnets found on the network."))
28
+ return
29
+
30
+ def _decode_bytes(value: Any, fallback: str) -> str:
31
+ if isinstance(value, bytes):
32
+ try:
33
+ decoded = value.decode("utf-8").strip()
34
+ return decoded or fallback
35
+ except UnicodeDecodeError:
36
+ return fallback
37
+ if isinstance(value, str):
38
+ return value or fallback
39
+ return fallback
40
+
41
+ table_data: list[dict[str, Any]] = []
42
+ state_counter: Counter[str] = Counter()
43
+ total_nodes = 0
44
+ total_active_nodes = 0
45
+
46
+ for subnet in subnets:
47
+ if hasattr(subnet, "model_dump"):
48
+ raw = subnet.model_dump()
49
+ elif isinstance(subnet, dict):
50
+ raw = dict(subnet)
51
+ else:
52
+ continue
53
+
54
+ name = _decode_bytes(raw.get("name"), "Unnamed")
55
+ owner = raw.get("owner") or "Unknown"
56
+ state_value = raw.get("state")
57
+ if hasattr(state_value, "value"):
58
+ state_text = state_value.value
59
+ elif hasattr(state_value, "name"):
60
+ state_text = state_value.name
61
+ else:
62
+ state_text = str(state_value or "unknown")
63
+ normalized_state = state_text.lower()
64
+ state_counter[normalized_state] += 1
65
+
66
+ total_nodes += raw.get("total_nodes") or 0
67
+ total_active_nodes += raw.get("total_active_nodes") or 0
68
+
69
+ # Prefer new field names from updated SubnetInfo, fall back to legacy keys if present
70
+ total_stake_raw = (
71
+ raw.get("total_subnet_stake")
72
+ or raw.get("total_stake")
73
+ or raw.get("total_stake_balance")
74
+ )
75
+ if isinstance(total_stake_raw, (int, float)):
76
+ # Convert from wei if the value looks like a wei amount
77
+ stake_display = (
78
+ int(total_stake_raw / 1e18) if total_stake_raw > 1e12 else int(total_stake_raw)
79
+ )
80
+ else:
81
+ stake_display = 0
82
+
83
+ table_data.append(
84
+ {
85
+ "id": raw.get("id", "N/A"),
86
+ "name": name,
87
+ "owner": owner,
88
+ "total_nodes": raw.get("total_nodes", 0),
89
+ "total_stake": stake_display,
90
+ "state": state_text.title(),
91
+ }
92
+ )
93
+
94
+ overview_lines = [
95
+ f"[htcli.value]Total Subnets:[/] {len(table_data)}",
96
+ f"[htcli.success]Active:[/] {state_counter.get('active', 0)}",
97
+ f"[htcli.warning]Registered:[/] {state_counter.get('registered', 0)}",
98
+ f"[htcli.error]Paused:[/] {state_counter.get('paused', 0)}",
99
+ "",
100
+ f"[htcli.value]Total Nodes:[/] {total_nodes}",
101
+ f"[htcli.value]Active Nodes:[/] {total_active_nodes}",
102
+ ]
103
+ overview_panel = HTCLIPanel(
104
+ "\n".join(overview_lines),
105
+ title="🌐 Network Overview",
106
+ border_style="htcli.accent",
107
+ highlight=True,
108
+ )
109
+ overview_panel.render()
110
+
111
+ table = create_subnet_table(table_data)
112
+ table.render()
113
+
114
+
115
+ def display_subnet_info(subnet: dict[str, Any]):
116
+ """Displays detailed information for a single subnet."""
117
+ if not subnet:
118
+ console.print(error("Subnet data could not be retrieved."))
119
+ return
120
+
121
+ # Use the specialized subnet details tree function from HTCLI components
122
+ tree = create_subnet_details_tree(subnet)
123
+ tree.render()
124
+
125
+
126
+ def display_subnet_nodes(nodes: list[dict[str, Any]]):
127
+ """Displays a list of nodes in a table."""
128
+ if not nodes:
129
+ console.print(info("No nodes found in this subnet."))
130
+ return
131
+
132
+ # Use HTCLITable for consistent styling
133
+ from ...ui.components import HTCLITable
134
+
135
+ table = HTCLITable(title="Subnet Nodes")
136
+ table.add_column("Node ID", style="htcli.value", justify="center")
137
+ table.add_column("Owner", style="htcli.address")
138
+ table.add_column("Hotkey", style="htcli.address")
139
+ table.add_column("Status", style="htcli.status.active")
140
+
141
+ # Add nodes with proper formatting
142
+ from ...utils.wallet.crypto import format_address_display
143
+ for i, node in enumerate(nodes):
144
+ node_id = str(node.get("id", i))
145
+ owner = node.get("owner", "N/A")
146
+ hotkey = node.get("hotkey", "N/A")
147
+ status = node.get("status", "Unknown")
148
+
149
+ # Format addresses properly with checksum
150
+ owner_formatted = f"[htcli.address]{format_address_display(owner)}[/]" if owner != "N/A" else owner
151
+ hotkey_formatted = f"[htcli.address]{format_address_display(hotkey)}[/]" if hotkey != "N/A" else hotkey
152
+
153
+ table.add_row(node_id, owner_formatted, hotkey_formatted, status)
154
+
155
+ # Panel header for consistent framing
156
+ panel = HTCLIPanel(
157
+ f"[htcli.accent]Subnet Nodes[/htcli.accent]\n\n[htcli.value]Total:[/htcli.value] {len(nodes)}",
158
+ title="🧩 Subnet Nodes",
159
+ border_style="htcli.info",
160
+ highlight=True,
161
+ )
162
+ panel.render()
163
+ table.render()
164
+
165
+
166
+ def display_activation_requirements(requirements: dict[str, Any]):
167
+ """Displays subnet activation requirements check with comprehensive timeline."""
168
+ subnet_id = requirements.get("subnet_id", "Unknown")
169
+
170
+ # Build epoch information section
171
+ epoch_info_lines = []
172
+ current_epoch = requirements.get("current_epoch")
173
+ current_block = requirements.get("current_block")
174
+ registration_epoch = requirements.get("registration_epoch")
175
+ min_registration_epochs = requirements.get("min_registration_epochs")
176
+ epochs_elapsed = requirements.get("epochs_elapsed")
177
+ epochs_remaining = requirements.get("epochs_remaining")
178
+
179
+ if current_epoch is not None:
180
+ epoch_info_lines.append(f"Current Epoch: {current_epoch}")
181
+ if current_block is not None:
182
+ epoch_info_lines.append(f"Current Block: {current_block:,}")
183
+ if registration_epoch is not None:
184
+ epoch_info_lines.append(f"Registration Epoch: {registration_epoch}")
185
+ if min_registration_epochs is not None:
186
+ epoch_info_lines.append(f"Minimum Registration Epochs: {min_registration_epochs}")
187
+ if epochs_elapsed is not None:
188
+ epoch_info_lines.append(f"Epochs Elapsed: {epochs_elapsed}")
189
+ if epochs_remaining is not None and epochs_remaining > 0:
190
+ epoch_info_lines.append(f"Epochs Remaining: {epochs_remaining}")
191
+
192
+ # Build timeline section from new timeline data
193
+ timeline_lines = []
194
+ timeline = requirements.get("timeline")
195
+ if timeline:
196
+ # Current phase - most important info
197
+ current_phase = timeline.get("current_phase")
198
+ if current_phase:
199
+ timeline_lines.append(f"🎯 Current Phase: {current_phase}")
200
+
201
+ # Registration datetime (when subnet was registered)
202
+ registration_datetime = timeline.get("registration_datetime")
203
+ if registration_datetime:
204
+ timeline_lines.append(f"📋 Registered: {registration_datetime}")
205
+
206
+ # Key milestones with datetime estimates
207
+ min_activation_epoch = timeline.get("min_activation_epoch")
208
+ registration_deadline_epoch = timeline.get("registration_deadline_epoch")
209
+ enactment_deadline_epoch = timeline.get("enactment_deadline_epoch")
210
+
211
+ if min_activation_epoch is not None:
212
+ time_info = timeline.get("time_until_min_activation", {})
213
+ time_str = time_info.get("human_readable", "")
214
+ datetime_str = time_info.get("datetime_short", "")
215
+ if datetime_str and time_str != "Now eligible":
216
+ timeline_lines.append(f"⏱️ Earliest Activation: Epoch {min_activation_epoch} ({time_str} - {datetime_str})")
217
+ else:
218
+ timeline_lines.append(f"⏱️ Earliest Activation: Epoch {min_activation_epoch} ({time_str})")
219
+
220
+ if registration_deadline_epoch is not None:
221
+ time_info = timeline.get("time_until_registration_deadline", {})
222
+ time_str = time_info.get("human_readable", "")
223
+ datetime_str = time_info.get("datetime_short", "")
224
+ if datetime_str and time_str != "Passed":
225
+ timeline_lines.append(f"📅 Registration Deadline: Epoch {registration_deadline_epoch} ({time_str} - {datetime_str})")
226
+ else:
227
+ timeline_lines.append(f"📅 Registration Deadline: Epoch {registration_deadline_epoch} ({time_str})")
228
+
229
+ if enactment_deadline_epoch is not None:
230
+ time_info = timeline.get("time_until_enactment_deadline", {})
231
+ time_str = time_info.get("human_readable", "")
232
+ datetime_str = time_info.get("datetime_short", "")
233
+ if datetime_str and "Passed" not in time_str:
234
+ timeline_lines.append(f"⚠️ Final Deadline: Epoch {enactment_deadline_epoch} ({time_str} - {datetime_str})")
235
+ else:
236
+ timeline_lines.append(f"⚠️ Final Deadline: Epoch {enactment_deadline_epoch} ({time_str})")
237
+
238
+ # Epoch length info
239
+ epoch_length = timeline.get("epoch_length")
240
+ block_time_ms = timeline.get("block_time_ms")
241
+ if epoch_length and block_time_ms:
242
+ epoch_duration_sec = (epoch_length * block_time_ms) / 1000
243
+ epoch_duration_min = epoch_duration_sec / 60
244
+ timeline_lines.append(f"📊 Epoch Duration: ~{epoch_duration_min:.0f} minutes ({epoch_length} blocks @ {block_time_ms/1000:.0f}s/block)")
245
+
246
+ # Registration block (approximate)
247
+ registration_block = timeline.get("registration_block_approx")
248
+ if registration_block is not None:
249
+ timeline_lines.append(f"🔗 Registration Block (approx): {registration_block:,}")
250
+
251
+ if requirements.get("can_activate", False):
252
+ # Success case - use HTCLI panel with success styling
253
+ content_lines = [f"✅ Subnet {subnet_id} can be activated!"]
254
+ content_lines.append("") # Empty line
255
+
256
+ # Add timeline section (most important for success case)
257
+ if timeline_lines:
258
+ content_lines.append("⏰ Timeline:")
259
+ content_lines.extend(f" {line}" for line in timeline_lines)
260
+ content_lines.append("") # Empty line
261
+
262
+ # Add epoch information
263
+ if epoch_info_lines:
264
+ content_lines.append("📊 Epoch Information:")
265
+ content_lines.extend(f" • {line}" for line in epoch_info_lines)
266
+ content_lines.append("") # Empty line
267
+
268
+ content_lines.extend(
269
+ f"✅ {req}" for req in requirements.get("requirements_met", [])
270
+ )
271
+
272
+ panel = HTCLIPanel(
273
+ "\n".join(content_lines),
274
+ title="✅ Activation Check Passed",
275
+ border_style="htcli.success",
276
+ highlight=True,
277
+ )
278
+ else:
279
+ # Failure case - show both met and failed requirements
280
+ content_lines = [f"❌ Subnet {subnet_id} cannot be activated yet."]
281
+ content_lines.append("") # Empty line
282
+
283
+ # Add timeline section (critical for understanding deadlines)
284
+ if timeline_lines:
285
+ content_lines.append("⏰ Timeline:")
286
+ content_lines.extend(f" {line}" for line in timeline_lines)
287
+ content_lines.append("") # Empty line
288
+
289
+ # Add epoch information
290
+ if epoch_info_lines:
291
+ content_lines.append("📊 Epoch Information:")
292
+ content_lines.extend(f" • {line}" for line in epoch_info_lines)
293
+ content_lines.append("") # Empty line
294
+
295
+ met_reqs = requirements.get("requirements_met", [])
296
+ failed_reqs = requirements.get("requirements_failed", [])
297
+
298
+ if met_reqs:
299
+ content_lines.append("✅ Requirements Met:")
300
+ content_lines.extend(f" ✅ {req}" for req in met_reqs)
301
+ content_lines.append("") # Empty line
302
+
303
+ if failed_reqs:
304
+ content_lines.append("❌ Requirements Failed:")
305
+ content_lines.extend(f" ❌ {req}" for req in failed_reqs)
306
+
307
+ panel = HTCLIPanel(
308
+ "\n".join(content_lines),
309
+ title="❌ Activation Check Failed",
310
+ border_style="htcli.error",
311
+ highlight=True,
312
+ )
313
+
314
+ panel.render()
315
+
316
+
317
+ def display_generic_success(response: dict[str, Any]):
318
+ """Displays a generic success message with a transaction hash."""
319
+ message = response.get("message", "Operation completed successfully.")
320
+ # Handle both 'transaction_hash' and 'extrinsic_hash' keys
321
+ tx_hash = response.get("transaction_hash") or response.get("extrinsic_hash")
322
+ block_number = response.get("block_number")
323
+ block_hash = response.get("block_hash")
324
+
325
+ # Debug: Log what we received
326
+ from ...utils.logging import get_logger
327
+ logger = get_logger(__name__)
328
+ logger.debug(f"display_generic_success received: message={message}, tx_hash={tx_hash}, block_number={block_number}, response_keys={list(response.keys())}")
329
+
330
+ if tx_hash:
331
+ # Use HTCLI panel for transaction success with hash
332
+ content = f"{message}\n\n[htcli.subtitle]Transaction Hash:[/] [htcli.hash]{tx_hash}[/]"
333
+ # Only show block number if it's not None
334
+ if block_number is not None:
335
+ content += f"\n[htcli.subtitle]Block Number:[/] [htcli.value]{block_number}[/]"
336
+ if block_hash:
337
+ content += f"\n[htcli.subtitle]Block Hash:[/] [htcli.hash]{block_hash}[/]"
338
+ panel = HTCLIPanel(
339
+ content,
340
+ title="✅ Operation Successful",
341
+ border_style="htcli.success",
342
+ highlight=True,
343
+ )
344
+ panel.render()
345
+ else:
346
+ # If no transaction hash, still show the message but warn about missing hash
347
+ content = f"{message}"
348
+ if response.get("success") is True:
349
+ content += "\n\n[htcli.warning]⚠️ Note: Transaction hash not available in response[/]"
350
+ content += f"\n[htcli.info]Response keys: {', '.join(response.keys())}[/]"
351
+ panel = HTCLIPanel(
352
+ content,
353
+ title="✅ Operation Successful",
354
+ border_style="htcli.success",
355
+ highlight=True,
356
+ )
357
+ panel.render()
358
+
359
+
360
+ def display_subnet_updates_summary(
361
+ subnet_id: int,
362
+ updates: list[tuple[str, dict[str, Any]]],
363
+ client: Optional[Any] = None,
364
+ ):
365
+ """
366
+ Display a consolidated summary of multiple subnet updates in a single panel.
367
+
368
+ Args:
369
+ subnet_id: The subnet ID that was updated
370
+ updates: List of tuples (field_name, result_dict) for each update
371
+ client: Optional client instance to fetch block numbers from block hashes
372
+ """
373
+ if not updates:
374
+ return
375
+
376
+ def format_field_name(field: str) -> str:
377
+ """Convert snake_case field name to Title Case display name."""
378
+ return field.replace("_", " ").title()
379
+
380
+ def get_transaction_hash(result: Any) -> Optional[str]:
381
+ """Extract transaction hash from result dict or object."""
382
+ if isinstance(result, dict):
383
+ return result.get("transaction_hash") or result.get("extrinsic_hash")
384
+ return (
385
+ getattr(result, "transaction_hash", None)
386
+ or getattr(result, "extrinsic_hash", None)
387
+ )
388
+
389
+ def get_block_hash(result: Any) -> Optional[str]:
390
+ """Extract block hash from result dict or object."""
391
+ if isinstance(result, dict):
392
+ return result.get("block_hash")
393
+ return getattr(result, "block_hash", None)
394
+
395
+ def get_block_number(result: Any) -> Optional[int]:
396
+ """Extract block number from result dict or object."""
397
+ if isinstance(result, dict):
398
+ return result.get("block_number")
399
+ return getattr(result, "block_number", None)
400
+
401
+ # Build content lines
402
+ content_lines = [f"[htcli.success]✅ Subnet {subnet_id} updated successfully![/htcli.success]\n"]
403
+
404
+ # Process each update
405
+ for field, result in updates:
406
+ field_display = format_field_name(field)
407
+ tx_hash = get_transaction_hash(result)
408
+ block_hash = get_block_hash(result)
409
+ block_number = get_block_number(result)
410
+
411
+ # Try to fetch block number from block hash if missing
412
+ if not block_number and block_hash and client and client.rpc and client.rpc.chain:
413
+ try:
414
+ block_number = client.rpc.chain.get_block_number(block_hash=block_hash)
415
+ except Exception:
416
+ pass
417
+
418
+ content_lines.append(f"[htcli.value]{field_display}:[/]")
419
+ if tx_hash:
420
+ content_lines.append(f" [htcli.subtitle]Transaction Hash:[/] [htcli.hash]{tx_hash}[/]")
421
+ if block_number:
422
+ content_lines.append(f" [htcli.subtitle]Block Number:[/] [htcli.value]{block_number}[/]")
423
+ if block_hash:
424
+ content_lines.append(f" [htcli.subtitle]Block Hash:[/] [htcli.hash]{block_hash}[/]")
425
+ content_lines.append("") # Empty line between updates
426
+
427
+ # Remove trailing empty line
428
+ if content_lines and content_lines[-1] == "":
429
+ content_lines.pop()
430
+
431
+ content = "\n".join(content_lines)
432
+
433
+ panel = HTCLIPanel(
434
+ content,
435
+ title="✅ Subnet Updates Complete",
436
+ border_style="htcli.success",
437
+ highlight=True,
438
+ )
439
+ panel.render(console.console)
440
+
441
+
442
+ def display_subnet_list_with_coldkey(data: Any, coldkey: str):
443
+ """Display subnets filtered by coldkey."""
444
+ if not data:
445
+ console.print(info(f"No subnets found for coldkey: {coldkey}"))
446
+ return
447
+
448
+ from ...models.responses import SubnetNodeInfo
449
+ from ...ui.components import create_subnet_table
450
+
451
+ # Handle different data types
452
+ if isinstance(data, list):
453
+ if data and isinstance(data[0], dict):
454
+ # Convert dict to SubnetNodeInfo models if needed
455
+ try:
456
+ subnet_nodes = [SubnetNodeInfo(**item) for item in data]
457
+ # Create and display table
458
+ table = create_subnet_table(subnet_nodes)
459
+ console.print(table)
460
+ except Exception:
461
+ # If conversion fails, display raw data
462
+ console.print(
463
+ info(f"Coldkey {coldkey} subnet nodes info (raw): {data}")
464
+ )
465
+ else:
466
+ # Handle case where data is not a list of dicts (e.g., [0])
467
+ console.print(
468
+ info(f"Coldkey {coldkey} has {len(data)} subnet nodes: {data}")
469
+ )
470
+ else:
471
+ console.print(info(f"Coldkey {coldkey} subnet nodes info: {data}"))
472
+
473
+
474
+ def display_subnet_info_rpc(subnet_info):
475
+ """Display subnet information from RPC response with bright, bold colors in a comprehensive table format."""
476
+ if not subnet_info:
477
+ console.print(info("No subnet information available"))
478
+ return
479
+
480
+ from ...ui.components import create_subnet_details_tree
481
+
482
+ # Create a detailed table view of subnet information (returns Table, not Tree)
483
+ table = create_subnet_details_tree(subnet_info, [])
484
+
485
+ # If it's a Table, print it directly; if it's a Tree, render it
486
+ if hasattr(table, 'add_row'):
487
+ console.print("\n")
488
+ console.print(table)
489
+ console.print("\n")
490
+ else:
491
+ table.render()
492
+
493
+
494
+ def display_subnet_nodes_rpc(nodes_data, subnet_id: Optional[int] = None):
495
+ """Display subnet nodes from RPC response."""
496
+ if not nodes_data:
497
+ if subnet_id is not None:
498
+ console.print(info(f"No nodes found for subnet {subnet_id}"))
499
+ else:
500
+ console.print(info("No nodes found"))
501
+ return
502
+
503
+ from ...ui.components import HTCLITable
504
+
505
+ # Sort nodes by node ID in ascending order
506
+ nodes_data = sorted(nodes_data, key=lambda x: x.get("id", 0))
507
+
508
+ # Create table for subnet nodes with footer enabled
509
+ table = HTCLITable(title="Nodes", show_lines=False, show_footer=True)
510
+ table.add_column("Node ID", justify="center", style="htcli.value")
511
+ table.add_column("Coldkey", style="htcli.address")
512
+ table.add_column("Hotkey", style="htcli.address")
513
+ table.add_column("Status", justify="center")
514
+ table.add_column("Own Stake", justify="right", style="htcli.amount")
515
+ table.add_column("Delegate Stake", justify="right", style="htcli.amount")
516
+ table.add_column("Total Stake", justify="right", style="bold green")
517
+
518
+ # Calculate totals for footer
519
+ total_nodes = len(nodes_data)
520
+ total_own_stake = sum(node.get('stake_balance', 0) / 1e18 for node in nodes_data)
521
+ total_delegate_stake = sum(node.get('node_delegate_stake_balance', 0) / 1e18 for node in nodes_data)
522
+ total_stake_all = total_own_stake + total_delegate_stake
523
+
524
+ for node in nodes_data:
525
+ own_stake = node.get('stake_balance', 0) / 1e18
526
+ delegate_stake = node.get('node_delegate_stake_balance', 0) / 1e18
527
+ total_stake = own_stake + delegate_stake
528
+
529
+ # Format addresses with checksum using centralized utility
530
+ from ...utils.wallet.crypto import format_address_display
531
+
532
+ table.add_row(
533
+ str(node.get("id", "N/A")),
534
+ format_address_display(node.get("coldkey", "Unknown")),
535
+ format_address_display(node.get("hotkey", "Unknown")),
536
+ node.get("status", "Unknown"),
537
+ f"{own_stake:,.2f} TENSOR",
538
+ f"{delegate_stake:,.2f} TENSOR",
539
+ f"{total_stake:,.2f} TENSOR",
540
+ )
541
+
542
+ # Set footer with statistics (similar to subnet list table)
543
+ footer_values = [
544
+ f"[bold]{total_nodes}[/bold]", # Total nodes count
545
+ "", # Coldkey column
546
+ "", # Hotkey column
547
+ "", # Status column
548
+ f"[bold]{total_own_stake:,.2f} TENSOR[/bold]", # Total own stake
549
+ f"[bold]{total_delegate_stake:,.2f} TENSOR[/bold]", # Total delegate stake
550
+ f"[bold]{total_stake_all:,.2f} TENSOR[/bold]", # Total stake
551
+ ]
552
+ table.set_column_footers(footer_values)
553
+
554
+ table.render()
555
+
556
+
557
+ def display_bootnodes_rpc(bootnodes_data, subnet_id: int):
558
+ """Display bootnodes from RPC response."""
559
+ if not bootnodes_data:
560
+ console.print(info(f"No bootnodes found for subnet {subnet_id}"))
561
+ return
562
+
563
+ from ...ui.components import HTCLIPanel
564
+
565
+ # Display bootnodes information
566
+ bootnodes_info = f"""
567
+ [htcli.accent]Subnet {subnet_id} Bootnodes[/htcli.accent]
568
+
569
+ [htcli.value]Official Bootnodes:[/htcli.value] {len(bootnodes_data.bootnodes)}
570
+ [htcli.value]Node Bootnodes:[/htcli.value] {len(bootnodes_data.node_bootnodes)}
571
+
572
+ [htcli.accent]Official Bootnodes:[/htcli.accent]
573
+ {chr(10).join(f"• {bootnode}" for bootnode in bootnodes_data.bootnodes) if bootnodes_data.bootnodes else "None"}
574
+
575
+ [htcli.accent]Node Bootnodes:[/htcli.accent]
576
+ {chr(10).join(f"• {bootnode}" for bootnode in bootnodes_data.node_bootnodes) if bootnodes_data.node_bootnodes else "None"}
577
+ """
578
+
579
+ panel = HTCLIPanel(
580
+ bootnodes_info,
581
+ title="🔗 Subnet Bootnodes",
582
+ border_style="htcli.info",
583
+ highlight=True,
584
+ )
585
+ panel.render()
586
+
587
+
588
+ def display_subnet_list_live(data: Any, coldkey: Optional[str] = None):
589
+ """Display subnets with live updates using Rich Live component."""
590
+ import time
591
+
592
+ from rich.layout import Layout
593
+ from rich.live import Live
594
+ from rich.panel import Panel
595
+ from rich.table import Table
596
+
597
+ # Create layout
598
+ layout = Layout()
599
+ layout.split_column(Layout(name="header"), Layout(name="content"))
600
+
601
+ # Header with coldkey info if provided
602
+ if coldkey:
603
+ header_content = f"[bold blue]🔍 Live Subnet Monitor[/bold blue]\n[yellow]Coldkey:[/yellow] {coldkey}"
604
+ else:
605
+ header_content = "[bold blue]🔍 Live Subnet Monitor[/bold blue]\n[yellow]All Subnets[/yellow]"
606
+
607
+ layout["header"].update(Panel(header_content, border_style="blue"))
608
+
609
+ # Create table for subnet data
610
+ table = Table(title="Subnets", show_header=True, header_style="bold magenta")
611
+ table.add_column("ID", style="cyan", no_wrap=True)
612
+ table.add_column("Name", style="green")
613
+ table.add_column("Owner", style="yellow")
614
+ table.add_column("Status", style="red")
615
+ table.add_column("Nodes", style="blue")
616
+
617
+ # Add sample data (you can replace this with actual data processing)
618
+ if isinstance(data, list):
619
+ for i, subnet in enumerate(data[:10]): # Limit to 10 for demo
620
+ if isinstance(subnet, dict):
621
+ table.add_row(
622
+ str(i),
623
+ subnet.get("name", "Unknown"),
624
+ subnet.get("owner", "Unknown")[:20] + "..."
625
+ if len(str(subnet.get("owner", ""))) > 20
626
+ else str(subnet.get("owner", "Unknown")),
627
+ "Active" if subnet.get("total_active_nodes", 0) > 0 else "Inactive",
628
+ str(subnet.get("total_nodes", 0)),
629
+ )
630
+ else:
631
+ # Handle non-dict items (like integers)
632
+ table.add_row(str(i), str(subnet), "N/A", "Unknown", "N/A")
633
+ else:
634
+ table.add_row("N/A", "No data", "N/A", "N/A", "N/A")
635
+
636
+ layout["content"].update(table)
637
+
638
+ # Use Live display
639
+ with Live(layout, refresh_per_second=2, screen=True) as live:
640
+ try:
641
+ # Simulate live updates
642
+ for i in range(10):
643
+ time.sleep(1)
644
+ # Update table with new data (you can replace this with real data fetching)
645
+ table.add_row(
646
+ str(i + 10),
647
+ f"Subnet {i + 10}",
648
+ "Owner...",
649
+ "Active" if i % 2 == 0 else "Inactive",
650
+ str(i + 5),
651
+ )
652
+ live.update(layout)
653
+ except KeyboardInterrupt:
654
+ pass
655
+
656
+
657
+ def display_registration_cost_comparison(cost_tensor: float, wallet_data: list[dict]):
658
+ """Display subnet registration cost compared with all wallet balances."""
659
+ import sys
660
+
661
+ from ...ui.components import HTCLIPanel, HTCLITable
662
+
663
+ # Display registration cost prominently
664
+ cost_panel_content = f"""[htcli.accent]Current Subnet Registration Cost[/htcli.accent]
665
+
666
+ [htcli.value]Cost:[/htcli.value] {cost_tensor:,.2f} TENSOR
667
+ [htcli.info]({cost_tensor * 1e18:,.0f} wei)[/htcli.info]
668
+
669
+ [htcli.info]💡 This cost is dynamic and changes based on network activity.[/htcli.info]
670
+ """
671
+
672
+ cost_panel = HTCLIPanel(
673
+ cost_panel_content,
674
+ title="💰 Registration Cost",
675
+ border_style="htcli.accent",
676
+ highlight=True,
677
+ )
678
+
679
+ console.print()
680
+ cost_panel.render()
681
+ console.print()
682
+
683
+ # Create comparison table
684
+ table = HTCLITable(title="📊 Wallet Balance Comparison")
685
+ table.add_column("Wallet Name", style="htcli.value", justify="left", width=20)
686
+ table.add_column("Address", style="htcli.info", justify="left", width=25)
687
+ table.add_column("Balance (TENSOR)", style="htcli.value", justify="right", width=20)
688
+ table.add_column("Status", style="htcli.value", justify="center", width=15)
689
+ table.add_column("Shortfall", style="htcli.error", justify="right", width=20)
690
+
691
+ sufficient_count = 0
692
+ insufficient_count = 0
693
+
694
+ for wallet in wallet_data:
695
+ name = wallet["name"]
696
+ address = wallet["address"]
697
+ # Truncate address for display
698
+ truncated_addr = (
699
+ f"{address[:8]}...{address[-8:]}" if len(address) > 20 else address
700
+ )
701
+ balance = wallet["balance"]
702
+ sufficient = wallet["sufficient"]
703
+ shortfall = wallet.get("shortfall", 0)
704
+ error = wallet.get("error")
705
+
706
+ if error:
707
+ status = "[htcli.error]❌ Error[/htcli.error]"
708
+ shortfall_str = error
709
+ elif sufficient:
710
+ status = "[htcli.success]✅ Sufficient[/htcli.success]"
711
+ shortfall_str = "-"
712
+ sufficient_count += 1
713
+ else:
714
+ status = "[htcli.error]❌ Insufficient[/htcli.error]"
715
+ shortfall_str = f"-{shortfall:,.2f}"
716
+ insufficient_count += 1
717
+
718
+ table.add_row(
719
+ name,
720
+ truncated_addr,
721
+ f"{balance:,.4f}",
722
+ status,
723
+ shortfall_str,
724
+ )
725
+
726
+ table.render()
727
+ console.print()
728
+
729
+ # Summary panel
730
+ summary_content = f"""[htcli.accent]Summary[/htcli.accent]
731
+
732
+ [htcli.value]Total Wallets:[/htcli.value] {len(wallet_data)}
733
+ [htcli.success]✅ Sufficient Balance:[/htcli.success] {sufficient_count}
734
+ [htcli.error]❌ Insufficient Balance:[/htcli.error] {insufficient_count}
735
+
736
+ """
737
+
738
+ if sufficient_count > 0:
739
+ summary_content += f"[htcli.success]✅ You can register a subnet with {sufficient_count} wallet(s)![/htcli.success]\n"
740
+ summary_content += "[htcli.info]Use: htcli subnet register[/htcli.info]"
741
+ else:
742
+ summary_content += "[htcli.error]❌ None of your wallets have sufficient balance.[/htcli.error]\n"
743
+ summary_content += f"[htcli.info]💡 Transfer at least {cost_tensor:,.2f} TENSOR to a wallet[/htcli.info]\n"
744
+ summary_content += "[htcli.info]Use: htcli wallet transfer[/htcli.info]"
745
+
746
+ summary_panel = HTCLIPanel(
747
+ summary_content,
748
+ title="📋 Summary",
749
+ border_style="htcli.info",
750
+ highlight=True,
751
+ )
752
+
753
+ summary_panel.render()
754
+ console.print()
755
+
756
+ # Ensure output is flushed
757
+ sys.stdout.flush()
758
+ sys.stderr.flush()
759
+
760
+
761
+ def display_emergency_validator_set_result(
762
+ result: dict[str, Any], subnet_id: int, node_ids: list[int], action: str = "set"
763
+ ):
764
+ """Display result of emergency validator set operation."""
765
+ if result.get("success"):
766
+ action_label = "set" if action == "set" else "cleared"
767
+ if node_ids:
768
+ console.print(
769
+ success(
770
+ f"✅ Emergency validator set {action_label} successfully for subnet {subnet_id}!"
771
+ )
772
+ )
773
+ console.print(info(f"Node IDs: {', '.join(map(str, node_ids))}"))
774
+ else:
775
+ console.print(
776
+ success(
777
+ f"✅ Emergency validator set cleared successfully for subnet {subnet_id}!"
778
+ )
779
+ )
780
+ display_generic_success(result)
781
+ else:
782
+ error_msg = result.get("error", "Operation failed")
783
+ console.print(error(f"❌ Failed to {action} emergency validator set: {error_msg}"))
784
+
785
+
786
+ def display_bootnode_access_result(
787
+ result: dict[str, Any], subnet_id: int, account: str, action: str = "add"
788
+ ):
789
+ """Display result of bootnode access operation."""
790
+ if result.get("success"):
791
+ action_label = "granted" if action == "add" else "revoked"
792
+ console.print(
793
+ success(
794
+ f"✅ Bootnode access {action_label} successfully for subnet {subnet_id}!"
795
+ )
796
+ )
797
+ console.print(info(f"Account: {account}"))
798
+ display_generic_success(result)
799
+ else:
800
+ error_msg = result.get("error", "Operation failed")
801
+ console.print(error(f"❌ Failed to {action} bootnode access: {error_msg}"))