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.
- htcli-1.1.0.dist-info/METADATA +509 -0
- htcli-1.1.0.dist-info/RECORD +140 -0
- htcli-1.1.0.dist-info/WHEEL +4 -0
- htcli-1.1.0.dist-info/entry_points.txt +2 -0
- htcli-1.1.0.dist-info/licenses/LICENSE +21 -0
- src/__init__.py +0 -0
- src/htcli/__init__.py +5 -0
- src/htcli/client/__init__.py +338 -0
- src/htcli/client/extrinsics/__init__.py +26 -0
- src/htcli/client/extrinsics/base.py +487 -0
- src/htcli/client/extrinsics/consensus.py +79 -0
- src/htcli/client/extrinsics/governance.py +714 -0
- src/htcli/client/extrinsics/identity.py +490 -0
- src/htcli/client/extrinsics/node.py +1054 -0
- src/htcli/client/extrinsics/overwatch.py +401 -0
- src/htcli/client/extrinsics/staking.py +1504 -0
- src/htcli/client/extrinsics/subnet.py +2218 -0
- src/htcli/client/extrinsics/validator.py +203 -0
- src/htcli/client/extrinsics/wallet.py +323 -0
- src/htcli/client/offchain/__init__.py +10 -0
- src/htcli/client/offchain/backup.py +385 -0
- src/htcli/client/offchain/config.py +541 -0
- src/htcli/client/offchain/wallet.py +839 -0
- src/htcli/client/rpc/__init__.py +20 -0
- src/htcli/client/rpc/chain.py +568 -0
- src/htcli/client/rpc/node.py +783 -0
- src/htcli/client/rpc/overwatch.py +680 -0
- src/htcli/client/rpc/staking.py +216 -0
- src/htcli/client/rpc/subnet.py +2104 -0
- src/htcli/client/rpc/wallet.py +912 -0
- src/htcli/commands/__init__.py +31 -0
- src/htcli/commands/chain/__init__.py +66 -0
- src/htcli/commands/chain/display.py +204 -0
- src/htcli/commands/chain/handlers.py +260 -0
- src/htcli/commands/config/__init__.py +158 -0
- src/htcli/commands/config/display.py +353 -0
- src/htcli/commands/config/handlers.py +347 -0
- src/htcli/commands/config/prompts.py +357 -0
- src/htcli/commands/consensus/__init__.py +61 -0
- src/htcli/commands/consensus/handlers.py +100 -0
- src/htcli/commands/governance/__init__.py +49 -0
- src/htcli/commands/governance/handlers.py +81 -0
- src/htcli/commands/node/__init__.py +304 -0
- src/htcli/commands/node/display.py +749 -0
- src/htcli/commands/node/error_handling.py +470 -0
- src/htcli/commands/node/handlers.py +844 -0
- src/htcli/commands/node/prompts.py +346 -0
- src/htcli/commands/overwatch/__init__.py +219 -0
- src/htcli/commands/overwatch/display.py +396 -0
- src/htcli/commands/overwatch/error_handling.py +276 -0
- src/htcli/commands/overwatch/handlers.py +443 -0
- src/htcli/commands/overwatch/prompts.py +359 -0
- src/htcli/commands/stake/__init__.py +736 -0
- src/htcli/commands/stake/display.py +1103 -0
- src/htcli/commands/stake/error_handling.py +425 -0
- src/htcli/commands/stake/handlers.py +1902 -0
- src/htcli/commands/stake/prompts.py +1080 -0
- src/htcli/commands/subnet/__init__.py +639 -0
- src/htcli/commands/subnet/display.py +801 -0
- src/htcli/commands/subnet/error_handling.py +524 -0
- src/htcli/commands/subnet/handlers.py +2855 -0
- src/htcli/commands/subnet/prompts.py +1225 -0
- src/htcli/commands/validator/__init__.py +192 -0
- src/htcli/commands/validator/display.py +54 -0
- src/htcli/commands/validator/handlers.py +340 -0
- src/htcli/commands/wallet/__init__.py +546 -0
- src/htcli/commands/wallet/display.py +806 -0
- src/htcli/commands/wallet/error_handling.py +210 -0
- src/htcli/commands/wallet/handlers.py +3040 -0
- src/htcli/commands/wallet/prompts.py +1518 -0
- src/htcli/config.py +184 -0
- src/htcli/dependencies.py +186 -0
- src/htcli/errors/__init__.py +63 -0
- src/htcli/errors/base.py +141 -0
- src/htcli/errors/display.py +20 -0
- src/htcli/errors/handlers.py +710 -0
- src/htcli/main.py +343 -0
- src/htcli/models/__init__.py +21 -0
- src/htcli/models/enums/enum_types.py +35 -0
- src/htcli/models/errors.py +103 -0
- src/htcli/models/requests/__init__.py +197 -0
- src/htcli/models/requests/config.py +70 -0
- src/htcli/models/requests/consensus.py +19 -0
- src/htcli/models/requests/governance.py +38 -0
- src/htcli/models/requests/identity.py +51 -0
- src/htcli/models/requests/key.py +22 -0
- src/htcli/models/requests/node.py +91 -0
- src/htcli/models/requests/overwatch.py +64 -0
- src/htcli/models/requests/staking.py +580 -0
- src/htcli/models/requests/subnet.py +195 -0
- src/htcli/models/requests/validator.py +139 -0
- src/htcli/models/requests/wallet.py +118 -0
- src/htcli/models/responses/__init__.py +147 -0
- src/htcli/models/responses/base.py +18 -0
- src/htcli/models/responses/chain.py +39 -0
- src/htcli/models/responses/config.py +58 -0
- src/htcli/models/responses/identity.py +102 -0
- src/htcli/models/responses/overwatch.py +51 -0
- src/htcli/models/responses/staking.py +502 -0
- src/htcli/models/responses/subnet.py +856 -0
- src/htcli/models/responses/wallet.py +185 -0
- src/htcli/ui/__init__.py +87 -0
- src/htcli/ui/colors.py +309 -0
- src/htcli/ui/components/__init__.py +60 -0
- src/htcli/ui/components/panels.py +174 -0
- src/htcli/ui/components/progress.py +166 -0
- src/htcli/ui/components/spinners.py +92 -0
- src/htcli/ui/components/tables.py +809 -0
- src/htcli/ui/components/trees.py +721 -0
- src/htcli/ui/display.py +336 -0
- src/htcli/ui/prompts.py +870 -0
- src/htcli/utils/__init__.py +76 -0
- src/htcli/utils/blockchain/__init__.py +75 -0
- src/htcli/utils/blockchain/formatting.py +368 -0
- src/htcli/utils/blockchain/patches.py +286 -0
- src/htcli/utils/blockchain/peer_id.py +186 -0
- src/htcli/utils/blockchain/staking.py +448 -0
- src/htcli/utils/blockchain/type_registry.py +1373 -0
- src/htcli/utils/blockchain/validation.py +179 -0
- src/htcli/utils/cache.py +613 -0
- src/htcli/utils/constants.py +38 -0
- src/htcli/utils/legacy/__init__.py +12 -0
- src/htcli/utils/legacy/colors.py +311 -0
- src/htcli/utils/legacy/crypto.py +1176 -0
- src/htcli/utils/legacy/formatting.py +452 -0
- src/htcli/utils/legacy/interactive.py +306 -0
- src/htcli/utils/legacy/subnet_manifest.py +265 -0
- src/htcli/utils/legacy/validation.py +488 -0
- src/htcli/utils/logging.py +183 -0
- src/htcli/utils/network/__init__.py +20 -0
- src/htcli/utils/network/subnet.py +344 -0
- src/htcli/utils/prompts.py +27 -0
- src/htcli/utils/scale_codec.py +155 -0
- src/htcli/utils/validation/__init__.py +57 -0
- src/htcli/utils/validation/prompt_validators.py +267 -0
- src/htcli/utils/wallet/__init__.py +65 -0
- src/htcli/utils/wallet/auth.py +151 -0
- src/htcli/utils/wallet/core.py +1069 -0
- src/htcli/utils/wallet/crypto.py +1615 -0
- 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)
|