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,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}"))
|