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,809 @@
1
+ """
2
+ HTCLI table components.
3
+ Provides enhanced tables for data display.
4
+ """
5
+
6
+ from typing import Any, Optional
7
+
8
+ from rich import box
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+ from src.htcli.utils.wallet.crypto import format_address_display
12
+
13
+ from ..colors import HTCLIColors
14
+ from ..display import get_console
15
+
16
+
17
+ class HTCLITable:
18
+ """Enhanced table for displaying HTCLI data."""
19
+
20
+ def __init__(
21
+ self,
22
+ title: str = None,
23
+ caption: str = None,
24
+ show_header: bool = True,
25
+ show_lines: bool = False,
26
+ show_edge: bool = True,
27
+ show_footer: bool = False,
28
+ border_style: str = "htcli.table.border",
29
+ header_style: str = "htcli.table.header",
30
+ footer_style: str = "table.footer",
31
+ row_styles: list[str] = None,
32
+ box_style=box.HORIZONTALS,
33
+ expand: bool = True,
34
+ padding: tuple = (0, 0),
35
+ ):
36
+ if row_styles is None:
37
+ row_styles = ["dim", ""]
38
+ self.table = Table(
39
+ title=title,
40
+ caption=caption,
41
+ show_header=show_header,
42
+ show_lines=show_lines,
43
+ show_edge=show_edge,
44
+ show_footer=show_footer,
45
+ border_style=border_style,
46
+ header_style=header_style,
47
+ footer_style=footer_style,
48
+ row_styles=row_styles or ["dim", ""],
49
+ box=box_style,
50
+ expand=expand,
51
+ padding=padding,
52
+ )
53
+
54
+ @classmethod
55
+ def minimal_header_table(
56
+ cls,
57
+ title: str = None,
58
+ caption: str = None,
59
+ header_style: str = "bold blue",
60
+ ):
61
+ """
62
+ Create a table with minimal styling - only header underlines and bottom border.
63
+ No side borders or column separators.
64
+ """
65
+ return cls(
66
+ title=title,
67
+ caption=caption,
68
+ show_header=True,
69
+ show_lines=False,
70
+ show_edge=False,
71
+ show_footer=False,
72
+ border_style="dim",
73
+ header_style=header_style,
74
+ box_style=box.HORIZONTALS,
75
+ expand=True,
76
+ padding=(1, 1),
77
+ )
78
+
79
+ def add_column(
80
+ self,
81
+ header: str,
82
+ footer: str = "",
83
+ style: str = None,
84
+ footer_style: str = None,
85
+ justify: str = "left",
86
+ min_width: int = None,
87
+ max_width: int = None,
88
+ width: int = None,
89
+ no_wrap: bool = False,
90
+ ):
91
+ """Add a column to the table."""
92
+ self.table.add_column(
93
+ header,
94
+ footer=footer,
95
+ style=style,
96
+ footer_style=footer_style,
97
+ justify=justify,
98
+ min_width=min_width,
99
+ max_width=max_width,
100
+ width=width,
101
+ no_wrap=no_wrap,
102
+ )
103
+
104
+ def add_row(self, *values):
105
+ """Add a row to the table."""
106
+ self.table.add_row(*[str(v) for v in values])
107
+
108
+ def add_blockchain_row(self, data: dict[str, Any]):
109
+ """Add a row with blockchain-specific formatting."""
110
+ formatted_values = []
111
+ for key, value in data.items():
112
+ if key.lower() in ["address", "account", "validator"]:
113
+ formatted_values.append(f"[htcli.address]{value}[/]")
114
+ elif key.lower() in ["amount", "balance", "stake"]:
115
+ formatted_values.append(f"[htcli.amount]{value}[/]")
116
+ elif key.lower() in ["status", "state"]:
117
+ color = HTCLIColors.get_status_color(str(value))
118
+ formatted_values.append(f"[{color}]{value}[/]")
119
+ elif key.lower() in ["hash", "tx_hash", "block_hash"]:
120
+ formatted_values.append(f"[htcli.hash]{value}[/]")
121
+ else:
122
+ formatted_values.append(str(value))
123
+ self.table.add_row(*formatted_values)
124
+
125
+ def set_column_footers(self, footers: list[str]):
126
+ """Set footers for all columns."""
127
+ if len(footers) != len(self.table.columns):
128
+ raise ValueError(
129
+ f"Number of footers ({len(footers)}) must match number of columns ({len(self.table.columns)})"
130
+ )
131
+
132
+ for i, footer in enumerate(footers):
133
+ self.table.columns[i].footer = footer
134
+
135
+ def render(self, console: Console = None, with_panel: bool = False):
136
+ """
137
+ Render the table wrapped inside a full Layout.
138
+
139
+ Args:
140
+ console (Console, optional): Rich console to print to.
141
+ with_panel (bool): If True, wraps the table in a Panel to fill vertical space.
142
+ """
143
+ console = console or get_console().console
144
+
145
+ renderable = self.table
146
+
147
+ console.print(renderable)
148
+
149
+ def get_renderable(self):
150
+ """Get the table as a renderable object."""
151
+ return self.table
152
+
153
+
154
+ # Specialized table factory functions
155
+ def create_subnet_table(subnets: list[dict]) -> HTCLITable:
156
+ """Create a standardized subnet table with bright, bold colors for displaying subnet information."""
157
+ # Table styling:
158
+ # - show_edge=False, header_style="bold white", border_style="bright_black"
159
+ # - style="bold", title_justify="center", show_lines=False, pad_edge=True
160
+ # Color palette values:
161
+ # HEADER = "#4196D6" (Light Blue)
162
+ # SUBHEADING = "#AFEFFF" (Pale Blue)
163
+ # SUBHEADING_EXTRA_1 = "#96A3C5" (Grayish Blue)
164
+ # STAKE_ALPHA = "#53B5A0" (Teal)
165
+ # POOLS.EMISSION = "#F8D384" (Light Orange)
166
+
167
+ # Get network name if available for title display
168
+ try:
169
+ from ...dependencies import get_config
170
+ config = get_config()
171
+ network_name = getattr(config, 'network', {}).get('endpoint', 'Hypertensor') if config else 'Hypertensor'
172
+ # Extract network identifier from endpoint if it's a URL
173
+ if '://' in network_name:
174
+ network_name = network_name.split('://')[-1].split('.')[0] if '.' in network_name else 'Hypertensor'
175
+ except:
176
+ network_name = 'Hypertensor'
177
+
178
+ table = HTCLITable(
179
+ title=f"\n[#4196D6]Subnets[/#4196D6]"
180
+ f"\nNetwork: [#AFEFFF]{network_name}[/#AFEFFF]\n\n",
181
+ header_style="bold white",
182
+ border_style="bright_black",
183
+ box_style=box.MINIMAL,
184
+ show_footer=True, # Enable footer for totals
185
+ show_edge=False,
186
+ show_lines=False,
187
+ padding=(0, 1), # Match Rich Table default spacing (vertical, horizontal)
188
+ row_styles=["", ""], # No alternating dimness - all rows same brightness
189
+ )
190
+
191
+ # Set table style to bold
192
+ table.table.style = "bold"
193
+ # Set title justification to center
194
+ table.table.title_justify = "center"
195
+ # Enable pad_edge
196
+ table.table.pad_edge = True
197
+
198
+ # Column styles with bright colors:
199
+ # Subnetid: centered, grey89 style
200
+ table.add_column(
201
+ "[bold white]Subnetid[/bold white]",
202
+ style="grey89",
203
+ justify="center",
204
+ )
205
+ # Name: left, cyan style
206
+ table.add_column("[bold white]Name[/bold white]", style="cyan", justify="left")
207
+ # Owner: left, steel_blue3 style (full address)
208
+ table.add_column(
209
+ "[bold white]Coldkey[/bold white]",
210
+ style="htcli.address",
211
+ justify="left",
212
+ no_wrap=True,
213
+ )
214
+ # Status: center, using #96A3C5 (Grayish Blue)
215
+ table.add_column(
216
+ "[bold white]Status[/bold white]",
217
+ style="#96A3C5",
218
+ justify="center",
219
+ )
220
+ # Total Nodes: center, using dark_sea_green2
221
+ table.add_column(
222
+ "[bold white]Nodes[/bold white]",
223
+ style="dark_sea_green2",
224
+ justify="center",
225
+ )
226
+ # Active Nodes: center, using #F8D384 (Light Orange)
227
+ table.add_column(
228
+ "[bold white]Active[/bold white]",
229
+ style="#F8D384",
230
+ justify="center",
231
+ )
232
+ # Min Stake: right, using #53B5A0 (Teal)
233
+ table.add_column(
234
+ "[bold white]Min Stake[/bold white]",
235
+ style="#53B5A0",
236
+ justify="right",
237
+ )
238
+ # Max Stake: right, using steel_blue3
239
+ table.add_column(
240
+ "[bold white]Max Stake[/bold white]",
241
+ style="steel_blue3",
242
+ justify="right",
243
+ )
244
+ # Delegate Stake: right, using #F8D384 (Light Orange) - shows total delegated stake
245
+ table.add_column(
246
+ "[bold white]Del. Stake[/bold white]",
247
+ style="#F8D384",
248
+ justify="right",
249
+ )
250
+ # Stake: left, using #53B5A0 (Teal)
251
+ table.add_column(
252
+ "[bold white]Stake[/bold white]",
253
+ style="#53B5A0",
254
+ justify="left",
255
+ )
256
+
257
+ def get_subnet_field(subnet, field_name, default=None):
258
+ """Get field from subnet whether it's a dict or Pydantic model."""
259
+ if isinstance(subnet, dict):
260
+ return subnet.get(field_name, default)
261
+ elif hasattr(subnet, field_name):
262
+ return getattr(subnet, field_name, default)
263
+ elif hasattr(subnet, "model_dump"):
264
+ # Pydantic model - convert to dict first
265
+ subnet_dict = subnet.model_dump()
266
+ return subnet_dict.get(field_name, default)
267
+ return default
268
+
269
+ # Initialize totals for footer
270
+ total_subnets = len(subnets)
271
+ total_nodes = 0
272
+ total_active_nodes = 0
273
+ total_min_stake = 0
274
+ total_max_stake = 0
275
+ total_delegate_stake = 0
276
+ total_stake = 0
277
+
278
+ for subnet in subnets:
279
+ # Convert to dict if it's a Pydantic model
280
+ if hasattr(subnet, "model_dump") and not isinstance(subnet, dict):
281
+ subnet_dict = subnet.model_dump()
282
+ else:
283
+ subnet_dict = subnet if isinstance(subnet, dict) else {}
284
+
285
+ # Get the actual state from blockchain (Registered, Active, Paused)
286
+ state = get_subnet_field(subnet, "state", "Unknown")
287
+ if state is None:
288
+ state = "Unknown"
289
+
290
+ # Format state with color based on actual state
291
+ if state == "Active" or state == "active":
292
+ status_display = "[bold green]Active[/]"
293
+ elif state == "Registered" or state == "registered":
294
+ status_display = "[bold yellow]Registered[/]"
295
+ elif state == "Paused" or state == "paused":
296
+ status_display = "[bold red]Paused[/]"
297
+ else:
298
+ # Handle lowercase or other variations
299
+ state_lower = str(state).lower()
300
+ if "active" in state_lower:
301
+ status_display = "[bold green]Active[/]"
302
+ elif "registered" in state_lower:
303
+ status_display = "[bold yellow]Registered[/]"
304
+ elif "paused" in state_lower:
305
+ status_display = "[bold red]Paused[/]"
306
+ else:
307
+ status_display = f"[dim]{state}[/]"
308
+
309
+ subnet_id = str(get_subnet_field(subnet, "id", "N/A"))
310
+
311
+ # Format friendly ID
312
+ friendly_id = get_subnet_field(subnet, "friendly_id")
313
+ friendly_id_display = str(friendly_id) if friendly_id is not None else "—"
314
+
315
+ # Format name - handle bytes/str
316
+ subnet_name = get_subnet_field(subnet, "name", "Unknown")
317
+ if isinstance(subnet_name, bytes):
318
+ try:
319
+ subnet_name = subnet_name.decode("utf-8")
320
+ except:
321
+ subnet_name = subnet_name.hex()[:20] + "..."
322
+ elif not isinstance(subnet_name, str):
323
+ subnet_name = str(subnet_name) if subnet_name else "Unknown"
324
+
325
+ # Format owner address - show full address
326
+ owner = get_subnet_field(subnet, "owner")
327
+ if owner and owner != "N/A" and owner is not None:
328
+ owner_str = str(owner)
329
+ owner_display = f"[htcli.address]{format_address_display(owner_str)}[/htcli.address]"
330
+ else:
331
+ owner_display = "[dim]N/A[/dim]"
332
+
333
+ # Format node counts
334
+ subnet_total_nodes = get_subnet_field(subnet, "total_nodes", 0) or 0
335
+ subnet_total_active = get_subnet_field(subnet, "total_active_nodes", 0) or 0
336
+ total_electable = get_subnet_field(subnet, "total_electable_nodes", 0) or 0
337
+
338
+ # Accumulate totals for footer
339
+ total_nodes += subnet_total_nodes
340
+ total_active_nodes += subnet_total_active
341
+
342
+ # Format stake values (convert from raw to TENSOR for display)
343
+ def format_stake(value, default=0):
344
+ """Format stake value in TENSOR units."""
345
+ if value is None:
346
+ return "0"
347
+ # Convert from raw (smallest unit) to TENSOR
348
+ tensor_value = value / 1e18 if value > 0 else 0
349
+ if tensor_value == 0:
350
+ return "0"
351
+ if tensor_value < 0.01:
352
+ return f"{tensor_value:.6f}"
353
+ if tensor_value < 1000:
354
+ return f"{tensor_value:.2f}"
355
+ return f"{tensor_value:,.0f}"
356
+
357
+ def get_stake_value(subnet, field_name, default=0):
358
+ """Get raw stake value in wei for calculations."""
359
+ value = get_subnet_field(subnet, field_name, default) or 0
360
+ return value if isinstance(value, (int, float)) else 0
361
+
362
+ # Get raw stake values for totals
363
+ subnet_min_stake_raw = get_stake_value(subnet, "min_stake", 0)
364
+ subnet_max_stake_raw = get_stake_value(subnet, "max_stake", 0)
365
+ # Prefer new field name from updated SubnetInfo, fall back to legacy key if present
366
+ subnet_total_stake_raw = get_stake_value(subnet, "total_subnet_stake", 0)
367
+ if subnet_total_stake_raw == 0:
368
+ subnet_total_stake_raw = get_stake_value(subnet, "total_stake", 0)
369
+ # Delegate stake (total delegated stake balance on subnet)
370
+ subnet_delegate_stake_raw = get_stake_value(subnet, "total_subnet_delegate_stake_balance", 0)
371
+
372
+ # Accumulate stake totals
373
+ total_min_stake += subnet_min_stake_raw
374
+ total_max_stake += subnet_max_stake_raw
375
+ total_delegate_stake += subnet_delegate_stake_raw
376
+ total_stake += subnet_total_stake_raw
377
+
378
+ # Format for display
379
+ min_stake = format_stake(subnet_min_stake_raw)
380
+ max_stake = format_stake(subnet_max_stake_raw)
381
+ delegate_stake = format_stake(subnet_delegate_stake_raw)
382
+ subnet_total_stake_formatted = format_stake(subnet_total_stake_raw)
383
+
384
+ # Format subnet name with cyan color
385
+ subnet_name_display = f"[cyan]{subnet_name}[/cyan]"
386
+
387
+ # Format stake values with TENSOR suffix if > 0
388
+ min_stake_display = f"{min_stake}" if min_stake != "0" else "0"
389
+ max_stake_display = f"{max_stake}" if max_stake != "0" else "0"
390
+ delegate_stake_display = f"{delegate_stake}" if delegate_stake != "0" else "0"
391
+ total_stake_display = f"{subnet_total_stake_formatted}" if subnet_total_stake_formatted != "0" else "0"
392
+
393
+ table.add_row(
394
+ subnet_id,
395
+ subnet_name_display,
396
+ owner_display,
397
+ status_display,
398
+ str(subnet_total_nodes),
399
+ str(subnet_total_active),
400
+ min_stake_display,
401
+ max_stake_display,
402
+ delegate_stake_display,
403
+ total_stake_display,
404
+ )
405
+
406
+ # Format totals for footer display
407
+ def format_total_stake(value):
408
+ """Format total stake value in TENSOR units for footer."""
409
+ if value is None or value == 0:
410
+ return "0"
411
+ # Convert from raw (smallest unit) to TENSOR
412
+ tensor_value = value / 1e18 if value > 0 else 0
413
+ if tensor_value == 0:
414
+ return "0"
415
+ if tensor_value < 0.01:
416
+ return f"{tensor_value:.6f}"
417
+ if tensor_value < 1000:
418
+ return f"{tensor_value:,.2f}"
419
+ return f"{tensor_value:,.0f}"
420
+
421
+ # Set footer with totals
422
+ footer_values = [
423
+ f"[bold]{total_subnets}[/bold]", # Total number of subnets
424
+ "", # Name column
425
+ "", # Owner column
426
+ "", # Status column
427
+ f"[bold]{total_nodes}[/bold]", # Total nodes
428
+ f"[bold]{total_active_nodes}[/bold]", # Total active nodes
429
+ f"[bold]{format_total_stake(total_min_stake)}[/bold]", # Total min stake
430
+ f"[bold]{format_total_stake(total_max_stake)}[/bold]", # Total max stake
431
+ f"[bold]{format_total_stake(total_delegate_stake)}[/bold]", # Total delegate stake
432
+ f"[bold]{format_total_stake(total_stake)}[/bold]", # Total stake
433
+ ]
434
+ table.set_column_footers(footer_values)
435
+
436
+ return table
437
+
438
+
439
+ def create_node_table(nodes: list) -> HTCLITable:
440
+ """Create a standardized node table for displaying all nodes."""
441
+ table = HTCLITable(
442
+ title="🔗 Network Nodes",
443
+ header_style="bold cyan",
444
+ border_style="cyan",
445
+ )
446
+ table.add_column("Subnet", style="bold", width=8)
447
+ table.add_column("Node ID", style="bold blue", width=8)
448
+ table.add_column("Coldkey", style="htcli.address", min_width=25, max_width=42)
449
+ table.add_column("Hotkey", style="htcli.address", min_width=25, max_width=42)
450
+ table.add_column("Status", style="bold", justify="center", width=14)
451
+ table.add_column("Stake", style="green", justify="right", width=15)
452
+ table.add_column("Penalties", style="yellow", justify="center", width=10)
453
+
454
+ for node in nodes:
455
+ # Handle both dict and object formats
456
+ if hasattr(node, 'subnet_id'):
457
+ subnet_id = str(node.subnet_id)
458
+ node_id = str(node.subnet_node_id)
459
+ coldkey = node.coldkey[:10] + "..." if len(node.coldkey) > 13 else node.coldkey
460
+ hotkey = node.hotkey[:10] + "..." if len(node.hotkey) > 13 else node.hotkey
461
+ stake_balance = node.stake_balance / 1e18 # Convert from wei
462
+ penalties = str(node.penalties)
463
+
464
+ # Get classification status
465
+ classification = node.classification
466
+ if isinstance(classification, dict):
467
+ node_class = classification.get('node_class', 'Unknown')
468
+ else:
469
+ node_class = 'Unknown'
470
+ else:
471
+ subnet_id = str(node.get("subnet_id", "N/A"))
472
+ node_id = str(node.get("subnet_node_id", "N/A"))
473
+ coldkey_full = node.get("coldkey", "N/A")
474
+ coldkey = coldkey_full[:10] + "..." if len(coldkey_full) > 13 else coldkey_full
475
+ hotkey_full = node.get("hotkey", "N/A")
476
+ hotkey = hotkey_full[:10] + "..." if len(hotkey_full) > 13 else hotkey_full
477
+ stake_balance = node.get("stake_balance", 0) / 1e18
478
+ penalties = str(node.get("penalties", 0))
479
+
480
+ classification = node.get("classification", {})
481
+ node_class = classification.get('node_class', 'Unknown') if isinstance(classification, dict) else 'Unknown'
482
+
483
+ # Format status with color
484
+ if node_class == 'Active':
485
+ status_display = "[bold green]✓ Active[/]"
486
+ elif node_class == 'Deactivated':
487
+ status_display = "[yellow]⚠ Deactivated[/]"
488
+ elif node_class == 'Included':
489
+ status_display = "[cyan]◉ Included[/]"
490
+ elif node_class == 'Queue':
491
+ status_display = "[blue]⋯ Queued[/]"
492
+ else:
493
+ status_display = f"[dim]{node_class}[/]"
494
+
495
+ table.add_row(
496
+ subnet_id,
497
+ node_id,
498
+ coldkey,
499
+ hotkey,
500
+ status_display,
501
+ status_display,
502
+ f"{stake_balance:,.4f}",
503
+ penalties,
504
+ penalties,
505
+ )
506
+
507
+ return table
508
+
509
+
510
+ def create_wallet_table(wallets: list[dict]) -> HTCLITable:
511
+ """Create a standardized wallet table."""
512
+ table = HTCLITable(
513
+ title="💰 Wallets",
514
+ header_style="bold green",
515
+ border_style="green",
516
+ )
517
+ table.add_column("Name", style="bold blue", min_width=15)
518
+ table.add_column("Type", style="bold", width=8)
519
+ table.add_column("Address", style="htcli.address", min_width=20)
520
+ table.add_column("Key Type", style="dim", width=8)
521
+ table.add_column("Balance", style="green", justify="right", width=12)
522
+
523
+ for wallet in wallets:
524
+ table.add_row(
525
+ wallet.get("name", "Unknown"),
526
+ wallet.get("wallet_type", "Unknown"),
527
+ wallet.get("address", "N/A"),
528
+ wallet.get("key_type", "Unknown"),
529
+ f"{wallet.get('balance', 0):,}",
530
+ )
531
+
532
+ return table
533
+
534
+
535
+ def create_balance_table(balances: list[dict], total_balance: float = 0) -> HTCLITable:
536
+ """Create a balance table for wallet balances with footer showing total."""
537
+ table = HTCLITable(
538
+ title="💰 Balance Information",
539
+ header_style="bold green",
540
+ border_style="green",
541
+ show_footer=True,
542
+ )
543
+ table.add_column("Wallet", style="bold blue", min_width=15)
544
+ table.add_column("Address", style="htcli.address", min_width=20)
545
+ table.add_column("Balance", style="green", justify="right", width=12)
546
+ table.add_column("Status", style="bold", justify="center", width=8)
547
+
548
+ # Track total for footer
549
+ total_balance_tensor = 0
550
+
551
+ for balance in balances:
552
+ balance_amount = balance.get("balance_tensor", balance.get("balance", 0))
553
+
554
+ # Handle both string and numeric values
555
+ if isinstance(balance_amount, str):
556
+ balance_display = balance_amount
557
+ elif isinstance(balance_amount, (int, float)) and balance_amount > 0:
558
+ # Convert from wei to TENSOR
559
+ balance_tensor = balance_amount / 1e18
560
+ balance_display = f"{balance_tensor:,.2f}"
561
+ total_balance_tensor += balance_tensor
562
+ else:
563
+ balance_display = "0.00"
564
+
565
+ table.add_row(
566
+ balance.get("name", "Unknown"),
567
+ balance.get("address", "N/A"),
568
+ balance_display,
569
+ balance.get("status", "Active"),
570
+ )
571
+
572
+ # Add footer row with total balance
573
+ footer = [
574
+ "",
575
+ "",
576
+ "[bold]Total[/bold]",
577
+ f"[bold]{total_balance_tensor:,.2f}[/bold]",
578
+ "",
579
+ ]
580
+ table.set_column_footers(footer)
581
+
582
+ return table
583
+
584
+
585
+ def create_htcli_balance_table(
586
+ balances: list[dict],
587
+ totals: dict,
588
+ network_name: Optional[str] = None,
589
+ show_totals: bool = True,
590
+ ) -> Table:
591
+ """Create an HTCLI balance table with staking breakdown and totals footer."""
592
+ from ...utils.wallet.crypto import format_address_display
593
+
594
+ title_lines = ["Wallet Coldkey Balance"]
595
+ if network_name:
596
+ title_lines.append(f"Network: {network_name}")
597
+ title_lines.append("[dim]All values in TENSOR[/dim]")
598
+ title = "\n".join(
599
+ [
600
+ f"[{HTCLIColors.PRIMARY}]{line}[/{HTCLIColors.PRIMARY}]"
601
+ if "dim" not in line else line
602
+ for line in title_lines
603
+ ]
604
+ )
605
+
606
+ table = Table(
607
+ title=f"\n{title}\n",
608
+ show_footer=show_totals,
609
+ show_edge=False,
610
+ border_style=HTCLIColors.GRAY_500,
611
+ box=box.SIMPLE_HEAVY,
612
+ pad_edge=False,
613
+ expand=True,
614
+ )
615
+
616
+ table.add_column(
617
+ "[white]Wallet Name", style=HTCLIColors.PRIMARY_LIGHT, no_wrap=True
618
+ )
619
+ table.add_column(
620
+ "[white]Coldkey Address", style="htcli.address", no_wrap=True
621
+ )
622
+ table.add_column(
623
+ "[white]Free Balance",
624
+ justify="right",
625
+ style=HTCLIColors.BALANCE,
626
+ no_wrap=True,
627
+ )
628
+ table.add_column(
629
+ "[white]Direct Stake",
630
+ justify="right",
631
+ style="#53B5A0", # Teal for direct stake
632
+ no_wrap=True,
633
+ )
634
+ table.add_column(
635
+ "[white]Delegate",
636
+ justify="right",
637
+ style=HTCLIColors.STAKING_ORANGE,
638
+ no_wrap=True,
639
+ )
640
+ table.add_column(
641
+ "[white]Node Del.",
642
+ justify="right",
643
+ style="#96A3C5", # Grayish blue for node delegate
644
+ no_wrap=True,
645
+ )
646
+ table.add_column(
647
+ "[white]Overwatch",
648
+ justify="right",
649
+ style="#E066FF", # Purple for overwatch stake
650
+ no_wrap=True,
651
+ )
652
+ table.add_column(
653
+ "[white]Unbonding",
654
+ justify="right",
655
+ style="#FF6B6B", # Coral/Red for unbonding (in waiting period)
656
+ no_wrap=True,
657
+ )
658
+ table.add_column(
659
+ "[white]Total Balance",
660
+ justify="right",
661
+ style=HTCLIColors.CRYPTO_GOLD,
662
+ no_wrap=True,
663
+ )
664
+
665
+ def _format_tensor(value: Optional[int], precision: int = 2) -> str:
666
+ if value is None:
667
+ return "--"
668
+ try:
669
+ tensor_value = value / 1e18
670
+ except TypeError:
671
+ return str(value)
672
+ return f"{tensor_value:,.{precision}f}"
673
+
674
+ for row in balances:
675
+ address = row.get("address") or "N/A"
676
+ address_display = format_address_display(address) if address else "N/A"
677
+
678
+ free_display = row.get("display_free") or _format_tensor(
679
+ row.get("free_balance", 0)
680
+ )
681
+ direct_display = _format_tensor(row.get("direct_stake", 0))
682
+ delegate_display = _format_tensor(row.get("delegate_stake", 0))
683
+ node_delegate_display = _format_tensor(row.get("node_delegate_stake", 0))
684
+ overwatch_display = _format_tensor(row.get("overwatch_stake", 0))
685
+ unbonding_display = _format_tensor(row.get("unbonding", 0))
686
+ total_value = row.get("total_balance")
687
+ if total_value is None:
688
+ total_value = (row.get("free_balance", 0) or 0) + (
689
+ row.get("staked_balance", 0) or 0
690
+ )
691
+ total_display = row.get("display_total") or _format_tensor(total_value)
692
+
693
+ table.add_row(
694
+ row.get("name", "Unknown"),
695
+ address_display,
696
+ free_display,
697
+ direct_display,
698
+ delegate_display,
699
+ node_delegate_display,
700
+ overwatch_display,
701
+ unbonding_display,
702
+ total_display,
703
+ )
704
+
705
+ if show_totals and totals:
706
+ wallet_count = len(balances)
707
+ footers = [
708
+ f"[bold white]{wallet_count} wallet{'s' if wallet_count != 1 else ''}[/bold white]",
709
+ "",
710
+ f"[bold]{_format_tensor(totals.get('free'))}[/bold]",
711
+ f"[bold]{_format_tensor(totals.get('direct_stake'))}[/bold]",
712
+ f"[bold]{_format_tensor(totals.get('delegate_stake'))}[/bold]",
713
+ f"[bold]{_format_tensor(totals.get('node_delegate_stake'))}[/bold]",
714
+ f"[bold]{_format_tensor(totals.get('overwatch_stake'))}[/bold]",
715
+ f"[bold]{_format_tensor(totals.get('unbonding'))}[/bold]",
716
+ f"[bold]{_format_tensor(totals.get('total'))}[/bold]",
717
+ ]
718
+ for column, footer in zip(table.columns, footers):
719
+ column.footer = footer
720
+
721
+ return table
722
+
723
+
724
+ def create_wallet_minimal_table(wallets: list[dict[str, Any]]) -> HTCLITable:
725
+ """
726
+ Create a wallet table with the same structure as display_keys_table but using HTCLITable.
727
+ """
728
+ from ...utils.wallet.crypto import format_address_display
729
+
730
+ table = HTCLITable(
731
+ title="Stored Wallets",
732
+ header_style="htcli.table.header",
733
+ border_style="htcli.table.border",
734
+ row_styles=["", "dim"],
735
+ box_style=box.SIMPLE_HEAD,
736
+ show_edge=False,
737
+ padding=(0, 1),
738
+ )
739
+
740
+ table.add_column("Wallet", style="bold white", min_width=18, no_wrap=True)
741
+ table.add_column("Type", style="htcli.secondary", justify="center", width=10)
742
+ table.add_column("Address", style="htcli.address", min_width=24)
743
+ table.add_column("Owner / Coldkey", style="htcli.address", min_width=24)
744
+ table.add_column("Key Type", style="htcli.value", justify="center", width=10)
745
+ table.add_column("Encryption", style="htcli.value", justify="center", width=12)
746
+
747
+ coldkeys = [k for k in wallets if not k.get("is_hotkey", False)]
748
+ hotkeys = [k for k in wallets if k.get("is_hotkey", False)]
749
+
750
+ address_to_name = {
751
+ key.get("ss58_address"): key.get("name", "Unknown") for key in coldkeys
752
+ }
753
+
754
+ def _build_row(key_info: dict, is_hotkey: bool):
755
+ name = key_info.get("name", "Unknown")
756
+ icon = "🔥" if is_hotkey else "❄️"
757
+ wallet_label = f"{icon} {name}"
758
+
759
+ wallet_type = (
760
+ "[htcli.secondary]HOTKEY[/htcli.secondary]"
761
+ if is_hotkey
762
+ else "[htcli.success]COLDKEY[/htcli.success]"
763
+ )
764
+
765
+ address = key_info.get("ss58_address") or key_info.get("address") or "N/A"
766
+ formatted_address = format_address_display(address, max_length=36)
767
+
768
+ if is_hotkey:
769
+ owner_address = key_info.get("owner_address")
770
+ if owner_address and owner_address in address_to_name:
771
+ owner_display = (
772
+ f"{format_address_display(owner_address, max_length=34)} "
773
+ f"({address_to_name[owner_address]})"
774
+ )
775
+ else:
776
+ owner_display = (
777
+ format_address_display(owner_address or "N/A", max_length=34)
778
+ )
779
+ else:
780
+ owner_display = formatted_address if formatted_address != "N/A" else "—"
781
+
782
+ encrypted_status = (
783
+ "[green]🔒 Encrypted[/green]"
784
+ if key_info.get("is_encrypted", True)
785
+ else "[yellow]Plain[/yellow]"
786
+ )
787
+
788
+ key_type = key_info.get("key_type", "—")
789
+ key_type_display = key_type.upper() if isinstance(key_type, str) else str(key_type)
790
+
791
+ table.add_row(
792
+ wallet_label,
793
+ wallet_type,
794
+ formatted_address,
795
+ owner_display,
796
+ key_type_display,
797
+ encrypted_status,
798
+ )
799
+
800
+ for key in coldkeys:
801
+ _build_row(key, is_hotkey=False)
802
+
803
+ if coldkeys and hotkeys:
804
+ table.table.add_section()
805
+
806
+ for key in hotkeys:
807
+ _build_row(key, is_hotkey=True)
808
+
809
+ return table