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,721 @@
1
+ """
2
+ HTCLI tree components.
3
+ Provides enhanced trees for hierarchical data display.
4
+ """
5
+
6
+ from typing import Any
7
+
8
+ from rich.console import Console
9
+ from rich.text import Text
10
+ from rich.tree import Tree
11
+
12
+ from ..colors import HTCLIColors
13
+ from ..display import get_console
14
+
15
+
16
+ class HTCLITree:
17
+ """Enhanced tree for displaying hierarchical HTCLI data."""
18
+
19
+ def __init__(
20
+ self,
21
+ label: str,
22
+ style: str = "htcli.tree",
23
+ guide_style: str = "htcli.tree.branch",
24
+ expanded: bool = True,
25
+ hide_root: bool = False,
26
+ ):
27
+ # Create the root label with proper styling
28
+ if isinstance(label, str):
29
+ root_label = Text(label, style="htcli.title")
30
+ else:
31
+ root_label = label
32
+
33
+ self.tree = Tree(
34
+ root_label,
35
+ style=style,
36
+ guide_style=guide_style,
37
+ expanded=expanded,
38
+ hide_root=hide_root,
39
+ )
40
+ self.style = style
41
+ self.guide_style = guide_style
42
+
43
+ def add_branch(
44
+ self,
45
+ label: str,
46
+ style: str = None,
47
+ expanded: bool = True,
48
+ data: dict[str, Any] = None,
49
+ parent=None,
50
+ ):
51
+ """Add a branch to the tree with optional data formatting."""
52
+ # Use parent branch or root tree
53
+ target = parent or self.tree
54
+
55
+ # Create formatted label
56
+ if isinstance(label, str):
57
+ branch_label = Text(label, style=style or "htcli.subtitle")
58
+ else:
59
+ branch_label = label
60
+
61
+ branch = target.add(branch_label, expanded=expanded)
62
+
63
+ # Add data as sub-items if provided
64
+ if data:
65
+ self._add_data_to_branch(branch, data)
66
+
67
+ return branch
68
+
69
+ def add_leaf(
70
+ self,
71
+ label: str,
72
+ value: Any = None,
73
+ style: str = None,
74
+ parent=None,
75
+ ):
76
+ """Add a leaf node (no children) to the tree."""
77
+ target = parent or self.tree
78
+
79
+ if value is not None:
80
+ # Format the key-value pair
81
+ formatted_value = self._format_value(str(value))
82
+ leaf_text = f"[{style or 'htcli.subtitle'}]{label}:[/] {formatted_value}"
83
+ else:
84
+ leaf_text = Text(label, style=style or "htcli.value")
85
+
86
+ return target.add(leaf_text)
87
+
88
+ def _add_data_to_branch(self, branch, data: dict[str, Any]):
89
+ """Add data dictionary as child nodes to a branch."""
90
+ for key, value in data.items():
91
+ if key.lower() == "name":
92
+ continue # Skip name as it's usually in the branch label
93
+
94
+ formatted_value = self._format_value(str(value), key)
95
+ key_display = key.replace("_", " ").title()
96
+ branch.add(f"[htcli.subtitle]{key_display}:[/] {formatted_value}")
97
+
98
+ def _format_value(self, value: str, key: str = None) -> str:
99
+ """Format a value based on its type/context."""
100
+ if key:
101
+ key_lower = key.lower()
102
+ if key_lower in ["address", "account", "validator", "owner"]:
103
+ return f"[htcli.address]{value}[/]"
104
+ elif key_lower in ["amount", "balance", "stake", "total_stake"]:
105
+ return f"[htcli.amount]{value}[/]"
106
+ elif key_lower in ["status", "state"]:
107
+ color = HTCLIColors.get_status_color(value)
108
+ return f"[{color}]{value}[/]"
109
+ elif key_lower in ["hash", "tx_hash", "block_hash", "peer_id"]:
110
+ return f"[htcli.hash]{value}[/]"
111
+ elif key_lower in ["id", "node_count", "nodes"]:
112
+ return f"[htcli.value]{value}[/]"
113
+
114
+ # Default formatting
115
+ return f"[htcli.value]{value}[/]"
116
+
117
+ def add_subnet_branch(self, subnet_data: dict[str, Any]):
118
+ """Add a subnet branch with standardized formatting."""
119
+ subnet_id = subnet_data.get("id", "Unknown")
120
+ subnet_name = subnet_data.get("name", "Unknown Subnet")
121
+
122
+ # Create main branch with emoji and name
123
+ branch_label = f"🌐 Subnet {subnet_id} ({subnet_name})"
124
+ branch = self.add_branch(branch_label, style="htcli.subnet", data=subnet_data)
125
+
126
+ return branch
127
+
128
+ def add_node_branch(self, node_data: dict[str, Any], parent=None):
129
+ """Add a node branch with standardized formatting."""
130
+ node_id = node_data.get("id", "Unknown")
131
+ peer_id = node_data.get("peer_id", "Unknown")
132
+
133
+ # Truncate peer_id for display
134
+ if peer_id != "Unknown" and len(peer_id) > 20:
135
+ display_peer = f"{peer_id[:12]}...{peer_id[-6:]}"
136
+ else:
137
+ display_peer = peer_id
138
+
139
+ # Create node branch with emoji and identifier
140
+ branch_label = f"🔗 Node {node_id} ({display_peer})"
141
+ branch = self.add_branch(
142
+ branch_label, style="htcli.node", data=node_data, parent=parent
143
+ )
144
+
145
+ return branch
146
+
147
+ def add_wallet_branch(self, wallet_data: dict[str, Any], parent=None):
148
+ """Add a wallet branch with standardized formatting."""
149
+ wallet_name = wallet_data.get("name", "Unknown Wallet")
150
+ wallet_type = wallet_data.get("type", "unknown")
151
+
152
+ # Create wallet branch with emoji and name
153
+ branch_label = f"👛 {wallet_name} ({wallet_type})"
154
+ branch = self.add_branch(
155
+ branch_label, style="htcli.value", data=wallet_data, parent=parent
156
+ )
157
+
158
+ return branch
159
+
160
+ def render(self, console: Console = None):
161
+ """Render the tree to console."""
162
+ console = console or get_console().console
163
+ console.print(self.tree)
164
+
165
+ def get_renderable(self):
166
+ """Get the tree as a renderable object."""
167
+ return self.tree
168
+
169
+
170
+ # Tree utility functions
171
+ def create_network_tree(
172
+ subnets: list[dict[str, Any]],
173
+ nodes: dict[int, list[dict[str, Any]]] = None,
174
+ title: str = "🌐 Hypertensor Network",
175
+ ) -> HTCLITree:
176
+ """Create a standardized network hierarchy tree."""
177
+ tree = HTCLITree(title, expanded=True)
178
+
179
+ nodes = nodes or {}
180
+
181
+ for subnet in subnets:
182
+ # Add subnet branch
183
+ subnet_branch = tree.add_subnet_branch(subnet)
184
+
185
+ # Add nodes under this subnet if available
186
+ subnet_id = subnet.get("id")
187
+ if subnet_id and subnet_id in nodes:
188
+ for node in nodes[subnet_id]:
189
+ tree.add_node_branch(node, parent=subnet_branch)
190
+
191
+ return tree
192
+
193
+
194
+ def create_wallet_tree(
195
+ wallets: list[dict[str, Any]],
196
+ transactions: dict[str, list[dict[str, Any]]] = None,
197
+ title: str = "👛 Wallet Overview",
198
+ ) -> HTCLITree:
199
+ """Create a standardized wallet tree with optional transaction history."""
200
+ tree = HTCLITree(title, expanded=True)
201
+
202
+ transactions = transactions or {}
203
+
204
+ for wallet in wallets:
205
+ # Add wallet branch
206
+ wallet_branch = tree.add_wallet_branch(wallet)
207
+
208
+ # Add recent transactions if available
209
+ wallet_name = wallet.get("name")
210
+ if wallet_name and wallet_name in transactions:
211
+ tx_branch = tree.add_branch(
212
+ "📋 Recent Transactions", style="htcli.subtitle", parent=wallet_branch
213
+ )
214
+
215
+ for tx in transactions[wallet_name][:5]: # Show last 5 transactions
216
+ tx_hash = tx.get("hash", "Unknown")
217
+ tx_amount = tx.get("amount", "0")
218
+ tx_status = tx.get("status", "unknown")
219
+
220
+ # Truncate hash for display
221
+ display_hash = (
222
+ f"{tx_hash[:8]}...{tx_hash[-8:]}" if len(tx_hash) > 16 else tx_hash
223
+ )
224
+
225
+ status_color = HTCLIColors.get_status_color(tx_status)
226
+ tx_label = f"[htcli.hash]{display_hash}[/] - [htcli.amount]{tx_amount}[/] - [{status_color}]{tx_status}[/]"
227
+ tree.add_leaf("", tx_label, parent=tx_branch)
228
+
229
+ return tree
230
+
231
+
232
+ def create_subnet_details_tree(
233
+ subnet_data: dict[str, Any], nodes: list[dict[str, Any]] = None
234
+ ):
235
+ """Create a detailed table view for a single subnet with bright, bold colors.
236
+
237
+ Returns a Rich Table for displaying subnet information in a comprehensive format.
238
+ """
239
+ from rich.table import Table
240
+
241
+ subnet_name = subnet_data.get("name", "Unknown Subnet")
242
+ subnet_id = subnet_data.get("id", "Unknown")
243
+
244
+ # Decode bytes fields for display
245
+ if isinstance(subnet_name, bytes):
246
+ subnet_name = subnet_name.decode("utf-8", errors="ignore")
247
+
248
+ # Helper function to format values
249
+ def format_field_value(key: str, value: Any) -> str:
250
+ """Format a field value for display."""
251
+ if value is None:
252
+ return "[dim]None[/dim]"
253
+
254
+ # Handle bytes
255
+ if isinstance(value, bytes):
256
+ try:
257
+ decoded = value.decode("utf-8")
258
+ return f"[cyan]{decoded}[/cyan]" if decoded else "[dim](empty)[/dim]"
259
+ except (UnicodeDecodeError, AttributeError):
260
+ return f"[dim]{value.hex()[:20]}...[/dim]"
261
+
262
+ # Handle enum/state
263
+ if hasattr(value, "value"):
264
+ value = value.value
265
+ elif hasattr(value, "name"):
266
+ value = value.name
267
+
268
+ value_str = str(value)
269
+
270
+ # Format based on field type
271
+ key_lower = key.lower()
272
+
273
+ # Handle percentage fields first (they contain "stake" but should be formatted as percentages)
274
+ if (
275
+ key_lower == "delegate_stake_percentage"
276
+ or "delegate_stake_percentage" in key_lower
277
+ ):
278
+ # Percentage fields are stored as wei where 1e18 = 100%
279
+ # Show the value even if it's u32::MAX or very large
280
+ if isinstance(value, (int, float)) and value >= 0:
281
+ percentage_value = (value / 1e18) * 100
282
+ # Show percentage if reasonable, otherwise show raw value
283
+ if 0 <= percentage_value <= 100:
284
+ return f"[#53B5A0]{percentage_value:.2f}%[/#53B5A0]"
285
+ else:
286
+ # Show raw value for u32::MAX or out-of-range values
287
+ return f"[yellow]{value}[/yellow]"
288
+ return f"[yellow]{value}[/yellow]" if value is not None else "[dim]Not set[/dim]"
289
+ elif "stake" in key_lower or "balance" in key_lower:
290
+ # Format as TENSOR (convert from wei) - always show value
291
+ if isinstance(value, (int, float)) and value >= 0:
292
+ tensor_value = value / 1e18
293
+ return f"[#53B5A0]{tensor_value:,.2f} TENSOR[/#53B5A0]"
294
+ elif isinstance(value, (int, float)):
295
+ # Show raw value if negative (shouldn't happen but show it anyway)
296
+ return f"[yellow]{value}[/yellow]"
297
+ return f"[yellow]{value}[/yellow]" if value is not None else "[dim]Not set[/dim]"
298
+ elif "address" in key_lower or "owner" in key_lower:
299
+ if value is None or value_str == "None" or not value_str:
300
+ return "[dim]Not set[/dim]"
301
+ # Always show full address, no truncation
302
+ return f"[steel_blue3]{value_str}[/steel_blue3]"
303
+ elif "state" in key_lower:
304
+ state_str = str(value).lower()
305
+ if "active" in state_str:
306
+ return "[bold green]Active[/bold green]"
307
+ elif "registered" in state_str:
308
+ return "[bold yellow]Registered[/bold yellow]"
309
+ elif "paused" in state_str:
310
+ return "[bold red]Paused[/bold red]"
311
+ elif key_lower == "start_epoch":
312
+ try:
313
+ numeric_value = int(value)
314
+ except Exception:
315
+ numeric_value = None
316
+ # u32::MAX (4294967295) indicates the subnet has not been activated yet
317
+ if numeric_value is not None and numeric_value == 4294967295:
318
+ return "[bold yellow]Not Active[/bold yellow] [dim](4294967295)[/dim]"
319
+ return f"[cyan]{value}[/cyan]"
320
+ elif key_lower == "registration_epoch":
321
+ try:
322
+ numeric_value = int(value)
323
+ except Exception:
324
+ numeric_value = None
325
+ # u32::MAX indicates the subnet has not been activated yet
326
+ if numeric_value is not None and numeric_value >= 0xFFFFFFFF:
327
+ return "[bold yellow]Subnet is not active yet[/bold yellow]"
328
+ return f"[cyan]{value}[/cyan]"
329
+ elif "epoch" in key_lower:
330
+ # Always show epoch value, even if it's u32::MAX
331
+ return f"[cyan]{value}[/cyan]"
332
+ elif isinstance(value, dict) and key_lower == "initial_coldkeys":
333
+ # Format initial_coldkeys as a nice list with addresses and max registrations
334
+ if not value or len(value) == 0:
335
+ return "[dim](empty)[/dim]"
336
+ items = []
337
+ for addr, max_regs in sorted(value.items()):
338
+ # Format address nicely and show max registrations
339
+ items.append(f" • [steel_blue3]{addr}[/steel_blue3]: [cyan]{max_regs} nodes[/cyan]")
340
+ # Show all items, one per line for readability
341
+ if len(items) <= 5:
342
+ return f"[cyan]{chr(10).join(items)}[/cyan]"
343
+ else:
344
+ # Show first 5, then count
345
+ return f"[cyan]{chr(10).join(items[:5])}{chr(10)} ... ({len(items)} total)[/cyan]"
346
+ elif isinstance(value, set):
347
+ if len(value) == 0:
348
+ return "[dim](empty)[/dim]"
349
+ # Format set contents nicely
350
+ if key_lower == "key_types":
351
+ # KeyType enum values - show name if available, otherwise value
352
+ items = []
353
+ for item in value:
354
+ if hasattr(item, 'name'):
355
+ items.append(item.name)
356
+ elif hasattr(item, 'value'):
357
+ items.append(str(item.value))
358
+ else:
359
+ items.append(str(item))
360
+ return f"[cyan]{', '.join(sorted(items))}[/cyan]"
361
+ elif key_lower == "bootnode_access":
362
+ # Address strings - show full addresses
363
+ items = list(value)
364
+ if len(items) > 3:
365
+ # Show first 3 full addresses, then count
366
+ return f"[cyan]{', '.join(list(items)[:3])}... ({len(items)} total)[/cyan]"
367
+ # Show all addresses in full
368
+ return f"[cyan]{', '.join(items)}[/cyan]"
369
+ elif key_lower == "bootnodes":
370
+ # Bytes - decode or show hex
371
+ items = []
372
+ for item in list(value)[:5]: # Limit to first 5
373
+ if isinstance(item, bytes):
374
+ try:
375
+ decoded = item.decode("utf-8")
376
+ items.append(decoded)
377
+ except (UnicodeDecodeError, AttributeError):
378
+ items.append(f"0x{item.hex()[:16]}...")
379
+ else:
380
+ items.append(str(item))
381
+ if len(value) > 5:
382
+ return f"[cyan]{', '.join(items)}... ({len(value)} total)[/cyan]"
383
+ return f"[cyan]{', '.join(items)}[/cyan]"
384
+ else:
385
+ # Generic set formatting
386
+ items = [str(item) for item in list(value)[:10]]
387
+ if len(value) > 10:
388
+ return f"[cyan]{', '.join(items)}... ({len(value)} total)[/cyan]"
389
+ return f"[cyan]{', '.join(items)}[/cyan]"
390
+ elif isinstance(value, (int, float)) and value > 1000000000000:
391
+ # Very large numbers - might be incorrectly decoded
392
+ # Format in scientific notation for readability
393
+ return f"[yellow]{value:.2e}[/yellow]"
394
+
395
+ return f"[white]{value_str}[/white]"
396
+
397
+ # Create table for subnet details with bright, bold colors
398
+ table = Table(
399
+ title=f"[bold white]Subnet [bold cyan]{subnet_id}[/bold cyan]: [cyan]{subnet_name}[/cyan]\n",
400
+ show_header=True,
401
+ header_style="bold white",
402
+ border_style="bright_black",
403
+ style="bold",
404
+ show_edge=False,
405
+ show_lines=False,
406
+ pad_edge=True,
407
+ title_justify="left",
408
+ )
409
+
410
+ table.add_column(
411
+ "[bold white]Field[/bold white]", style="bold tan", no_wrap=True, width=40
412
+ )
413
+ table.add_column(
414
+ "[bold white]Value[/bold white]", style="white", overflow="fold", width=60
415
+ )
416
+
417
+ # Define field display order and grouping
418
+ field_groups = {
419
+ "Basic Information": [
420
+ "id",
421
+ "friendly_id",
422
+ "name",
423
+ "repo",
424
+ "description",
425
+ "misc",
426
+ "state",
427
+ "owner",
428
+ "pending_owner",
429
+ ],
430
+ "Node Configuration": [
431
+ "total_nodes",
432
+ "total_active_nodes",
433
+ "total_electable_nodes",
434
+ "max_registered_nodes",
435
+ "churn_limit",
436
+ "churn_limit_multiplier",
437
+ "node_registrations_this_epoch",
438
+ ],
439
+ "Staking Configuration": [
440
+ "min_stake",
441
+ "max_stake",
442
+ "current_min_delegate_stake",
443
+ "delegate_stake_percentage",
444
+ "last_delegate_stake_rewards_update",
445
+ "total_subnet_stake",
446
+ "total_subnet_delegate_stake_shares",
447
+ "total_subnet_delegate_stake_balance",
448
+ ],
449
+ "Epoch Settings": [
450
+ "start_epoch",
451
+ "registration_epoch",
452
+ "prev_pause_epoch",
453
+ "queue_immunity_epochs",
454
+ "target_node_registrations_per_epoch",
455
+ "subnet_node_queue_epochs",
456
+ "idle_classification_epochs",
457
+ "included_classification_epochs",
458
+ ],
459
+ "Reputation Settings": [
460
+ "reputation",
461
+ "min_subnet_node_reputation",
462
+ "subnet_node_min_weight_decrease_reputation_threshold",
463
+ "absent_decrease_reputation_factor",
464
+ "included_increase_reputation_factor",
465
+ "below_min_weight_decrease_reputation_factor",
466
+ "non_attestor_decrease_reputation_factor",
467
+ "non_consensus_attestor_decrease_reputation_factor",
468
+ "validator_absent_subnet_node_reputation_factor",
469
+ "validator_non_consensus_subnet_node_reputation_factor",
470
+ ],
471
+ "Other": [
472
+ "node_burn_rate_alpha",
473
+ "current_node_burn_rate",
474
+ "initial_coldkeys",
475
+ "initial_coldkey_data",
476
+ "key_types",
477
+ "slot_index",
478
+ "slot_assignment",
479
+ "bootnode_access",
480
+ "bootnodes",
481
+ ],
482
+ }
483
+
484
+ # Add fields grouped by category - SHOW ALL FIELDS
485
+ displayed_fields = set()
486
+ for group_name, fields in field_groups.items():
487
+ # Special handling: always show owner and pending_owner even if None
488
+ # Also show sets even if empty (they'll display as "(empty)")
489
+ group_fields = []
490
+ for f in fields:
491
+ if f in subnet_data:
492
+ # Always include ALL fields - show everything
493
+ group_fields.append(f)
494
+ if group_fields:
495
+ # Add group header
496
+ table.add_row(f"[bold cyan]{group_name}[/bold cyan]", "", style="dim")
497
+
498
+ for field in group_fields:
499
+ value = subnet_data[field]
500
+ formatted_value = format_field_value(field, value)
501
+ field_display = field.replace("_", " ").title()
502
+ table.add_row(f" {field_display}", formatted_value)
503
+ displayed_fields.add(field)
504
+
505
+ # Add any remaining fields not in groups - SHOW ALL, even if None or 0
506
+ for key, value in subnet_data.items():
507
+ if key not in displayed_fields:
508
+ formatted_value = format_field_value(key, value)
509
+ key_display = key.replace("_", " ").title()
510
+ table.add_row(key_display, formatted_value)
511
+
512
+ return table
513
+
514
+
515
+ def create_transaction_tree(
516
+ transactions: list[dict[str, Any]], title: str = "📋 Transactions"
517
+ ) -> HTCLITree:
518
+ """Create a tree view for transaction history."""
519
+ tree = HTCLITree(title, expanded=True)
520
+
521
+ # Group transactions by status
522
+ status_groups = {}
523
+ for tx in transactions:
524
+ status = tx.get("status", "unknown")
525
+ if status not in status_groups:
526
+ status_groups[status] = []
527
+ status_groups[status].append(tx)
528
+
529
+ # Add each status group as a branch
530
+ for status, tx_list in status_groups.items():
531
+ status_color = HTCLIColors.get_status_color(status)
532
+ status_branch = tree.add_branch(
533
+ f"[{status_color}]{status.title()} ({len(tx_list)})[/]",
534
+ style="htcli.subtitle",
535
+ )
536
+
537
+ for tx in tx_list:
538
+ tx_hash = tx.get("hash", "Unknown")
539
+ tx_amount = tx.get("amount", "0")
540
+ tx_from = tx.get("from", "Unknown")
541
+ tx_to = tx.get("to", "Unknown")
542
+
543
+ # Create transaction details
544
+ tx_details = {
545
+ "hash": tx_hash,
546
+ "amount": f"{tx_amount} TENSOR",
547
+ "from": tx_from,
548
+ "to": tx_to,
549
+ "timestamp": tx.get("timestamp", "Unknown"),
550
+ }
551
+
552
+ # Truncate hash for branch label
553
+ display_hash = (
554
+ f"{tx_hash[:8]}...{tx_hash[-8:]}" if len(tx_hash) > 16 else tx_hash
555
+ )
556
+ tree.add_branch(
557
+ f"📄 {display_hash}",
558
+ style="htcli.hash",
559
+ data=tx_details,
560
+ parent=status_branch,
561
+ )
562
+
563
+ return tree
564
+
565
+
566
+ def create_wallet_hierarchy_tree(wallets: list[dict[str, Any]]) -> None:
567
+ """
568
+ Create a beautiful hierarchical tree display showing coldkeys and their associated hotkeys.
569
+ Enhanced version with Rich Tree, icons, encryption status, and better formatting.
570
+ """
571
+ from collections import defaultdict
572
+
573
+ from ..display import HTCLIConsole
574
+
575
+ console = HTCLIConsole()
576
+
577
+ # Separate coldkeys and hotkeys
578
+ coldkeys = [k for k in wallets if not k.get("is_hotkey", False)]
579
+ hotkeys = [k for k in wallets if k.get("is_hotkey", False)]
580
+
581
+ # If no wallets, return early (should be handled by display_wallet_list, but safety check)
582
+ if not coldkeys and not hotkeys:
583
+ return
584
+
585
+ # Create a set of coldkey addresses for quick lookup
586
+ coldkey_addresses = {coldkey.get("ss58_address") for coldkey in coldkeys}
587
+
588
+ # Group hotkeys by owner
589
+ hotkeys_by_owner = defaultdict(list)
590
+ orphaned_hotkeys = []
591
+
592
+ for hotkey in hotkeys:
593
+ owner_address = hotkey.get("owner_address")
594
+ owner_coldkey_name = hotkey.get("owner_coldkey_name")
595
+ # Check if owner exists (by address or by name)
596
+ owner_found = False
597
+
598
+ # First, try to find by address
599
+ if owner_address and owner_address in coldkey_addresses:
600
+ # Valid owner found by address - add to grouped hotkeys
601
+ hotkeys_by_owner[owner_address].append(hotkey)
602
+ owner_found = True
603
+ elif owner_coldkey_name:
604
+ # Try to find by coldkey name (even if owner_address doesn't match)
605
+ matching_coldkey = next(
606
+ (ck for ck in coldkeys if ck.get("name") == owner_coldkey_name),
607
+ None
608
+ )
609
+ if matching_coldkey:
610
+ # Found by name - add to grouped hotkeys
611
+ hotkeys_by_owner[matching_coldkey.get("ss58_address")].append(hotkey)
612
+ owner_found = True
613
+
614
+ # If owner_address exists but doesn't match any coldkey, it's orphaned
615
+ # (This handles the case where coldkey was deleted but hotkey file remains)
616
+ if not owner_found:
617
+ orphaned_hotkeys.append(hotkey)
618
+
619
+ # Create root tree
620
+ root = Tree("💼 Wallets", style="bold cyan", guide_style="dim cyan")
621
+
622
+ # Display coldkeys with their hotkeys
623
+ for coldkey in coldkeys:
624
+ coldkey_name = coldkey.get("name", "N/A")
625
+ coldkey_address = coldkey.get("ss58_address", "N/A")
626
+ is_encrypted = coldkey.get("is_encrypted", False)
627
+ key_type = coldkey.get("key_type", "ecdsa").upper()
628
+
629
+ # Format coldkey label with icon, name, address, and status
630
+ # Show lock icon only for encrypted wallets, no icon for unencrypted
631
+ coldkey_label = Text()
632
+ coldkey_label.append("Coldkey ", style="bold cyan")
633
+ coldkey_label.append(coldkey_name, style="bold green")
634
+ coldkey_label.append(f" {coldkey_address}", style="dim white")
635
+ coldkey_label.append(f" [{key_type}]", style="dim cyan")
636
+ # Only show lock icon for encrypted wallets
637
+ if is_encrypted:
638
+ coldkey_label.append(" 🔒", style="dim")
639
+
640
+ # Add coldkey branch
641
+ coldkey_branch = root.add(coldkey_label)
642
+
643
+ # Find and display hotkeys owned by this coldkey
644
+ owned_hotkeys = hotkeys_by_owner.get(coldkey_address, [])
645
+
646
+ if not owned_hotkeys:
647
+ # No hotkeys - show placeholder
648
+ coldkey_branch.add(Text("(no hotkeys)", style="dim italic"))
649
+
650
+ for hotkey in owned_hotkeys:
651
+ hotkey_name = hotkey.get("name", "N/A")
652
+ # Use EVM address if available (for ECDSA keys), otherwise fall back to ss58_address
653
+ hotkey_address = hotkey.get("evm_address") or hotkey.get("address") or hotkey.get("ss58_address", "N/A")
654
+ is_hotkey_encrypted = hotkey.get("is_encrypted", False)
655
+ hotkey_key_type = hotkey.get("key_type", "ecdsa").upper()
656
+
657
+ # Format hotkey label
658
+ # Show lock icon only for encrypted hotkeys, no icon for unencrypted
659
+ hotkey_label = Text()
660
+ hotkey_label.append("⚡ ", style="bold red")
661
+ hotkey_label.append("Hotkey ", style="bold yellow")
662
+ hotkey_label.append(hotkey_name, style="bold green")
663
+ hotkey_label.append(f" {hotkey_address}", style="dim white")
664
+ hotkey_label.append(f" [{hotkey_key_type}]", style="dim cyan")
665
+ # Only show lock icon for encrypted hotkeys
666
+ if is_hotkey_encrypted:
667
+ hotkey_label.append(" 🔒", style="dim")
668
+
669
+ coldkey_branch.add(hotkey_label)
670
+
671
+ # Display orphaned hotkeys (hotkeys without a valid coldkey owner)
672
+ if orphaned_hotkeys:
673
+ orphaned_branch = root.add(
674
+ Text("⚠️ Orphaned Hotkeys (no valid owner)", style="bold yellow")
675
+ )
676
+
677
+ for hotkey in orphaned_hotkeys:
678
+ hotkey_name = hotkey.get("name", "N/A")
679
+ # Use EVM address if available (for ECDSA keys), otherwise fall back to ss58_address
680
+ hotkey_address = hotkey.get("evm_address") or hotkey.get("address") or hotkey.get("ss58_address", "N/A")
681
+ owner_address = hotkey.get("owner_address", "Unknown")
682
+ owner_coldkey_name = hotkey.get("owner_coldkey_name", "Unknown")
683
+ is_hotkey_encrypted = hotkey.get("is_encrypted", False)
684
+ hotkey_key_type = hotkey.get("key_type", "ecdsa").upper()
685
+
686
+ # Format orphaned hotkey label
687
+ # Show lock icon only for encrypted hotkeys, no icon for unencrypted
688
+ orphaned_label = Text()
689
+ orphaned_label.append("⚡ ", style="bold red")
690
+ orphaned_label.append("Hotkey ", style="bold yellow")
691
+ orphaned_label.append(hotkey_name, style="bold green")
692
+ orphaned_label.append(f" {hotkey_address}", style="dim white")
693
+ orphaned_label.append(f" [{hotkey_key_type}]", style="dim cyan")
694
+ # Only show lock icon for encrypted hotkeys
695
+ if is_hotkey_encrypted:
696
+ orphaned_label.append(" 🔒", style="dim")
697
+ orphaned_label.append(f" (owner: {owner_coldkey_name or owner_address})", style="dim red")
698
+
699
+ orphaned_branch.add(orphaned_label)
700
+
701
+ # Display the tree
702
+ console.print(root)
703
+
704
+ # Show enhanced summary
705
+ total_coldkeys = len(coldkeys)
706
+ total_hotkeys = len(hotkeys)
707
+ total_orphaned = len(orphaned_hotkeys)
708
+
709
+ console.print()
710
+ summary_text = Text()
711
+ summary_text.append("📊 Summary: ", style="bold")
712
+ summary_text.append(f"{total_coldkeys} coldkey", style="cyan")
713
+ summary_text.append("s" if total_coldkeys != 1 else "", style="cyan")
714
+ summary_text.append(", ", style="dim")
715
+ summary_text.append(f"{total_hotkeys} hotkey", style="yellow")
716
+ summary_text.append("s" if total_hotkeys != 1 else "", style="yellow")
717
+ if total_orphaned > 0:
718
+ summary_text.append(" (", style="dim")
719
+ summary_text.append(f"{total_orphaned} orphaned", style="yellow")
720
+ summary_text.append(")", style="dim")
721
+ console.print(summary_text)