bittensor-cli 9.0.3__py3-none-any.whl → 9.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.
@@ -0,0 +1,2876 @@
1
+ import asyncio
2
+ import json
3
+ import netaddr
4
+ from dataclasses import asdict, is_dataclass
5
+ from typing import Any, Dict, List
6
+ from pywry import PyWry
7
+
8
+ from bittensor_cli.src.bittensor.balances import Balance
9
+ from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface
10
+ from bittensor_cli.src.bittensor.utils import console
11
+ from bittensor_wallet import Wallet
12
+
13
+ root_symbol_html = f"&#x{ord('τ'):X};"
14
+
15
+
16
+ class Encoder(json.JSONEncoder):
17
+ """JSON encoder for serializing dataclasses and balances"""
18
+
19
+ def default(self, obj):
20
+ if is_dataclass(obj):
21
+ return asdict(obj)
22
+
23
+ elif isinstance(obj, Balance):
24
+ return obj.tao
25
+
26
+ return super().default(obj)
27
+
28
+
29
+ async def display_network_dashboard(
30
+ wallet: Wallet,
31
+ subtensor: "SubtensorInterface",
32
+ prompt: bool = True,
33
+ ) -> bool:
34
+ """
35
+ Generate and display the HTML interface.
36
+ """
37
+ try:
38
+ with console.status("[dark_sea_green3]Fetching data...", spinner="earth"):
39
+ _subnet_data = await fetch_subnet_data(wallet, subtensor)
40
+ subnet_data = process_subnet_data(_subnet_data)
41
+ html_content = generate_full_page(subnet_data)
42
+
43
+ console.print(
44
+ "[dark_sea_green3]Opening dashboard in a window. Press Ctrl+C to close.[/dark_sea_green3]"
45
+ )
46
+ window = PyWry()
47
+ window.send_html(
48
+ html=html_content,
49
+ title="Bittensor View",
50
+ width=1200,
51
+ height=800,
52
+ )
53
+ window.start()
54
+ await asyncio.sleep(10)
55
+ try:
56
+ while True:
57
+ if _has_exited(window):
58
+ break
59
+ await asyncio.sleep(1)
60
+ except KeyboardInterrupt:
61
+ console.print("\n[yellow]Closing Bittensor View...[/yellow]")
62
+ finally:
63
+ if not _has_exited(window):
64
+ try:
65
+ window.close()
66
+ except Exception:
67
+ pass
68
+
69
+ except Exception as e:
70
+ print(f"Error: {e}")
71
+ return False
72
+
73
+
74
+ def int_to_ip(int_val: int) -> str:
75
+ """Maps to an ip string"""
76
+ return str(netaddr.IPAddress(int_val))
77
+
78
+
79
+ def get_hotkey_identity(
80
+ hotkey_ss58: str,
81
+ identities: dict,
82
+ old_identities: dict,
83
+ trucate_length: int = 4,
84
+ ) -> str:
85
+ """Fetch identity of hotkey from both sources"""
86
+ if hk_identity := identities["hotkeys"].get(hotkey_ss58):
87
+ return hk_identity.get("identity", {}).get("name", "") or hk_identity.get(
88
+ "display", "~"
89
+ )
90
+ elif old_identity := old_identities.get(hotkey_ss58):
91
+ return old_identity.display
92
+ else:
93
+ return f"{hotkey_ss58[:trucate_length]}...{hotkey_ss58[-trucate_length:]}"
94
+
95
+
96
+ async def fetch_subnet_data(
97
+ wallet: Wallet, subtensor: "SubtensorInterface"
98
+ ) -> Dict[str, Any]:
99
+ """
100
+ Fetch subnet data from the network.
101
+ """
102
+ block_hash = await subtensor.substrate.get_chain_head()
103
+
104
+ (
105
+ balance,
106
+ stake_info,
107
+ metagraphs_info,
108
+ subnets_info,
109
+ ck_hk_identities,
110
+ old_identities,
111
+ block_number,
112
+ ) = await asyncio.gather(
113
+ subtensor.get_balance(wallet.coldkeypub.ss58_address, block_hash=block_hash),
114
+ subtensor.get_stake_for_coldkey(
115
+ wallet.coldkeypub.ss58_address, block_hash=block_hash
116
+ ),
117
+ subtensor.get_all_metagraphs_info(block_hash=block_hash),
118
+ subtensor.all_subnets(block_hash=block_hash),
119
+ subtensor.fetch_coldkey_hotkey_identities(block_hash=block_hash),
120
+ subtensor.get_delegate_identities(block_hash=block_hash),
121
+ subtensor.substrate.get_block_number(block_hash=block_hash),
122
+ )
123
+
124
+ return {
125
+ "balance": balance,
126
+ "stake_info": stake_info,
127
+ "metagraphs_info": metagraphs_info,
128
+ "subnets_info": subnets_info,
129
+ "ck_hk_identities": ck_hk_identities,
130
+ "old_identities": old_identities,
131
+ "wallet": wallet,
132
+ "block_number": block_number,
133
+ }
134
+
135
+
136
+ def process_subnet_data(raw_data: Dict[str, Any]) -> Dict[str, Any]:
137
+ """
138
+ Process and prepare subnet data.
139
+ """
140
+ balance = raw_data["balance"]
141
+ stake_info = raw_data["stake_info"]
142
+ metagraphs_info = raw_data["metagraphs_info"]
143
+ subnets_info = raw_data["subnets_info"]
144
+ ck_hk_identities = raw_data["ck_hk_identities"]
145
+ old_identities = raw_data["old_identities"]
146
+ wallet = raw_data["wallet"]
147
+ block_number = raw_data["block_number"]
148
+
149
+ pool_info = {info.netuid: info for info in subnets_info}
150
+
151
+ total_ideal_stake_value = Balance.from_tao(0)
152
+ total_slippage_value = Balance.from_tao(0)
153
+
154
+ # Process stake
155
+ stake_dict: Dict[int, List[Dict[str, Any]]] = {}
156
+ for stake in stake_info:
157
+ if stake.stake.tao > 0:
158
+ slippage_value, _, slippage_percentage = pool_info[
159
+ stake.netuid
160
+ ].alpha_to_tao_with_slippage(stake.stake)
161
+ ideal_value = pool_info[stake.netuid].alpha_to_tao(stake.stake)
162
+ total_ideal_stake_value += ideal_value
163
+ total_slippage_value += slippage_value
164
+ stake_dict.setdefault(stake.netuid, []).append(
165
+ {
166
+ "hotkey": stake.hotkey_ss58,
167
+ "hotkey_identity": get_hotkey_identity(
168
+ stake.hotkey_ss58, ck_hk_identities, old_identities
169
+ ),
170
+ "amount": stake.stake.tao,
171
+ "emission": stake.emission.tao,
172
+ "is_registered": stake.is_registered,
173
+ "tao_emission": stake.tao_emission.tao,
174
+ "ideal_value": ideal_value.tao,
175
+ "slippage_value": slippage_value.tao,
176
+ "slippage_percentage": slippage_percentage,
177
+ }
178
+ )
179
+
180
+ # Process metagraph
181
+ subnets = []
182
+ for meta_info in metagraphs_info:
183
+ subnet_stakes = stake_dict.get(meta_info.netuid, [])
184
+ metagraph_info = {
185
+ "netuid": meta_info.netuid,
186
+ "name": meta_info.name,
187
+ "symbol": meta_info.symbol,
188
+ "alpha_in": 0 if meta_info.netuid == 0 else meta_info.alpha_in.tao,
189
+ "alpha_out": meta_info.alpha_out.tao,
190
+ "tao_in": 0 if meta_info.netuid == 0 else meta_info.tao_in.tao,
191
+ "tao_in_emission": meta_info.tao_in_emission.tao,
192
+ "num_uids": meta_info.num_uids,
193
+ "max_uids": meta_info.max_uids,
194
+ "moving_price": meta_info.moving_price.tao,
195
+ "blocks_since_last_step": "~"
196
+ if meta_info.netuid == 0
197
+ else meta_info.blocks_since_last_step,
198
+ "tempo": "~" if meta_info.netuid == 0 else meta_info.tempo,
199
+ "registration_allowed": meta_info.registration_allowed,
200
+ "commit_reveal_weights_enabled": meta_info.commit_reveal_weights_enabled,
201
+ "hotkeys": meta_info.hotkeys,
202
+ "coldkeys": meta_info.coldkeys,
203
+ "updated_identities": [],
204
+ "processed_axons": [],
205
+ "rank": meta_info.rank,
206
+ "trust": meta_info.trust,
207
+ "consensus": meta_info.consensus,
208
+ "incentives": meta_info.incentives,
209
+ "dividends": meta_info.dividends,
210
+ "active": meta_info.active,
211
+ "validator_permit": meta_info.validator_permit,
212
+ "pruning_score": meta_info.pruning_score,
213
+ "last_update": meta_info.last_update,
214
+ "block_at_registration": meta_info.block_at_registration,
215
+ }
216
+
217
+ # Process axon data and convert IPs
218
+ for axon in meta_info.axons:
219
+ if axon:
220
+ processed_axon = {
221
+ "ip": int_to_ip(axon["ip"]) if axon["ip"] else "N/A",
222
+ "port": axon["port"],
223
+ "ip_type": axon["ip_type"],
224
+ }
225
+ metagraph_info["processed_axons"].append(processed_axon)
226
+ else:
227
+ metagraph_info["processed_axons"].append(None)
228
+
229
+ # Add identities
230
+ for hotkey in meta_info.hotkeys:
231
+ identity = get_hotkey_identity(
232
+ hotkey, ck_hk_identities, old_identities, trucate_length=2
233
+ )
234
+ metagraph_info["updated_identities"].append(identity)
235
+
236
+ # Balance conversion
237
+ for field in [
238
+ "emission",
239
+ "alpha_stake",
240
+ "tao_stake",
241
+ "total_stake",
242
+ ]:
243
+ if hasattr(meta_info, field):
244
+ raw_data = getattr(meta_info, field)
245
+ if isinstance(raw_data, list):
246
+ metagraph_info[field] = [
247
+ x.tao if hasattr(x, "tao") else x for x in raw_data
248
+ ]
249
+ else:
250
+ metagraph_info[field] = raw_data
251
+
252
+ # Calculate price
253
+ price = (
254
+ 1
255
+ if metagraph_info["netuid"] == 0
256
+ else metagraph_info["tao_in"] / metagraph_info["alpha_in"]
257
+ if metagraph_info["alpha_in"] > 0
258
+ else 0
259
+ )
260
+
261
+ # Package it all up
262
+ symbol_html = f"&#x{ord(meta_info.symbol):X};"
263
+ subnets.append(
264
+ {
265
+ "netuid": meta_info.netuid,
266
+ "name": meta_info.name,
267
+ "symbol": symbol_html,
268
+ "price": price,
269
+ "market_cap": float(
270
+ (metagraph_info["alpha_in"] + metagraph_info["alpha_out"]) * price
271
+ )
272
+ if price
273
+ else 0,
274
+ "emission": metagraph_info["tao_in_emission"],
275
+ "total_stake": metagraph_info["alpha_out"],
276
+ "your_stakes": subnet_stakes,
277
+ "metagraph_info": metagraph_info,
278
+ }
279
+ )
280
+ subnets.sort(key=lambda x: x["market_cap"], reverse=True)
281
+ return {
282
+ "wallet_info": {
283
+ "name": wallet.name,
284
+ "balance": balance.tao,
285
+ "coldkey": wallet.coldkeypub.ss58_address,
286
+ "total_ideal_stake_value": total_ideal_stake_value.tao,
287
+ "total_slippage_value": total_slippage_value.tao,
288
+ },
289
+ "subnets": subnets,
290
+ "block_number": block_number,
291
+ }
292
+
293
+
294
+ def _has_exited(handler) -> bool:
295
+ """Check if PyWry process has cleanly exited with returncode 0."""
296
+ return (
297
+ hasattr(handler, "runner")
298
+ and handler.runner is not None
299
+ and handler.runner.returncode == 0
300
+ )
301
+
302
+
303
+ def generate_full_page(data: Dict[str, Any]) -> str:
304
+ """
305
+ Generate full HTML content for the interface.
306
+ """
307
+ serializable_data = {
308
+ "wallet_info": data["wallet_info"],
309
+ "subnets": data["subnets"],
310
+ }
311
+ wallet_info_json = json.dumps(
312
+ serializable_data["wallet_info"], cls=Encoder
313
+ ).replace("'", "'")
314
+ subnets_json = json.dumps(serializable_data["subnets"], cls=Encoder).replace(
315
+ "'", "'"
316
+ )
317
+
318
+ return f"""
319
+ <!DOCTYPE html>
320
+ <html>
321
+ <head>
322
+ <title>Bittensor CLI Interface</title>
323
+ <style>
324
+ {get_css_styles()}
325
+ </style>
326
+ </head>
327
+ <body>
328
+ <!-- Embedded JSON data used by JS -->
329
+ <div id="initial-data"
330
+ data-wallet-info='{wallet_info_json}'
331
+ data-subnets='{subnets_json}'>
332
+ </div>
333
+ <div id="splash-screen">
334
+ <div class="splash-content">
335
+ <div class="title-row">
336
+ <h1 class="splash-title">Btcli View</h1>
337
+ <span class="beta-text">Beta</span>
338
+ </div>
339
+ </div>
340
+ </div>
341
+
342
+ <!-- Main content area -->
343
+ <div id="main-content">
344
+ {generate_main_header(data["wallet_info"], data["block_number"])}
345
+ {generate_main_filters()}
346
+ {generate_subnets_table(data["subnets"])}
347
+ </div>
348
+
349
+ <!-- Subnet details page (hidden by default) -->
350
+ <div id="subnet-page" style="display: none;">
351
+ {generate_subnet_details_header()}
352
+ {generate_subnet_metrics()}
353
+ {generate_neuron_details()}
354
+ </div>
355
+
356
+ <script>
357
+ {get_javascript()}
358
+ </script>
359
+ </body>
360
+ </html>
361
+ """
362
+
363
+
364
+ def generate_subnet_details_header() -> str:
365
+ """
366
+ Generates the header section for the subnet details page,
367
+ including the back button, toggle controls, title, and network visualization.
368
+ """
369
+ return """
370
+ <div class="subnet-header">
371
+ <div class="header-row">
372
+ <button class="back-button">&larr; Back</button>
373
+ <div class="toggle-group">
374
+ <label class="toggle-label">
375
+ <input type="checkbox" id="stake-toggle" onchange="toggleStakeView()">
376
+ Show Stakes
377
+ </label>
378
+ <label class="toggle-label">
379
+ <input type="checkbox" id="verbose-toggle" onchange="toggleVerboseNumbers()">
380
+ Precise Numbers
381
+ </label>
382
+ </div>
383
+ </div>
384
+
385
+ <div class="subnet-title-row">
386
+ <div class="title-price">
387
+ <h2 id="subnet-title"></h2>
388
+ <div class="subnet-price" id="subnet-price"></div>
389
+ </div>
390
+ <div class="network-visualization-container">
391
+ <div class="network-visualization">
392
+ <canvas id="network-canvas" width="700" height="80"></canvas>
393
+ </div>
394
+ </div>
395
+ </div>
396
+ <div class="network-metrics">
397
+ <div class="metric-card network-card">
398
+ <div class="metric-label">Moving Price</div>
399
+ <div id="network-moving-price" class="metric-value"></div>
400
+ </div>
401
+ <div class="metric-card network-card">
402
+ <div class="metric-label">Registration</div>
403
+ <div id="network-registration" class="metric-value registration-status"></div>
404
+ </div>
405
+ <div class="metric-card network-card">
406
+ <div class="metric-label">CR Weights</div>
407
+ <div id="network-cr" class="metric-value cr-status"></div>
408
+ </div>
409
+ <div class="metric-card network-card">
410
+ <div class="metric-label">Neurons</div>
411
+ <div id="network-neurons" class="metric-value"></div>
412
+ </div>
413
+ <div class="metric-card network-card">
414
+ <div class="metric-label">Blocks Since Step</div>
415
+ <div id="network-blocks-since-step" class="metric-value"></div>
416
+ </div>
417
+ </div>
418
+ </div>
419
+ """
420
+
421
+
422
+ def generate_subnet_metrics() -> str:
423
+ """
424
+ Generates the metrics section for the subnet details page,
425
+ including market metrics and the stakes table.
426
+ """
427
+ return """
428
+ <div class="metrics-section">
429
+ <div class="metrics-group market-metrics">
430
+ <div class="metric-card">
431
+ <div class="metric-label">Market Cap</div>
432
+ <div id="subnet-market-cap" class="metric-value"></div>
433
+ </div>
434
+ <div class="metric-card">
435
+ <div class="metric-label">Total Stake</div>
436
+ <div id="subnet-total-stake" class="metric-value"></div>
437
+ </div>
438
+ <div class="metric-card">
439
+ <div class="metric-label">Alpha Reserves</div>
440
+ <div id="network-alpha-in" class="metric-value"></div>
441
+ </div>
442
+ <div class="metric-card">
443
+ <div class="metric-label">Tao Reserves</div>
444
+ <div id="network-tau-in" class="metric-value"></div>
445
+ </div>
446
+ <div class="metric-card">
447
+ <div class="metric-label">Emission</div>
448
+ <div id="subnet-emission" class="metric-value"></div>
449
+ </div>
450
+ </div>
451
+
452
+ <div class="stakes-container">
453
+ <div class="stakes-header">
454
+ <h3 class="view-header">Metagraph</h3>
455
+ <div class="button-group">
456
+ <button class="manage-button add-stake-button" disabled title="Coming soon">
457
+ Add Stake (Coming soon)
458
+ </button>
459
+ <button class="manage-button export-csv-button" disabled title="Coming soon">
460
+ Export CSV (Coming soon)
461
+ </button>
462
+ </div>
463
+ </div>
464
+
465
+ <div class="stakes-table-container">
466
+ <table class="stakes-table">
467
+ <thead>
468
+ <tr>
469
+ <th>Hotkey</th>
470
+ <th>Amount</th>
471
+ <th>Value</th>
472
+ <th>Value (w/ slippage)</th>
473
+ <th>Alpha emission</th>
474
+ <th>Tao emission</th>
475
+ <th>Registered</th>
476
+ <th>Actions</th>
477
+ </tr>
478
+ </thead>
479
+ <tbody id="stakes-table-body">
480
+ </tbody>
481
+ </table>
482
+ </div>
483
+ </div>
484
+ </div>
485
+ """
486
+
487
+
488
+ def generate_neuron_details() -> str:
489
+ """
490
+ Generates the neuron detail container, which is hidden by default.
491
+ This section shows detailed information for a selected neuron.
492
+ """
493
+ return """
494
+ <div id="neuron-detail-container" style="display: none;">
495
+ <div class="neuron-detail-header">
496
+ <button class="back-button neuron-detail-back" onclick="closeNeuronDetails()">&larr; Back</button>
497
+ </div>
498
+ <div class="neuron-detail-content">
499
+ <div class="neuron-info-top">
500
+ <h2 class="neuron-name" id="neuron-name"></h2>
501
+ <div class="neuron-keys">
502
+ <div class="hotkey-label">
503
+ <span style="color: #FF9900;">Hotkey:</span>
504
+ <span id="neuron-hotkey" class="truncated-address"></span>
505
+ </div>
506
+ <div class="coldkey-label">
507
+ <span style="color: #FF9900;">Coldkey:</span>
508
+ <span id="neuron-coldkey" class="truncated-address"></span>
509
+ </div>
510
+ </div>
511
+ </div>
512
+ <div class="neuron-cards-container">
513
+ <!-- First row: Stakes, Dividends, Incentive, Emissions -->
514
+ <div class="neuron-metrics-row">
515
+ <div class="metric-card">
516
+ <div class="metric-label">Stake Weight</div>
517
+ <div id="neuron-stake-total" class="metric-value formatted-number"
518
+ data-value="0" data-symbol=""></div>
519
+ </div>
520
+
521
+ <div class="metric-card">
522
+ <div class="metric-label">Stake (Alpha)</div>
523
+ <div id="neuron-stake-token" class="metric-value formatted-number"
524
+ data-value="0" data-symbol=""></div>
525
+ </div>
526
+
527
+ <div class="metric-card">
528
+ <div class="metric-label">Stake (Root)</div>
529
+ <div id="neuron-stake-root" class="metric-value formatted-number"
530
+ data-value="0" data-symbol="&#x03C4;"></div>
531
+ </div>
532
+
533
+ <div class="metric-card">
534
+ <div class="metric-label">Dividends</div>
535
+ <div id="neuron-dividends" class="metric-value formatted-number"
536
+ data-value="0" data-symbol=""></div>
537
+ </div>
538
+
539
+ <div class="metric-card">
540
+ <div class="metric-label">Incentive</div>
541
+ <div id="neuron-incentive" class="metric-value formatted-number"
542
+ data-value="0" data-symbol=""></div>
543
+ </div>
544
+
545
+ <div class="metric-card">
546
+ <div class="metric-label">Emissions</div>
547
+ <div id="neuron-emissions" class="metric-value formatted-number"
548
+ data-value="0" data-symbol=""></div>
549
+ </div>
550
+ </div>
551
+
552
+ <!-- Second row: Rank, Trust, Pruning Score, Validator Permit, Consensus, Last Update -->
553
+ <div class="neuron-metrics-row">
554
+ <div class="metric-card">
555
+ <div class="metric-label">Rank</div>
556
+ <div id="neuron-rank" class="metric-value"></div>
557
+ </div>
558
+
559
+ <div class="metric-card">
560
+ <div class="metric-label">Trust</div>
561
+ <div id="neuron-trust" class="metric-value"></div>
562
+ </div>
563
+
564
+ <div class="metric-card">
565
+ <div class="metric-label">Pruning Score</div>
566
+ <div id="neuron-pruning-score" class="metric-value"></div>
567
+ </div>
568
+
569
+ <div class="metric-card">
570
+ <div class="metric-label">Validator Permit</div>
571
+ <div id="neuron-validator-permit" class="metric-value"></div>
572
+ </div>
573
+
574
+ <div class="metric-card">
575
+ <div class="metric-label">Consensus</div>
576
+ <div id="neuron-consensus" class="metric-value"></div>
577
+ </div>
578
+
579
+ <div class="metric-card">
580
+ <div class="metric-label">Last Update</div>
581
+ <div id="neuron-last-update" class="metric-value"></div>
582
+ </div>
583
+ </div>
584
+
585
+ <!-- Third row: Reg Block, IP Info, Active -->
586
+ <div class="neuron-metrics-row last-row">
587
+ <div class="metric-card">
588
+ <div class="metric-label">Reg Block</div>
589
+ <div id="neuron-reg-block" class="metric-value"></div>
590
+ </div>
591
+
592
+ <div class="metric-card">
593
+ <div class="metric-label">IP Info</div>
594
+ <div id="neuron-ipinfo" class="metric-value"></div>
595
+ </div>
596
+
597
+ <div class="metric-card">
598
+ <div class="metric-label">Active</div>
599
+ <div id="neuron-active" class="metric-value"></div>
600
+ </div>
601
+ </div>
602
+ </div>
603
+ </div>
604
+ </div>
605
+ """
606
+
607
+
608
+ def generate_main_header(wallet_info: Dict[str, Any], block_number: int) -> str:
609
+ truncated_coldkey = f"{wallet_info['coldkey'][:6]}...{wallet_info['coldkey'][-6:]}"
610
+
611
+ # Calculate slippage percentage
612
+ ideal_value = wallet_info["total_ideal_stake_value"]
613
+ slippage_value = wallet_info["total_slippage_value"]
614
+ slippage_percentage = (
615
+ ((ideal_value - slippage_value) / ideal_value * 100) if ideal_value > 0 else 0
616
+ )
617
+
618
+ return f"""
619
+ <div class="header">
620
+ <div class="wallet-info">
621
+ <span class="wallet-name">{wallet_info["name"]}</span>
622
+ <div class="wallet-address-container" onclick="copyToClipboard('{wallet_info["coldkey"]}', this)">
623
+ <span class="wallet-address" title="Click to copy">{truncated_coldkey}</span>
624
+ <span class="copy-indicator">Copy</span>
625
+ </div>
626
+ </div>
627
+ <div class="stake-metrics">
628
+ <div class="stake-metric">
629
+ <span class="metric-label">Block</span>
630
+ <span class="metric-value" style="color: #FF9900;">{block_number}</span>
631
+ </div>
632
+ <div class="stake-metric">
633
+ <span class="metric-label">Balance</span>
634
+ <span class="metric-value">{wallet_info["balance"]:.4f} {root_symbol_html}</span>
635
+ </div>
636
+ <div class="stake-metric">
637
+ <span class="metric-label">Total Stake Value</span>
638
+ <span class="metric-value">{wallet_info["total_ideal_stake_value"]:.4f} {root_symbol_html}</span>
639
+ </div>
640
+ <div class="stake-metric">
641
+ <span class="metric-label">Slippage Impact</span>
642
+ <span class="metric-value slippage-value">
643
+ {slippage_percentage:.2f}% <span class="slippage-detail">({wallet_info["total_slippage_value"]:.4f} {root_symbol_html})</span>
644
+ </span>
645
+ </div>
646
+ </div>
647
+ </div>
648
+ """
649
+
650
+
651
+ def generate_main_filters() -> str:
652
+ return """
653
+ <div class="filters-section">
654
+ <div class="search-box">
655
+ <input type="text" id="subnet-search" placeholder="search for name, or netuid..." onkeyup="filterSubnets()">
656
+ </div>
657
+ <div class="filter-toggles">
658
+ <label>
659
+ <input type="checkbox" id="show-verbose" onchange="toggleVerboseNumbers()">
660
+ Precise Numbers
661
+ </label>
662
+ <label>
663
+ <input type="checkbox" id="show-staked" onchange="filterSubnets()">
664
+ Show Only Staked
665
+ </label>
666
+ <label>
667
+ <input type="checkbox" id="show-tiles" onchange="toggleTileView()" checked>
668
+ Tile View
669
+ </label>
670
+ <label class="disabled-label" title="Coming soon">
671
+ <input type="checkbox" id="live-mode" disabled>
672
+ Live Mode (coming soon)
673
+ </label>
674
+ </div>
675
+ </div>
676
+ <div id="subnet-tiles-container" class="subnet-tiles-container"></div>
677
+ """
678
+
679
+
680
+ def generate_subnets_table(subnets: List[Dict[str, Any]]) -> str:
681
+ rows = []
682
+ for subnet in subnets:
683
+ total_your_stake = sum(stake["amount"] for stake in subnet["your_stakes"])
684
+ stake_status = (
685
+ '<span class="stake-status staked">Staked</span>'
686
+ if total_your_stake > 0
687
+ else '<span class="stake-status unstaked">Not Staked</span>'
688
+ )
689
+ rows.append(f"""
690
+ <tr class="subnet-row" onclick="showSubnetPage({subnet["netuid"]})">
691
+ <td class="subnet-name" data-value="{subnet["netuid"]}"><span style="color: #FF9900">{subnet["netuid"]}</span> - {subnet["name"]}</td>
692
+ <td class="price" data-value="{subnet["price"]}"><span class="formatted-number" data-value="{subnet["price"]}" data-symbol="{subnet["symbol"]}"></span></td>
693
+ <td class="market-cap" data-value="{subnet["market_cap"]}"><span class="formatted-number" data-value="{subnet["market_cap"]}" data-symbol="{root_symbol_html}"></span></td>
694
+ <td class="your-stake" data-value="{total_your_stake}"><span class="formatted-number" data-value="{total_your_stake}" data-symbol="{subnet["symbol"]}"></span></td>
695
+ <td class="emission" data-value="{subnet["emission"]}"><span class="formatted-number" data-value="{subnet["emission"]}" data-symbol="{root_symbol_html}"></span></td>
696
+ <td class="stake-status-cell">{stake_status}</td>
697
+ </tr>
698
+ """)
699
+ return f"""
700
+ <div class="subnets-table-container">
701
+ <table class="subnets-table">
702
+ <thead>
703
+ <tr>
704
+ <th class="sortable" onclick="sortMainTable(0)">Subnet</th>
705
+ <th class="sortable" onclick="sortMainTable(1)">Price</th>
706
+ <th class="sortable" onclick="sortMainTable(2)" data-sort="desc">Market Cap</th>
707
+ <th class="sortable" onclick="sortMainTable(3)">Your Stake</th>
708
+ <th class="sortable" onclick="sortMainTable(4)">Emission</th>
709
+ <th>Status</th>
710
+ </tr>
711
+ </thead>
712
+ <tbody>
713
+ {"".join(rows)}
714
+ </tbody>
715
+ </table>
716
+ </div>
717
+ """
718
+
719
+
720
+ def generate_subnet_details_html() -> str:
721
+ return """
722
+ <div id="subnet-modal" class="modal hidden">
723
+ <div class="modal-content">
724
+ <div class="modal-header">
725
+ <h2 class="subnet-title"></h2>
726
+ <button class="close-button" onclick="closeSubnetModal()">&times;</button>
727
+ </div>
728
+ <div class="subnet-overview">
729
+ <div class="overview-item">
730
+ <span class="label">Price</span>
731
+ <span class="value price"></span>
732
+ </div>
733
+ <div class="overview-item">
734
+ <span class="label">Market Cap</span>
735
+ <span class="value market-cap"></span>
736
+ </div>
737
+ <div class="overview-item">
738
+ <span class="label">Emission Rate</span>
739
+ <span class="value emission"></span>
740
+ </div>
741
+ <div class="overview-item">
742
+ <span class="label">Your Total Stake</span>
743
+ <span class="value total-stake"></span>
744
+ </div>
745
+ </div>
746
+ <div class="stakes-section">
747
+ <h3>Your Stakes</h3>
748
+ <div class="stakes-list"></div>
749
+ </div>
750
+ </div>
751
+ </div>
752
+ """
753
+
754
+
755
+ def get_css_styles() -> str:
756
+ """Get CSS styles for the interface."""
757
+ return """
758
+ /* ===================== Base Styles & Typography ===================== */
759
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Noto+Sans:wght@400;500;600&display=swap');
760
+
761
+ body {
762
+ font-family: 'Inter', 'Noto Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Arial Unicode MS', sans-serif;
763
+ margin: 0;
764
+ padding: 24px;
765
+ background: #000000;
766
+ color: #ffffff;
767
+ }
768
+
769
+ input, button, select {
770
+ font-family: inherit;
771
+ font-feature-settings: normal;
772
+ }
773
+
774
+ /* ===================== Main Page Header ===================== */
775
+ .header {
776
+ display: flex;
777
+ justify-content: space-between;
778
+ align-items: center;
779
+ padding: 16px 24px;
780
+ background: rgba(255, 255, 255, 0.05);
781
+ border-radius: 12px;
782
+ margin-bottom: 24px;
783
+ backdrop-filter: blur(10px);
784
+ }
785
+
786
+ .wallet-info {
787
+ display: flex;
788
+ flex-direction: column;
789
+ gap: 4px;
790
+ }
791
+
792
+ .wallet-name {
793
+ font-size: 1.1em;
794
+ font-weight: 500;
795
+ color: #FF9900;
796
+ }
797
+
798
+ .wallet-address-container {
799
+ position: relative;
800
+ cursor: pointer;
801
+ display: inline-flex;
802
+ align-items: center;
803
+ gap: 8px;
804
+ }
805
+
806
+ .wallet-address {
807
+ font-size: 0.9em;
808
+ color: rgba(255, 255, 255, 0.5);
809
+ font-family: monospace;
810
+ transition: color 0.2s ease;
811
+ }
812
+
813
+ .wallet-address-container:hover .wallet-address {
814
+ color: rgba(255, 255, 255, 0.8);
815
+ }
816
+
817
+ .copy-indicator {
818
+ background: rgba(255, 153, 0, 0.1);
819
+ color: rgba(255, 153, 0, 0.8);
820
+ padding: 2px 6px;
821
+ border-radius: 4px;
822
+ font-size: 0.7em;
823
+ transition: all 0.2s ease;
824
+ opacity: 0;
825
+ }
826
+
827
+ .wallet-address-container:hover .copy-indicator {
828
+ opacity: 1;
829
+ background: rgba(255, 153, 0, 0.2);
830
+ }
831
+
832
+ .wallet-address-container.copied .copy-indicator {
833
+ opacity: 1;
834
+ background: rgba(255, 153, 0, 0.3);
835
+ color: #FF9900;
836
+ }
837
+
838
+ .stake-metrics {
839
+ display: flex;
840
+ gap: 24px;
841
+ align-items: center;
842
+ }
843
+
844
+ .stake-metric {
845
+ display: flex;
846
+ flex-direction: column;
847
+ align-items: flex-end;
848
+ gap: 2px;
849
+ position: relative;
850
+ padding: 8px 16px;
851
+ border-radius: 8px;
852
+ transition: all 0.2s ease;
853
+ }
854
+
855
+ .stake-metric:hover {
856
+ background: rgba(255, 153, 0, 0.05);
857
+ }
858
+
859
+ .stake-metric .metric-label {
860
+ font-size: 0.8em;
861
+ color: rgba(255, 255, 255, 0.6);
862
+ text-transform: uppercase;
863
+ letter-spacing: 0.5px;
864
+ }
865
+
866
+ .stake-metric .metric-value {
867
+ font-size: 1.1em;
868
+ font-weight: 500;
869
+ color: #FF9900;
870
+ font-feature-settings: "tnum";
871
+ font-variant-numeric: tabular-nums;
872
+ }
873
+
874
+ .slippage-value {
875
+ display: flex;
876
+ align-items: center;
877
+ gap: 6px;
878
+ }
879
+
880
+ .slippage-detail {
881
+ font-size: 0.8em;
882
+ color: rgba(255, 255, 255, 0.5);
883
+ }
884
+
885
+ /* ===================== Main Page Filters ===================== */
886
+ .filters-section {
887
+ display: flex;
888
+ justify-content: space-between;
889
+ align-items: center;
890
+ margin: 24px 0;
891
+ gap: 16px;
892
+ }
893
+
894
+ .search-box input {
895
+ padding: 10px 16px;
896
+ border-radius: 8px;
897
+ border: 1px solid rgba(255, 255, 255, 0.1);
898
+ background: rgba(255, 255, 255, 0.03);
899
+ color: rgba(255, 255, 255, 0.7);
900
+ width: 240px;
901
+ font-size: 0.9em;
902
+ transition: all 0.2s ease;
903
+ }
904
+ .search-box input::placeholder {
905
+ color: rgba(255, 255, 255, 0.4);
906
+ }
907
+
908
+ .search-box input:focus {
909
+ outline: none;
910
+ border-color: rgba(255, 153, 0, 0.5);
911
+ background: rgba(255, 255, 255, 0.06);
912
+ color: rgba(255, 255, 255, 0.9);
913
+ }
914
+
915
+ .filter-toggles {
916
+ display: flex;
917
+ gap: 16px;
918
+ }
919
+
920
+ .filter-toggles label {
921
+ display: flex;
922
+ align-items: center;
923
+ gap: 8px;
924
+ color: rgba(255, 255, 255, 0.7);
925
+ font-size: 0.9em;
926
+ cursor: pointer;
927
+ user-select: none;
928
+ }
929
+
930
+ /* Checkbox styling for both main page and subnet page */
931
+ .filter-toggles input[type="checkbox"],
932
+ .toggle-label input[type="checkbox"] {
933
+ -webkit-appearance: none;
934
+ -moz-appearance: none;
935
+ appearance: none;
936
+ width: 18px;
937
+ height: 18px;
938
+ border: 2px solid rgba(255, 153, 0, 0.3);
939
+ border-radius: 4px;
940
+ background: rgba(0, 0, 0, 0.2);
941
+ cursor: pointer;
942
+ position: relative;
943
+ transition: all 0.2s ease;
944
+ }
945
+
946
+ .filter-toggles input[type="checkbox"]:hover,
947
+ .toggle-label input[type="checkbox"]:hover {
948
+ border-color: #FF9900;
949
+ }
950
+
951
+ .filter-toggles input[type="checkbox"]:checked,
952
+ .toggle-label input[type="checkbox"]:checked {
953
+ background: #FF9900;
954
+ border-color: #FF9900;
955
+ }
956
+
957
+ .filter-toggles input[type="checkbox"]:checked::after,
958
+ .toggle-label input[type="checkbox"]:checked::after {
959
+ content: '';
960
+ position: absolute;
961
+ left: 5px;
962
+ top: 2px;
963
+ width: 4px;
964
+ height: 8px;
965
+ border: solid #000;
966
+ border-width: 0 2px 2px 0;
967
+ transform: rotate(45deg);
968
+ }
969
+
970
+ .filter-toggles label:hover,
971
+ .toggle-label:hover {
972
+ color: rgba(255, 255, 255, 0.9);
973
+ }
974
+ .disabled-label {
975
+ opacity: 0.5;
976
+ cursor: not-allowed;
977
+ }
978
+ .add-stake-button {
979
+ padding: 10px 20px;
980
+ font-size: 0.8rem;
981
+ }
982
+ .export-csv-button {
983
+ padding: 10px 20px;
984
+ font-size: 0.8rem;
985
+ }
986
+ .button-group {
987
+ display: flex;
988
+ gap: 8px;
989
+ }
990
+
991
+ /* ===================== Main Page Subnet Table ===================== */
992
+ .subnets-table-container {
993
+ background: rgba(255, 255, 255, 0.02);
994
+ border-radius: 12px;
995
+ overflow: hidden;
996
+ }
997
+
998
+ .subnets-table {
999
+ width: 100%;
1000
+ border-collapse: collapse;
1001
+ font-size: 0.95em;
1002
+ }
1003
+
1004
+ .subnets-table th {
1005
+ background: rgba(255, 255, 255, 0.05);
1006
+ font-weight: 500;
1007
+ text-align: left;
1008
+ padding: 16px;
1009
+ color: rgba(255, 255, 255, 0.7);
1010
+ }
1011
+
1012
+ .subnets-table td {
1013
+ padding: 14px 16px;
1014
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
1015
+ }
1016
+
1017
+ .subnet-row {
1018
+ cursor: pointer;
1019
+ transition: background-color 0.2s ease;
1020
+ }
1021
+
1022
+ .subnet-row:hover {
1023
+ background: rgba(255, 255, 255, 0.05);
1024
+ }
1025
+
1026
+ .subnet-name {
1027
+ color: #ffffff;
1028
+ font-weight: 500;
1029
+ font-size: 0.95em;
1030
+ }
1031
+
1032
+ .price, .market-cap, .your-stake, .emission {
1033
+ font-family: 'Inter', monospace;
1034
+ font-size: 1.0em;
1035
+ font-feature-settings: "tnum";
1036
+ font-variant-numeric: tabular-nums;
1037
+ letter-spacing: 0.01em;
1038
+ white-space: nowrap;
1039
+ }
1040
+
1041
+ .stake-status {
1042
+ font-size: 0.85em;
1043
+ padding: 4px 8px;
1044
+ border-radius: 4px;
1045
+ background: rgba(255, 255, 255, 0.05);
1046
+ }
1047
+
1048
+ .stake-status.staked {
1049
+ background: rgba(255, 153, 0, 0.1);
1050
+ color: #FF9900;
1051
+ }
1052
+
1053
+ .subnets-table th.sortable {
1054
+ cursor: pointer;
1055
+ position: relative;
1056
+ padding-right: 20px;
1057
+ }
1058
+
1059
+ .subnets-table th.sortable:hover {
1060
+ color: #FF9900;
1061
+ }
1062
+
1063
+ .subnets-table th[data-sort] {
1064
+ color: #FF9900;
1065
+ }
1066
+
1067
+ /* ===================== Subnet Tiles View ===================== */
1068
+ .subnet-tiles-container {
1069
+ display: flex;
1070
+ flex-wrap: wrap;
1071
+ justify-content: center;
1072
+ gap: 1rem;
1073
+ padding: 1rem;
1074
+ }
1075
+
1076
+ .subnet-tile {
1077
+ width: clamp(75px, 6vw, 600px);
1078
+ height: clamp(75px, 6vw, 600px);
1079
+ display: flex;
1080
+ flex-direction: column;
1081
+ align-items: center;
1082
+ justify-content: center;
1083
+ background: rgba(255, 255, 255, 0.05);
1084
+ border-radius: 8px;
1085
+ position: relative;
1086
+ cursor: pointer;
1087
+ transition: all 0.2s ease;
1088
+ overflow: hidden;
1089
+ font-size: clamp(0.6rem, 1vw, 1.4rem);
1090
+ }
1091
+
1092
+ .tile-netuid {
1093
+ position: absolute;
1094
+ top: 0.4em;
1095
+ left: 0.4em;
1096
+ font-size: 0.7em;
1097
+ color: rgba(255, 255, 255, 0.6);
1098
+ }
1099
+
1100
+ .tile-symbol {
1101
+ font-size: 1.6em;
1102
+ margin-bottom: 0.4em;
1103
+ color: #FF9900;
1104
+ }
1105
+
1106
+ .tile-name {
1107
+ display: block;
1108
+ width: 100%;
1109
+ white-space: nowrap;
1110
+ overflow: hidden;
1111
+ text-overflow: ellipsis;
1112
+ font-size: 1em;
1113
+ text-align: center;
1114
+ color: rgba(255, 255, 255, 0.9);
1115
+ margin: 0 0.4em;
1116
+ }
1117
+
1118
+ .tile-market-cap {
1119
+ font-size: 0.9em;
1120
+ color: rgba(255, 255, 255, 0.5);
1121
+ margin-top: 2px;
1122
+ }
1123
+
1124
+ .subnet-tile:hover {
1125
+ transform: translateY(-2px);
1126
+ box-shadow:
1127
+ 0 0 12px rgba(255, 153, 0, 0.6),
1128
+ 0 0 24px rgba(255, 153, 0, 0.3);
1129
+ background: rgba(255, 255, 255, 0.08);
1130
+ }
1131
+
1132
+ .subnet-tile.staked {
1133
+ border: 1px solid rgba(255, 153, 0, 0.3);
1134
+ }
1135
+
1136
+ .subnet-tile.staked::before {
1137
+ content: '';
1138
+ position: absolute;
1139
+ top: 0.4em;
1140
+ right: 0.4em;
1141
+ width: 0.5em;
1142
+ height: 0.5em;
1143
+ border-radius: 50%;
1144
+ background: #FF9900;
1145
+ }
1146
+
1147
+ /* ===================== Subnet Detail Page Header ===================== */
1148
+ .subnet-header {
1149
+ padding: 16px;
1150
+ border-radius: 12px;
1151
+ margin-bottom: 0px;
1152
+ }
1153
+
1154
+ .subnet-header h2 {
1155
+ margin: 0;
1156
+ font-size: 1.3em;
1157
+ }
1158
+
1159
+ .subnet-price {
1160
+ font-size: 1.3em;
1161
+ color: #FF9900;
1162
+ }
1163
+
1164
+ .subnet-title-row {
1165
+ display: grid;
1166
+ grid-template-columns: 300px 1fr 300px;
1167
+ align-items: start;
1168
+ margin: 0;
1169
+ position: relative;
1170
+ min-height: 60px;
1171
+ }
1172
+
1173
+ .title-price {
1174
+ grid-column: 1;
1175
+ padding-top: 0;
1176
+ margin-top: -10px;
1177
+ }
1178
+
1179
+ .header-row {
1180
+ display: flex;
1181
+ justify-content: space-between;
1182
+ align-items: center;
1183
+ width: 100%;
1184
+ margin-bottom: 16px;
1185
+ }
1186
+
1187
+ .toggle-group {
1188
+ display: flex;
1189
+ flex-direction: column;
1190
+ gap: 8px;
1191
+ align-items: flex-end;
1192
+ }
1193
+
1194
+ .toggle-label {
1195
+ display: flex;
1196
+ align-items: center;
1197
+ gap: 8px;
1198
+ color: rgba(255, 255, 255, 0.7);
1199
+ font-size: 0.9em;
1200
+ cursor: pointer;
1201
+ user-select: none;
1202
+ }
1203
+
1204
+ .back-button {
1205
+ background: rgba(255, 255, 255, 0.05);
1206
+ border: 1px solid rgba(255, 255, 255, 0.1);
1207
+ color: rgba(255, 255, 255, 0.8);
1208
+ padding: 8px 16px;
1209
+ border-radius: 8px;
1210
+ cursor: pointer;
1211
+ font-size: 0.9em;
1212
+ transition: all 0.2s ease;
1213
+ margin-bottom: 16px;
1214
+ }
1215
+
1216
+ .back-button:hover {
1217
+ background: rgba(255, 255, 255, 0.1);
1218
+ border-color: rgba(255, 255, 255, 0.2);
1219
+ }
1220
+
1221
+ /* ===================== Network Visualization ===================== */
1222
+ .network-visualization-container {
1223
+ position: absolute;
1224
+ left: 50%;
1225
+ transform: translateX(-50%);
1226
+ top: -50px;
1227
+ width: 700px;
1228
+ height: 80px;
1229
+ z-index: 1;
1230
+ }
1231
+
1232
+ .network-visualization {
1233
+ width: 700px;
1234
+ height: 80px;
1235
+ position: relative;
1236
+ }
1237
+
1238
+ #network-canvas {
1239
+ background: transparent;
1240
+ position: relative;
1241
+ z-index: 1;
1242
+ }
1243
+
1244
+ /* Gradient behind visualization */
1245
+ .network-visualization::after {
1246
+ content: '';
1247
+ position: absolute;
1248
+ top: 0;
1249
+ left: 0;
1250
+ right: 0;
1251
+ height: 100%;
1252
+ background: linear-gradient(to bottom, rgba(0, 0, 0, 0.95) 0%, rgba(0, 0, 0, 0.8) 100%);
1253
+ z-index: 0;
1254
+ pointer-events: none;
1255
+ }
1256
+
1257
+ /* ===================== Subnet Detail Metrics ===================== */
1258
+ .network-metrics {
1259
+ display: grid;
1260
+ grid-template-columns: repeat(5, 1fr);
1261
+ gap: 12px;
1262
+ margin: 0;
1263
+ margin-top: 16px;
1264
+ }
1265
+
1266
+ /* Base card styles - applied to both network and metric cards */
1267
+ .network-card, .metric-card {
1268
+ background: rgba(255, 255, 255, 0.05);
1269
+ border-radius: 8px;
1270
+ padding: 12px 16px;
1271
+ min-height: 50px;
1272
+ display: flex;
1273
+ flex-direction: column;
1274
+ justify-content: center;
1275
+ gap: 4px;
1276
+ }
1277
+
1278
+ /* Separate styling for moving price value */
1279
+ #network-moving-price {
1280
+ color: #FF9900;
1281
+ }
1282
+
1283
+ .metrics-section {
1284
+ margin-top: 0px;
1285
+ margin-bottom: 16px;
1286
+ }
1287
+
1288
+ .metrics-group {
1289
+ display: grid;
1290
+ grid-template-columns: repeat(5, 1fr);
1291
+ gap: 12px;
1292
+ margin: 0;
1293
+ margin-top: 2px;
1294
+ }
1295
+
1296
+ .market-metrics .metric-card {
1297
+ background: rgba(255, 255, 255, 0.05);
1298
+ min-height: 70px;
1299
+ }
1300
+
1301
+ .metric-label {
1302
+ font-size: 0.85em;
1303
+ color: rgba(255, 255, 255, 0.7);
1304
+ margin: 0;
1305
+ }
1306
+
1307
+ .metric-value {
1308
+ font-size: 1.2em;
1309
+ line-height: 1.3;
1310
+ margin: 0;
1311
+ }
1312
+
1313
+ /* Add status colors */
1314
+ .registration-status {
1315
+ color: #2ECC71;
1316
+ }
1317
+
1318
+ .registration-status.closed {
1319
+ color: #ff4444; /* Red color for closed status */
1320
+ }
1321
+
1322
+ .cr-status {
1323
+ color: #2ECC71;
1324
+ }
1325
+
1326
+ .cr-status.disabled {
1327
+ color: #ff4444; /* Red color for disabled status */
1328
+ }
1329
+
1330
+ /* ===================== Stakes Table ===================== */
1331
+ .stakes-container {
1332
+ margin-top: 24px;
1333
+ padding: 0 24px;
1334
+ }
1335
+
1336
+ .stakes-header {
1337
+ display: flex;
1338
+ justify-content: space-between;
1339
+ align-items: center;
1340
+ margin-bottom: 16px;
1341
+ }
1342
+
1343
+ .stakes-header h3 {
1344
+ font-size: 1.2em;
1345
+ color: #ffffff;
1346
+ margin: 0;
1347
+ }
1348
+
1349
+ .stakes-table-container {
1350
+ background: rgba(255, 255, 255, 0.02);
1351
+ border-radius: 12px;
1352
+ overflow: hidden;
1353
+ margin-bottom: 24px;
1354
+ width: 100%;
1355
+ }
1356
+
1357
+ .stakes-table {
1358
+ width: 100%;
1359
+ border-collapse: collapse;
1360
+ }
1361
+
1362
+ .stakes-table th {
1363
+ background: rgba(255, 255, 255, 0.05);
1364
+ padding: 16px;
1365
+ text-align: left;
1366
+ font-weight: 500;
1367
+ color: rgba(255, 255, 255, 0.7);
1368
+ }
1369
+
1370
+ .stakes-table td {
1371
+ padding: 16px;
1372
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
1373
+ }
1374
+
1375
+ .stakes-table tr {
1376
+ transition: background-color 0.2s ease;
1377
+ }
1378
+
1379
+ .stakes-table tr:nth-child(even) {
1380
+ background: rgba(255, 255, 255, 0.02);
1381
+ }
1382
+
1383
+ .stakes-table tr:hover {
1384
+ background: transparent;
1385
+ }
1386
+
1387
+ .no-stakes-row td {
1388
+ text-align: center;
1389
+ padding: 32px;
1390
+ color: rgba(255, 255, 255, 0.5);
1391
+ }
1392
+
1393
+ /* Table styles consistency */
1394
+ .stakes-table th, .network-table th {
1395
+ background: rgba(255, 255, 255, 0.05);
1396
+ padding: 16px;
1397
+ text-align: left;
1398
+ font-weight: 500;
1399
+ color: rgba(255, 255, 255, 0.7);
1400
+ transition: color 0.2s ease;
1401
+ }
1402
+
1403
+ /* Sortable columns */
1404
+ .stakes-table th.sortable, .network-table th.sortable {
1405
+ cursor: pointer;
1406
+ }
1407
+
1408
+ /* Active sort column - only change color */
1409
+ .stakes-table th.sortable[data-sort], .network-table th.sortable[data-sort] {
1410
+ color: #FF9900;
1411
+ }
1412
+
1413
+ /* Hover effects - only change color */
1414
+ .stakes-table th.sortable:hover, .network-table th.sortable:hover {
1415
+ color: #FF9900;
1416
+ }
1417
+
1418
+ /* Remove hover background from table rows */
1419
+ .stakes-table tr:hover {
1420
+ background: transparent;
1421
+ }
1422
+
1423
+ /* ===================== Network Table ===================== */
1424
+ .network-table-container {
1425
+ margin-top: 60px;
1426
+ position: relative;
1427
+ z-index: 2;
1428
+ background: rgba(0, 0, 0, 0.8);
1429
+ }
1430
+
1431
+ .network-table {
1432
+ width: 100%;
1433
+ border-collapse: collapse;
1434
+ table-layout: fixed;
1435
+ }
1436
+
1437
+ .network-table th {
1438
+ background: rgba(255, 255, 255, 0.05);
1439
+ padding: 16px;
1440
+ text-align: left;
1441
+ font-weight: 500;
1442
+ color: rgba(255, 255, 255, 0.7);
1443
+ }
1444
+
1445
+ .network-table td {
1446
+ padding: 16px;
1447
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
1448
+ }
1449
+
1450
+ .network-table tr {
1451
+ cursor: pointer;
1452
+ transition: background-color 0.2s ease;
1453
+ }
1454
+
1455
+ .network-table tr:hover {
1456
+ background-color: rgba(255, 255, 255, 0.05);
1457
+ }
1458
+
1459
+ .network-table tr:nth-child(even) {
1460
+ background-color: rgba(255, 255, 255, 0.02);
1461
+ }
1462
+
1463
+ .network-table tr:nth-child(even):hover {
1464
+ background-color: rgba(255, 255, 255, 0.05);
1465
+ }
1466
+
1467
+ .network-search-container {
1468
+ display: flex;
1469
+ align-items: center;
1470
+ margin-bottom: 16px;
1471
+ padding: 0 16px;
1472
+ }
1473
+
1474
+ .network-search {
1475
+ width: 100%;
1476
+ padding: 12px 16px;
1477
+ border: 1px solid rgba(255, 153, 0, 0.2);
1478
+ border-radius: 8px;
1479
+ background: rgba(0, 0, 0, 0.2);
1480
+ color: #ffffff;
1481
+ font-size: 0.95em;
1482
+ transition: all 0.2s ease;
1483
+ }
1484
+
1485
+ .network-search:focus {
1486
+ outline: none;
1487
+ border-color: rgba(255, 153, 0, 0.5);
1488
+ background: rgba(0, 0, 0, 0.3);
1489
+ caret-color: #FF9900;
1490
+ }
1491
+
1492
+ .network-search::placeholder {
1493
+ color: rgba(255, 255, 255, 0.3);
1494
+ }
1495
+
1496
+ /* ===================== Cell Styles & Formatting ===================== */
1497
+ .hotkey-cell {
1498
+ max-width: 200px;
1499
+ position: relative;
1500
+ }
1501
+
1502
+ .hotkey-container {
1503
+ position: relative;
1504
+ display: inline-block;
1505
+ max-width: 100%;
1506
+ }
1507
+
1508
+ .hotkey-identity, .truncated-address {
1509
+ color: rgba(255, 255, 255, 0.8);
1510
+ display: inline-block;
1511
+ max-width: 100%;
1512
+ overflow: hidden;
1513
+ text-overflow: ellipsis;
1514
+ }
1515
+
1516
+ .copy-button {
1517
+ position: absolute;
1518
+ top: -20px; /* Position above the text */
1519
+ right: 0;
1520
+ background: rgba(255, 153, 0, 0.1);
1521
+ color: rgba(255, 255, 255, 0.6);
1522
+ padding: 2px 6px;
1523
+ border-radius: 4px;
1524
+ font-size: 0.7em;
1525
+ cursor: pointer;
1526
+ opacity: 0;
1527
+ transition: all 0.2s ease;
1528
+ transform: translateY(5px);
1529
+ }
1530
+
1531
+ .hotkey-container:hover .copy-button {
1532
+ opacity: 1;
1533
+ transform: translateY(0);
1534
+ }
1535
+
1536
+ .copy-button:hover {
1537
+ background: rgba(255, 153, 0, 0.2);
1538
+ color: #FF9900;
1539
+ }
1540
+
1541
+ .address-cell {
1542
+ max-width: 150px;
1543
+ position: relative;
1544
+ white-space: nowrap;
1545
+ overflow: hidden;
1546
+ text-overflow: ellipsis;
1547
+ }
1548
+
1549
+ .address-container {
1550
+ display: flex;
1551
+ align-items: center;
1552
+ cursor: pointer;
1553
+ position: relative;
1554
+ }
1555
+
1556
+ .address-container:hover::after {
1557
+ content: 'Click to copy';
1558
+ position: absolute;
1559
+ right: 0;
1560
+ top: 50%;
1561
+ transform: translateY(-50%);
1562
+ background: rgba(255, 153, 0, 0.1);
1563
+ color: #FF9900;
1564
+ padding: 2px 6px;
1565
+ border-radius: 4px;
1566
+ font-size: 0.8em;
1567
+ opacity: 0.8;
1568
+ }
1569
+
1570
+ .truncated-address {
1571
+ font-family: monospace;
1572
+ color: rgba(255, 255, 255, 0.8);
1573
+ overflow: hidden;
1574
+ text-overflow: ellipsis;
1575
+ }
1576
+
1577
+ .truncated-address:hover {
1578
+ color: #FF9900;
1579
+ }
1580
+
1581
+ .registered-yes {
1582
+ color: #FF9900;
1583
+ font-weight: 500;
1584
+ display: flex;
1585
+ align-items: center;
1586
+ gap: 4px;
1587
+ }
1588
+
1589
+ .registered-no {
1590
+ color: #ff4444;
1591
+ font-weight: 500;
1592
+ display: flex;
1593
+ align-items: center;
1594
+ gap: 4px;
1595
+ }
1596
+
1597
+ .manage-button {
1598
+ background: rgba(255, 153, 0, 0.1);
1599
+ border: 1px solid rgba(255, 153, 0, 0.2);
1600
+ color: #FF9900;
1601
+ padding: 6px 12px;
1602
+ border-radius: 6px;
1603
+ cursor: pointer;
1604
+ transition: all 0.2s ease;
1605
+ }
1606
+
1607
+ .manage-button:hover {
1608
+ background: rgba(255, 153, 0, 0.2);
1609
+ transform: translateY(-1px);
1610
+ }
1611
+
1612
+ .hotkey-identity {
1613
+ display: inline-block;
1614
+ max-width: 100%;
1615
+ overflow: hidden;
1616
+ text-overflow: ellipsis;
1617
+ color: #FF9900;
1618
+ }
1619
+
1620
+ .identity-cell {
1621
+ max-width: 700px;
1622
+ font-size: 0.90em;
1623
+ letter-spacing: -0.2px;
1624
+ color: #FF9900;
1625
+ }
1626
+
1627
+ .per-day {
1628
+ font-size: 0.75em;
1629
+ opacity: 0.7;
1630
+ margin-left: 4px;
1631
+ }
1632
+
1633
+ /* ===================== Neuron Detail Panel ===================== */
1634
+ #neuron-detail-container {
1635
+ background: rgba(255, 255, 255, 0.02);
1636
+ border-radius: 12px;
1637
+ padding: 16px;
1638
+ margin-top: 16px;
1639
+ }
1640
+
1641
+ .neuron-detail-header {
1642
+ display: flex;
1643
+ align-items: center;
1644
+ gap: 16px;
1645
+ margin-bottom: 16px;
1646
+ }
1647
+
1648
+ .neuron-detail-content {
1649
+ display: flex;
1650
+ flex-direction: column;
1651
+ gap: 16px;
1652
+ }
1653
+
1654
+ .neuron-info-top {
1655
+ display: flex;
1656
+ flex-direction: column;
1657
+ gap: 8px;
1658
+ }
1659
+
1660
+ .neuron-keys {
1661
+ display: flex;
1662
+ flex-direction: column;
1663
+ gap: 4px;
1664
+ font-size: 0.9em;
1665
+ color: rgba(255, 255, 255, 0.6);
1666
+ font-size: 1em;
1667
+ color: rgba(255, 255, 255, 0.7);
1668
+ }
1669
+
1670
+ .neuron-cards-container {
1671
+ display: flex;
1672
+ flex-direction: column;
1673
+ gap: 12px;
1674
+ }
1675
+
1676
+ .neuron-metrics-row {
1677
+ display: grid;
1678
+ grid-template-columns: repeat(6, 1fr);
1679
+ gap: 12px;
1680
+ margin: 0;
1681
+ }
1682
+
1683
+ .neuron-metrics-row.last-row {
1684
+ grid-template-columns: repeat(3, 1fr);
1685
+ }
1686
+
1687
+ /* IP Info styling */
1688
+ #neuron-ipinfo {
1689
+ font-size: 0.85em;
1690
+ line-height: 1.4;
1691
+ white-space: nowrap;
1692
+ }
1693
+
1694
+ #neuron-ipinfo .no-connection {
1695
+ color: #ff4444;
1696
+ font-weight: 500;
1697
+ }
1698
+
1699
+ /* Adjust metric card for IP info to accommodate multiple lines */
1700
+ .neuron-cards-container .metric-card:has(#neuron-ipinfo) {
1701
+ min-height: 85px;
1702
+ }
1703
+
1704
+ /* ===================== Subnet Page Color Overrides ===================== */
1705
+ /* Subnet page specific style */
1706
+ .subnet-page .metric-card-title,
1707
+ .subnet-page .network-card-title {
1708
+ color: rgba(255, 255, 255, 0.7);
1709
+ }
1710
+
1711
+ .subnet-page .metric-card .metric-value,
1712
+ .subnet-page .metric-value {
1713
+ color: white;
1714
+ }
1715
+
1716
+ /* Green values */
1717
+ .subnet-page .validator-true,
1718
+ .subnet-page .active-yes,
1719
+ .subnet-page .registration-open,
1720
+ .subnet-page .cr-enabled,
1721
+ .subnet-page .ip-info {
1722
+ color: #FF9900;
1723
+ }
1724
+
1725
+ /* Red values */
1726
+ .subnet-page .validator-false,
1727
+ .subnet-page .active-no,
1728
+ .subnet-page .registration-closed,
1729
+ .subnet-page .cr-disabled,
1730
+ .subnet-page .ip-na {
1731
+ color: #ff4444;
1732
+ }
1733
+
1734
+ /* Keep symbols green in subnet page */
1735
+ .subnet-page .symbol {
1736
+ color: #FF9900;
1737
+ }
1738
+
1739
+ /* ===================== Responsive Styles ===================== */
1740
+ @media (max-width: 1200px) {
1741
+ .stakes-table {
1742
+ display: block;
1743
+ overflow-x: auto;
1744
+ }
1745
+
1746
+ .network-metrics {
1747
+ grid-template-columns: repeat(3, 1fr);
1748
+ }
1749
+ }
1750
+
1751
+ @media (min-width: 1201px) {
1752
+ .network-metrics {
1753
+ grid-template-columns: repeat(5, 1fr);
1754
+ }
1755
+ }
1756
+ /* ===== Splash Screen ===== */
1757
+ #splash-screen {
1758
+ position: fixed;
1759
+ top: 0;
1760
+ left: 0;
1761
+ width: 100vw;
1762
+ height: 100vh;
1763
+ background: #000000;
1764
+ display: flex;
1765
+ align-items: center;
1766
+ justify-content: center;
1767
+ z-index: 999999;
1768
+ opacity: 1;
1769
+ transition: opacity 1s ease;
1770
+ }
1771
+
1772
+ #splash-screen.fade-out {
1773
+ opacity: 0;
1774
+ }
1775
+
1776
+ .splash-content {
1777
+ text-align: center;
1778
+ color: #FF9900;
1779
+ opacity: 0;
1780
+ animation: fadeIn 1.2s ease forwards;
1781
+ }
1782
+ @keyframes fadeIn {
1783
+ 0% {
1784
+ opacity: 0;
1785
+ transform: scale(0.97);
1786
+ }
1787
+ 100% {
1788
+ opacity: 1;
1789
+ transform: scale(1);
1790
+ }
1791
+ }
1792
+
1793
+ /* Title & text styling */
1794
+ .title-row {
1795
+ display: flex;
1796
+ align-items: baseline;
1797
+ gap: 1rem;
1798
+ }
1799
+
1800
+ .splash-title {
1801
+ font-size: 2.4rem;
1802
+ margin: 0;
1803
+ padding: 0;
1804
+ font-weight: 600;
1805
+ color: #FF9900;
1806
+ }
1807
+
1808
+ .beta-text {
1809
+ font-size: 0.9rem;
1810
+ color: #FF9900;
1811
+ background: rgba(255, 153, 0, 0.1);
1812
+ padding: 2px 6px;
1813
+ border-radius: 4px;
1814
+ font-weight: 500;
1815
+ }
1816
+
1817
+
1818
+ """
1819
+
1820
+
1821
+ def get_javascript() -> str:
1822
+ return """
1823
+ /* ===================== Global Variables ===================== */
1824
+ const root_symbol_html = '&#x03C4;';
1825
+ let verboseNumbers = false;
1826
+
1827
+ /* ===================== Clipboard Functions ===================== */
1828
+ /**
1829
+ * Copies text to clipboard and shows visual feedback
1830
+ * @param {string} text The text to copy
1831
+ * @param {HTMLElement} element Optional element to show feedback on
1832
+ */
1833
+ function copyToClipboard(text, element) {
1834
+ navigator.clipboard.writeText(text)
1835
+ .then(() => {
1836
+ const targetElement = element || (event && event.target);
1837
+
1838
+ if (targetElement) {
1839
+ const copyIndicator = targetElement.querySelector('.copy-indicator');
1840
+
1841
+ if (copyIndicator) {
1842
+ const originalText = copyIndicator.textContent;
1843
+ copyIndicator.textContent = 'Copied!';
1844
+ copyIndicator.style.color = '#FF9900';
1845
+
1846
+ setTimeout(() => {
1847
+ copyIndicator.textContent = originalText;
1848
+ copyIndicator.style.color = '';
1849
+ }, 1000);
1850
+ } else {
1851
+ const originalText = targetElement.textContent;
1852
+ targetElement.textContent = 'Copied!';
1853
+ targetElement.style.color = '#FF9900';
1854
+
1855
+ setTimeout(() => {
1856
+ targetElement.textContent = originalText;
1857
+ targetElement.style.color = '';
1858
+ }, 1000);
1859
+ }
1860
+ }
1861
+ })
1862
+ .catch(err => {
1863
+ console.error('Failed to copy:', err);
1864
+ });
1865
+ }
1866
+
1867
+
1868
+ /* ===================== Initialization and DOMContentLoaded Handler ===================== */
1869
+ document.addEventListener('DOMContentLoaded', function() {
1870
+ try {
1871
+ const initialDataElement = document.getElementById('initial-data');
1872
+ if (!initialDataElement) {
1873
+ throw new Error('Initial data element (#initial-data) not found.');
1874
+ }
1875
+ window.initialData = {
1876
+ wallet_info: JSON.parse(initialDataElement.getAttribute('data-wallet-info')),
1877
+ subnets: JSON.parse(initialDataElement.getAttribute('data-subnets'))
1878
+ };
1879
+ } catch (error) {
1880
+ console.error('Error loading initial data:', error);
1881
+ }
1882
+
1883
+ // Return to the main list of subnets.
1884
+ const backButton = document.querySelector('.back-button');
1885
+ if (backButton) {
1886
+ backButton.addEventListener('click', function() {
1887
+ // First check if neuron details are visible and close them if needed
1888
+ const neuronDetails = document.getElementById('neuron-detail-container');
1889
+ if (neuronDetails && neuronDetails.style.display !== 'none') {
1890
+ closeNeuronDetails();
1891
+ return; // Stop here, don't go back to main page yet
1892
+ }
1893
+
1894
+ // Otherwise go back to main subnet list
1895
+ document.getElementById('main-content').style.display = 'block';
1896
+ document.getElementById('subnet-page').style.display = 'none';
1897
+ });
1898
+ }
1899
+
1900
+
1901
+ // Splash screen logic
1902
+ const splash = document.getElementById('splash-screen');
1903
+ const mainContent = document.getElementById('main-content');
1904
+ mainContent.style.display = 'none';
1905
+
1906
+ setTimeout(() => {
1907
+ splash.classList.add('fade-out');
1908
+ splash.addEventListener('transitionend', () => {
1909
+ splash.style.display = 'none';
1910
+ mainContent.style.display = 'block';
1911
+ }, { once: true });
1912
+ }, 2000);
1913
+
1914
+ initializeFormattedNumbers();
1915
+
1916
+ // Keep main page's "verbose" checkbox and the Subnet page's "verbose" checkbox in sync
1917
+ const mainVerboseCheckbox = document.getElementById('show-verbose');
1918
+ const subnetVerboseCheckbox = document.getElementById('verbose-toggle');
1919
+ if (mainVerboseCheckbox && subnetVerboseCheckbox) {
1920
+ mainVerboseCheckbox.addEventListener('change', function() {
1921
+ subnetVerboseCheckbox.checked = this.checked;
1922
+ toggleVerboseNumbers();
1923
+ });
1924
+ subnetVerboseCheckbox.addEventListener('change', function() {
1925
+ mainVerboseCheckbox.checked = this.checked;
1926
+ toggleVerboseNumbers();
1927
+ });
1928
+ }
1929
+
1930
+ // Initialize tile view as default
1931
+ const tilesContainer = document.getElementById('subnet-tiles-container');
1932
+ const tableContainer = document.querySelector('.subnets-table-container');
1933
+
1934
+ // Generate and show tiles
1935
+ generateSubnetTiles();
1936
+ tilesContainer.style.display = 'flex';
1937
+ tableContainer.style.display = 'none';
1938
+ });
1939
+
1940
+ /* ===================== Main Page Functions ===================== */
1941
+ /**
1942
+ * Sort the main Subnets table by the specified column index.
1943
+ * Toggles ascending/descending on each click.
1944
+ * @param {number} columnIndex Index of the column to sort.
1945
+ */
1946
+ function sortMainTable(columnIndex) {
1947
+ const table = document.querySelector('.subnets-table');
1948
+ const headers = table.querySelectorAll('th');
1949
+ const header = headers[columnIndex];
1950
+
1951
+ // Determine new sort direction
1952
+ let isDescending = header.getAttribute('data-sort') !== 'desc';
1953
+
1954
+ // Clear sort markers on all columns, then set the new one
1955
+ headers.forEach(th => { th.removeAttribute('data-sort'); });
1956
+ header.setAttribute('data-sort', isDescending ? 'desc' : 'asc');
1957
+
1958
+ // Sort rows based on numeric value (or netuid in col 0)
1959
+ const tbody = table.querySelector('tbody');
1960
+ const rows = Array.from(tbody.querySelectorAll('tr'));
1961
+ rows.sort((rowA, rowB) => {
1962
+ const cellA = rowA.cells[columnIndex];
1963
+ const cellB = rowB.cells[columnIndex];
1964
+
1965
+ // Special handling for the first column with netuid in data-value
1966
+ if (columnIndex === 0) {
1967
+ const netuidA = parseInt(cellA.getAttribute('data-value'), 10);
1968
+ const netuidB = parseInt(cellB.getAttribute('data-value'), 10);
1969
+ return isDescending ? (netuidB - netuidA) : (netuidA - netuidB);
1970
+ }
1971
+
1972
+ // Otherwise parse float from data-value
1973
+ const valueA = parseFloat(cellA.getAttribute('data-value')) || 0;
1974
+ const valueB = parseFloat(cellB.getAttribute('data-value')) || 0;
1975
+ return isDescending ? (valueB - valueA) : (valueA - valueB);
1976
+ });
1977
+
1978
+ // Re-inject rows in sorted order
1979
+ tbody.innerHTML = '';
1980
+ rows.forEach(row => tbody.appendChild(row));
1981
+ }
1982
+
1983
+ /**
1984
+ * Filters the main Subnets table rows based on user search and "Show Only Staked" checkbox.
1985
+ */
1986
+ function filterSubnets() {
1987
+ const searchText = document.getElementById('subnet-search').value.toLowerCase();
1988
+ const showStaked = document.getElementById('show-staked').checked;
1989
+ const showTiles = document.getElementById('show-tiles').checked;
1990
+
1991
+ // Filter table rows
1992
+ const rows = document.querySelectorAll('.subnet-row');
1993
+ rows.forEach(row => {
1994
+ const name = row.querySelector('.subnet-name').textContent.toLowerCase();
1995
+ const stakeStatus = row.querySelector('.stake-status').textContent; // "Staked" or "Not Staked"
1996
+
1997
+ let isVisible = name.includes(searchText);
1998
+ if (showStaked) {
1999
+ // If "Show only Staked" is checked, the row must have "Staked" to be visible
2000
+ isVisible = isVisible && (stakeStatus === 'Staked');
2001
+ }
2002
+ row.style.display = isVisible ? '' : 'none';
2003
+ });
2004
+
2005
+ // Filter tiles if they're being shown
2006
+ if (showTiles) {
2007
+ const tiles = document.querySelectorAll('.subnet-tile');
2008
+ tiles.forEach(tile => {
2009
+ const name = tile.querySelector('.tile-name').textContent.toLowerCase();
2010
+ const netuid = tile.querySelector('.tile-netuid').textContent;
2011
+ const isStaked = tile.classList.contains('staked');
2012
+
2013
+ let isVisible = name.includes(searchText) || netuid.includes(searchText);
2014
+ if (showStaked) {
2015
+ isVisible = isVisible && isStaked;
2016
+ }
2017
+ tile.style.display = isVisible ? '' : 'none';
2018
+ });
2019
+ }
2020
+ }
2021
+
2022
+
2023
+ /* ===================== Subnet Detail Page Functions ===================== */
2024
+ /**
2025
+ * Displays the Subnet page (detailed view) for the selected netuid.
2026
+ * Hides the main content and populates all the metrics / stakes / network table.
2027
+ * @param {number} netuid The netuid of the subnet to show in detail.
2028
+ */
2029
+ function showSubnetPage(netuid) {
2030
+ try {
2031
+ window.currentSubnet = netuid;
2032
+ window.scrollTo(0, 0);
2033
+
2034
+ const subnet = window.initialData.subnets.find(s => s.netuid === parseInt(netuid, 10));
2035
+ if (!subnet) {
2036
+ throw new Error(`Subnet not found for netuid: ${netuid}`);
2037
+ }
2038
+ window.currentSubnetSymbol = subnet.symbol;
2039
+
2040
+ // Insert the "metagraph" table beneath the "stakes" table in the hidden container
2041
+ const networkTableHTML = `
2042
+ <div class="network-table-container" style="display: none;">
2043
+ <div class="network-search-container">
2044
+ <input type="text" class="network-search" placeholder="Search for name, hotkey, or coldkey ss58..."
2045
+ oninput="filterNetworkTable(this.value)" id="network-search">
2046
+ </div>
2047
+ <table class="network-table">
2048
+ <thead>
2049
+ <tr>
2050
+ <th>Name</th>
2051
+ <th>Stake Weight</th>
2052
+ <th>Stake <span style="color: #FF9900">${subnet.symbol}</span></th>
2053
+ <th>Stake <span style="color: #FF9900">${root_symbol_html}</span></th>
2054
+ <th>Dividends</th>
2055
+ <th>Incentive</th>
2056
+ <th>Emissions <span class="per-day">/day</span></th>
2057
+ <th>Hotkey</th>
2058
+ <th>Coldkey</th>
2059
+ </tr>
2060
+ </thead>
2061
+ <tbody>
2062
+ ${generateNetworkTableRows(subnet.metagraph_info)}
2063
+ </tbody>
2064
+ </table>
2065
+ </div>
2066
+ `;
2067
+
2068
+ // Show/hide main content vs. subnet detail
2069
+ document.getElementById('main-content').style.display = 'none';
2070
+ document.getElementById('subnet-page').style.display = 'block';
2071
+
2072
+ document.querySelector('#subnet-title').textContent = `${subnet.netuid} - ${subnet.name}`;
2073
+ document.querySelector('#subnet-price').innerHTML = formatNumber(subnet.price, subnet.symbol);
2074
+ document.querySelector('#subnet-market-cap').innerHTML = formatNumber(subnet.market_cap, root_symbol_html);
2075
+ document.querySelector('#subnet-total-stake').innerHTML= formatNumber(subnet.total_stake, subnet.symbol);
2076
+ document.querySelector('#subnet-emission').innerHTML = formatNumber(subnet.emission, root_symbol_html);
2077
+
2078
+
2079
+ const metagraphInfo = subnet.metagraph_info;
2080
+ document.querySelector('#network-alpha-in').innerHTML = formatNumber(metagraphInfo.alpha_in, subnet.symbol);
2081
+ document.querySelector('#network-tau-in').innerHTML = formatNumber(metagraphInfo.tao_in, root_symbol_html);
2082
+ document.querySelector('#network-moving-price').innerHTML = formatNumber(metagraphInfo.moving_price, subnet.symbol);
2083
+
2084
+ // Registration status
2085
+ const registrationElement = document.querySelector('#network-registration');
2086
+ registrationElement.textContent = metagraphInfo.registration_allowed ? 'Open' : 'Closed';
2087
+ registrationElement.classList.toggle('closed', !metagraphInfo.registration_allowed);
2088
+
2089
+ // Commit-Reveal Weight status
2090
+ const crElement = document.querySelector('#network-cr');
2091
+ crElement.textContent = metagraphInfo.commit_reveal_weights_enabled ? 'Enabled' : 'Disabled';
2092
+ crElement.classList.toggle('disabled', !metagraphInfo.commit_reveal_weights_enabled);
2093
+
2094
+ // Blocks since last step, out of tempo
2095
+ document.querySelector('#network-blocks-since-step').innerHTML =
2096
+ `${metagraphInfo.blocks_since_last_step}/${metagraphInfo.tempo}`;
2097
+
2098
+ // Number of neurons vs. max
2099
+ document.querySelector('#network-neurons').innerHTML =
2100
+ `${metagraphInfo.num_uids}/${metagraphInfo.max_uids}`;
2101
+
2102
+ // Update "Your Stakes" table
2103
+ const stakesTableBody = document.querySelector('#stakes-table-body');
2104
+ stakesTableBody.innerHTML = '';
2105
+ if (subnet.your_stakes && subnet.your_stakes.length > 0) {
2106
+ subnet.your_stakes.forEach(stake => {
2107
+ const row = document.createElement('tr');
2108
+ row.innerHTML = `
2109
+ <td class="hotkey-cell">
2110
+ <div class="hotkey-container">
2111
+ <span class="hotkey-identity" style="color: #FF9900">${stake.hotkey_identity}</span>
2112
+ <!-- Remove the unused event param -->
2113
+ <span class="copy-button" onclick="copyToClipboard('${stake.hotkey}')">copy</span>
2114
+ </div>
2115
+ </td>
2116
+ <td>${formatNumber(stake.amount, subnet.symbol)}</td>
2117
+ <td>${formatNumber(stake.ideal_value, root_symbol_html)}</td>
2118
+ <td>${formatNumber(stake.slippage_value, root_symbol_html)} (${stake.slippage_percentage.toFixed(2)}%)</td>
2119
+ <td>${formatNumber(stake.emission, subnet.symbol + '/day')}</td>
2120
+ <td>${formatNumber(stake.tao_emission, root_symbol_html + '/day')}</td>
2121
+ <td class="registered-cell">
2122
+ <span class="${stake.is_registered ? 'registered-yes' : 'registered-no'}">
2123
+ ${stake.is_registered ? 'Yes' : 'No'}
2124
+ </span>
2125
+ </td>
2126
+ <td class="actions-cell">
2127
+ <button class="manage-button">Coming soon</button>
2128
+ </td>
2129
+ `;
2130
+ stakesTableBody.appendChild(row);
2131
+ });
2132
+ } else {
2133
+ // If no user stake in this subnet
2134
+ stakesTableBody.innerHTML = `
2135
+ <tr class="no-stakes-row">
2136
+ <td colspan="8">No stakes found for this subnet</td>
2137
+ </tr>
2138
+ `;
2139
+ }
2140
+
2141
+ // Remove any previously injected network table then add the new one
2142
+ const existingNetworkTable = document.querySelector('.network-table-container');
2143
+ if (existingNetworkTable) {
2144
+ existingNetworkTable.remove();
2145
+ }
2146
+ document.querySelector('.stakes-table-container').insertAdjacentHTML('afterend', networkTableHTML);
2147
+
2148
+ // Format the new numbers
2149
+ initializeFormattedNumbers();
2150
+
2151
+ // Initialize connectivity visualization (the dots / lines "animation")
2152
+ setTimeout(() => { initNetworkVisualization(); }, 100);
2153
+
2154
+ // Toggle whether we are showing the "Your Stakes" or "Metagraph" table
2155
+ toggleStakeView();
2156
+
2157
+ // Initialize sorting on newly injected table columns
2158
+ initializeSorting();
2159
+
2160
+ // Auto-sort by Stake descending on the network table for convenience
2161
+ setTimeout(() => {
2162
+ const networkTable = document.querySelector('.network-table');
2163
+ if (networkTable) {
2164
+ const stakeColumn = networkTable.querySelector('th:nth-child(2)');
2165
+ if (stakeColumn) {
2166
+ sortTable(networkTable, 1, stakeColumn, true);
2167
+ stakeColumn.setAttribute('data-sort', 'desc');
2168
+ }
2169
+ }
2170
+ }, 100);
2171
+
2172
+ console.log('Subnet page updated successfully');
2173
+ } catch (error) {
2174
+ console.error('Error updating subnet page:', error);
2175
+ }
2176
+ }
2177
+
2178
+ /**
2179
+ * Generates the rows for the "Neurons" table (shown when the user unchecks "Show Stakes").
2180
+ * Each row, when clicked, calls showNeuronDetails(i).
2181
+ * @param {Object} metagraphInfo The "metagraph_info" of the subnet that holds hotkeys, etc.
2182
+ */
2183
+ function generateNetworkTableRows(metagraphInfo) {
2184
+ const rows = [];
2185
+ console.log('Generating network table rows with data:', metagraphInfo);
2186
+
2187
+ for (let i = 0; i < metagraphInfo.hotkeys.length; i++) {
2188
+ // Subnet symbol is used to show token vs. root stake
2189
+ const subnet = window.initialData.subnets.find(s => s.netuid === window.currentSubnet);
2190
+ const subnetSymbol = subnet ? subnet.symbol : '';
2191
+
2192
+ // Possibly show hotkey/coldkey truncated for readability
2193
+ const truncatedHotkey = truncateAddress(metagraphInfo.hotkeys[i]);
2194
+ const truncatedColdkey = truncateAddress(metagraphInfo.coldkeys[i]);
2195
+ const identityName = metagraphInfo.updated_identities[i] || '~';
2196
+
2197
+ // Root stake is being scaled by 0.18 arbitrarily here
2198
+ const adjustedRootStake = metagraphInfo.tao_stake[i] * 0.18;
2199
+
2200
+ rows.push(`
2201
+ <tr onclick="showNeuronDetails(${i})">
2202
+ <td class="identity-cell">${identityName}</td>
2203
+ <td data-value="${metagraphInfo.total_stake[i]}">
2204
+ <span class="formatted-number" data-value="${metagraphInfo.total_stake[i]}" data-symbol="${subnetSymbol}"></span>
2205
+ </td>
2206
+ <td data-value="${metagraphInfo.alpha_stake[i]}">
2207
+ <span class="formatted-number" data-value="${metagraphInfo.alpha_stake[i]}" data-symbol="${subnetSymbol}"></span>
2208
+ </td>
2209
+ <td data-value="${adjustedRootStake}">
2210
+ <span class="formatted-number" data-value="${adjustedRootStake}" data-symbol="${root_symbol_html}"></span>
2211
+ </td>
2212
+ <td data-value="${metagraphInfo.dividends[i]}">
2213
+ <span class="formatted-number" data-value="${metagraphInfo.dividends[i]}" data-symbol=""></span>
2214
+ </td>
2215
+ <td data-value="${metagraphInfo.incentives[i]}">
2216
+ <span class="formatted-number" data-value="${metagraphInfo.incentives[i]}" data-symbol=""></span>
2217
+ </td>
2218
+ <td data-value="${metagraphInfo.emission[i]}">
2219
+ <span class="formatted-number" data-value="${metagraphInfo.emission[i]}" data-symbol="${subnetSymbol}"></span>
2220
+ </td>
2221
+ <td class="address-cell">
2222
+ <div class="hotkey-container" data-full-address="${metagraphInfo.hotkeys[i]}">
2223
+ <span class="truncated-address">${truncatedHotkey}</span>
2224
+ <span class="copy-button" onclick="event.stopPropagation(); copyToClipboard('${metagraphInfo.hotkeys[i]}')">copy</span>
2225
+ </div>
2226
+ </td>
2227
+ <td class="address-cell">
2228
+ <div class="hotkey-container" data-full-address="${metagraphInfo.coldkeys[i]}">
2229
+ <span class="truncated-address">${truncatedColdkey}</span>
2230
+ <span class="copy-button" onclick="event.stopPropagation(); copyToClipboard('${metagraphInfo.coldkeys[i]}')">copy</span>
2231
+ </div>
2232
+ </td>
2233
+ </tr>
2234
+ `);
2235
+ }
2236
+ return rows.join('');
2237
+ }
2238
+
2239
+ /**
2240
+ * Handles toggling between the "Your Stakes" view and the "Neurons" view on the Subnet page.
2241
+ * The "Show Stakes" checkbox (#stake-toggle) controls which table is visible.
2242
+ */
2243
+ function toggleStakeView() {
2244
+ const showStakes = document.getElementById('stake-toggle').checked;
2245
+ const stakesTable = document.querySelector('.stakes-table-container');
2246
+ const networkTable = document.querySelector('.network-table-container');
2247
+ const sectionHeader = document.querySelector('.view-header');
2248
+ const neuronDetails = document.getElementById('neuron-detail-container');
2249
+ const addStakeButton = document.querySelector('.add-stake-button');
2250
+ const exportCsvButton = document.querySelector('.export-csv-button');
2251
+ const stakesHeader = document.querySelector('.stakes-header');
2252
+
2253
+ // First, close neuron details if they're open
2254
+ if (neuronDetails && neuronDetails.style.display !== 'none') {
2255
+ neuronDetails.style.display = 'none';
2256
+ }
2257
+
2258
+ // Always show the section header and stakes header when toggling views
2259
+ if (sectionHeader) sectionHeader.style.display = 'block';
2260
+ if (stakesHeader) stakesHeader.style.display = 'flex';
2261
+
2262
+ if (showStakes) {
2263
+ // Show the Stakes table, hide the Neurons table
2264
+ stakesTable.style.display = 'block';
2265
+ networkTable.style.display = 'none';
2266
+ sectionHeader.textContent = 'Your Stakes';
2267
+ if (addStakeButton) {
2268
+ addStakeButton.style.display = 'none';
2269
+ }
2270
+ if (exportCsvButton) {
2271
+ exportCsvButton.style.display = 'none';
2272
+ }
2273
+ } else {
2274
+ // Show the Neurons table, hide the Stakes table
2275
+ stakesTable.style.display = 'none';
2276
+ networkTable.style.display = 'block';
2277
+ sectionHeader.textContent = 'Metagraph';
2278
+ if (addStakeButton) {
2279
+ addStakeButton.style.display = 'block';
2280
+ }
2281
+ if (exportCsvButton) {
2282
+ exportCsvButton.style.display = 'block';
2283
+ }
2284
+ }
2285
+ }
2286
+
2287
+ /**
2288
+ * Called when you click a row in the "Neurons" table, to display more detail about that neuron.
2289
+ * This hides the "Neurons" table and shows the #neuron-detail-container.
2290
+ * @param {number} rowIndex The index of the neuron in the arrays (hotkeys, coldkeys, etc.)
2291
+ */
2292
+ function showNeuronDetails(rowIndex) {
2293
+ try {
2294
+ // Hide the network table & stakes table
2295
+ const networkTable = document.querySelector('.network-table-container');
2296
+ if (networkTable) networkTable.style.display = 'none';
2297
+ const stakesTable = document.querySelector('.stakes-table-container');
2298
+ if (stakesTable) stakesTable.style.display = 'none';
2299
+
2300
+ // Hide the stakes header with the action buttons
2301
+ const stakesHeader = document.querySelector('.stakes-header');
2302
+ if (stakesHeader) stakesHeader.style.display = 'none';
2303
+
2304
+ // Hide the view header that says "Neurons"
2305
+ const viewHeader = document.querySelector('.view-header');
2306
+ if (viewHeader) viewHeader.style.display = 'none';
2307
+
2308
+ // Show the neuron detail panel
2309
+ const detailContainer = document.getElementById('neuron-detail-container');
2310
+ if (detailContainer) detailContainer.style.display = 'block';
2311
+
2312
+ // Pull out the current subnet
2313
+ const subnet = window.initialData.subnets.find(s => s.netuid === window.currentSubnet);
2314
+ if (!subnet) {
2315
+ console.error('No subnet data for netuid:', window.currentSubnet);
2316
+ return;
2317
+ }
2318
+
2319
+ const metagraphInfo = subnet.metagraph_info;
2320
+ const subnetSymbol = subnet.symbol || '';
2321
+
2322
+ // Pull axon data, for IP info
2323
+ const axonData = metagraphInfo.processed_axons ? metagraphInfo.processed_axons[rowIndex] : null;
2324
+ let ipInfoString;
2325
+
2326
+ // Update IP info card - hide header if IP info is present
2327
+ const ipInfoCard = document.getElementById('neuron-ipinfo').closest('.metric-card');
2328
+ if (axonData && axonData.ip !== 'N/A') {
2329
+ // If we have valid IP info, hide the "IP Info" label
2330
+ if (ipInfoCard && ipInfoCard.querySelector('.metric-label')) {
2331
+ ipInfoCard.querySelector('.metric-label').style.display = 'none';
2332
+ }
2333
+ // Format IP info with green labels
2334
+ ipInfoString = `<span style="color: #FF9900">IP:</span> ${axonData.ip}<br>` +
2335
+ `<span style="color: #FF9900">Port:</span> ${axonData.port}<br>` +
2336
+ `<span style="color: #FF9900">Type:</span> ${axonData.ip_type}`;
2337
+ } else {
2338
+ // If no IP info, show the label
2339
+ if (ipInfoCard && ipInfoCard.querySelector('.metric-label')) {
2340
+ ipInfoCard.querySelector('.metric-label').style.display = 'block';
2341
+ }
2342
+ ipInfoString = '<span style="color: #ff4444; font-size: 1.2em;">N/A</span>';
2343
+ }
2344
+
2345
+ // Basic identity and hotkey/coldkey info
2346
+ const name = metagraphInfo.updated_identities[rowIndex] || '~';
2347
+ const hotkey = metagraphInfo.hotkeys[rowIndex];
2348
+ const coldkey = metagraphInfo.coldkeys[rowIndex];
2349
+ const rank = metagraphInfo.rank ? metagraphInfo.rank[rowIndex] : 0;
2350
+ const trust = metagraphInfo.trust ? metagraphInfo.trust[rowIndex] : 0;
2351
+ const pruning = metagraphInfo.pruning_score ? metagraphInfo.pruning_score[rowIndex] : 0;
2352
+ const vPermit = metagraphInfo.validator_permit ? metagraphInfo.validator_permit[rowIndex] : false;
2353
+ const lastUpd = metagraphInfo.last_update ? metagraphInfo.last_update[rowIndex] : 0;
2354
+ const consensus = metagraphInfo.consensus ? metagraphInfo.consensus[rowIndex] : 0;
2355
+ const regBlock = metagraphInfo.block_at_registration ? metagraphInfo.block_at_registration[rowIndex] : 0;
2356
+ const active = metagraphInfo.active ? metagraphInfo.active[rowIndex] : false;
2357
+
2358
+ // Update UI fields
2359
+ document.getElementById('neuron-name').textContent = name;
2360
+ document.getElementById('neuron-name').style.color = '#FF9900';
2361
+
2362
+ document.getElementById('neuron-hotkey').textContent = hotkey;
2363
+ document.getElementById('neuron-coldkey').textContent = coldkey;
2364
+ document.getElementById('neuron-trust').textContent = trust.toFixed(4);
2365
+ document.getElementById('neuron-pruning-score').textContent = pruning.toFixed(4);
2366
+
2367
+ // Validator
2368
+ const validatorElem = document.getElementById('neuron-validator-permit');
2369
+ if (vPermit) {
2370
+ validatorElem.style.color = '#2ECC71';
2371
+ validatorElem.textContent = 'True';
2372
+ } else {
2373
+ validatorElem.style.color = '#ff4444';
2374
+ validatorElem.textContent = 'False';
2375
+ }
2376
+
2377
+ document.getElementById('neuron-last-update').textContent = lastUpd;
2378
+ document.getElementById('neuron-consensus').textContent = consensus.toFixed(4);
2379
+ document.getElementById('neuron-reg-block').textContent = regBlock;
2380
+ document.getElementById('neuron-ipinfo').innerHTML = ipInfoString;
2381
+
2382
+ const activeElem = document.getElementById('neuron-active');
2383
+ if (active) {
2384
+ activeElem.style.color = '#2ECC71';
2385
+ activeElem.textContent = 'Yes';
2386
+ } else {
2387
+ activeElem.style.color = '#ff4444';
2388
+ activeElem.textContent = 'No';
2389
+ }
2390
+
2391
+ // Add stake data ("total_stake", "alpha_stake", "tao_stake")
2392
+ document.getElementById('neuron-stake-total').setAttribute(
2393
+ 'data-value', metagraphInfo.total_stake[rowIndex]
2394
+ );
2395
+ document.getElementById('neuron-stake-total').setAttribute(
2396
+ 'data-symbol', subnetSymbol
2397
+ );
2398
+
2399
+ document.getElementById('neuron-stake-token').setAttribute(
2400
+ 'data-value', metagraphInfo.alpha_stake[rowIndex]
2401
+ );
2402
+ document.getElementById('neuron-stake-token').setAttribute(
2403
+ 'data-symbol', subnetSymbol
2404
+ );
2405
+
2406
+ // Multiply tao_stake by 0.18
2407
+ const originalStakeRoot = metagraphInfo.tao_stake[rowIndex];
2408
+ const calculatedStakeRoot = originalStakeRoot * 0.18;
2409
+
2410
+ document.getElementById('neuron-stake-root').setAttribute(
2411
+ 'data-value', calculatedStakeRoot
2412
+ );
2413
+ document.getElementById('neuron-stake-root').setAttribute(
2414
+ 'data-symbol', root_symbol_html
2415
+ );
2416
+ // Also set the inner text right away, so we show a correct format on load
2417
+ document.getElementById('neuron-stake-root').innerHTML =
2418
+ formatNumber(calculatedStakeRoot, root_symbol_html);
2419
+
2420
+ // Dividends, Incentive
2421
+ document.getElementById('neuron-dividends').setAttribute(
2422
+ 'data-value', metagraphInfo.dividends[rowIndex]
2423
+ );
2424
+ document.getElementById('neuron-dividends').setAttribute('data-symbol', '');
2425
+
2426
+ document.getElementById('neuron-incentive').setAttribute(
2427
+ 'data-value', metagraphInfo.incentives[rowIndex]
2428
+ );
2429
+ document.getElementById('neuron-incentive').setAttribute('data-symbol', '');
2430
+
2431
+ // Emissions
2432
+ document.getElementById('neuron-emissions').setAttribute(
2433
+ 'data-value', metagraphInfo.emission[rowIndex]
2434
+ );
2435
+ document.getElementById('neuron-emissions').setAttribute('data-symbol', subnetSymbol);
2436
+
2437
+ // Rank
2438
+ document.getElementById('neuron-rank').textContent = rank.toFixed(4);
2439
+
2440
+ // Re-run formatting so the newly updated data-values appear in numeric form
2441
+ initializeFormattedNumbers();
2442
+ } catch (err) {
2443
+ console.error('Error showing neuron details:', err);
2444
+ }
2445
+ }
2446
+
2447
+ /**
2448
+ * Closes the neuron detail panel and goes back to whichever table was selected ("Stakes" or "Metagraph").
2449
+ */
2450
+ function closeNeuronDetails() {
2451
+ // Hide neuron details
2452
+ const detailContainer = document.getElementById('neuron-detail-container');
2453
+ if (detailContainer) detailContainer.style.display = 'none';
2454
+
2455
+ // Show the stakes header with action buttons
2456
+ const stakesHeader = document.querySelector('.stakes-header');
2457
+ if (stakesHeader) stakesHeader.style.display = 'flex';
2458
+
2459
+ // Show the view header again
2460
+ const viewHeader = document.querySelector('.view-header');
2461
+ if (viewHeader) viewHeader.style.display = 'block';
2462
+
2463
+ // Show the appropriate table based on toggle state
2464
+ const showStakes = document.getElementById('stake-toggle').checked;
2465
+ const stakesTable = document.querySelector('.stakes-table-container');
2466
+ const networkTable = document.querySelector('.network-table-container');
2467
+
2468
+ if (showStakes) {
2469
+ stakesTable.style.display = 'block';
2470
+ networkTable.style.display = 'none';
2471
+
2472
+ // Hide action buttons when showing stakes
2473
+ const addStakeButton = document.querySelector('.add-stake-button');
2474
+ const exportCsvButton = document.querySelector('.export-csv-button');
2475
+ if (addStakeButton) addStakeButton.style.display = 'none';
2476
+ if (exportCsvButton) exportCsvButton.style.display = 'none';
2477
+ } else {
2478
+ stakesTable.style.display = 'none';
2479
+ networkTable.style.display = 'block';
2480
+
2481
+ // Show action buttons when showing metagraph
2482
+ const addStakeButton = document.querySelector('.add-stake-button');
2483
+ const exportCsvButton = document.querySelector('.export-csv-button');
2484
+ if (addStakeButton) addStakeButton.style.display = 'block';
2485
+ if (exportCsvButton) exportCsvButton.style.display = 'block';
2486
+ }
2487
+ }
2488
+
2489
+
2490
+ /* ===================== Number Formatting Functions ===================== */
2491
+ /**
2492
+ * Toggles the numeric display between "verbose" and "short" notations
2493
+ * across all .formatted-number elements on the page.
2494
+ */
2495
+ function toggleVerboseNumbers() {
2496
+ // We read from the main or subnet checkboxes
2497
+ verboseNumbers =
2498
+ document.getElementById('verbose-toggle')?.checked ||
2499
+ document.getElementById('show-verbose')?.checked ||
2500
+ false;
2501
+
2502
+ // Reformat all visible .formatted-number elements
2503
+ document.querySelectorAll('.formatted-number').forEach(element => {
2504
+ const value = parseFloat(element.dataset.value);
2505
+ const symbol = element.dataset.symbol;
2506
+ element.innerHTML = formatNumber(value, symbol);
2507
+ });
2508
+
2509
+ // If we're currently on the Subnet detail page, update those numbers too
2510
+ if (document.getElementById('subnet-page').style.display !== 'none') {
2511
+ updateAllNumbers();
2512
+ }
2513
+ }
2514
+
2515
+ /**
2516
+ * Scans all .formatted-number elements and replaces their text with
2517
+ * the properly formatted version (short or verbose).
2518
+ */
2519
+ function initializeFormattedNumbers() {
2520
+ document.querySelectorAll('.formatted-number').forEach(element => {
2521
+ const value = parseFloat(element.dataset.value);
2522
+ const symbol = element.dataset.symbol;
2523
+ element.innerHTML = formatNumber(value, symbol);
2524
+ });
2525
+ }
2526
+
2527
+ /**
2528
+ * Called by toggleVerboseNumbers() to reformat key metrics on the Subnet page
2529
+ * that might not be directly wrapped in .formatted-number but need to be updated anyway.
2530
+ */
2531
+ function updateAllNumbers() {
2532
+ try {
2533
+ const subnet = window.initialData.subnets.find(s => s.netuid === window.currentSubnet);
2534
+ if (!subnet) {
2535
+ console.error('Could not find subnet data for netuid:', window.currentSubnet);
2536
+ return;
2537
+ }
2538
+ // Reformat a few items in the Subnet detail header
2539
+ document.querySelector('#subnet-market-cap').innerHTML =
2540
+ formatNumber(subnet.market_cap, root_symbol_html);
2541
+ document.querySelector('#subnet-total-stake').innerHTML =
2542
+ formatNumber(subnet.total_stake, subnet.symbol);
2543
+ document.querySelector('#subnet-emission').innerHTML =
2544
+ formatNumber(subnet.emission, root_symbol_html);
2545
+
2546
+ // Reformat the Metagraph table data
2547
+ const netinfo = subnet.metagraph_info;
2548
+ document.querySelector('#network-alpha-in').innerHTML =
2549
+ formatNumber(netinfo.alpha_in, subnet.symbol);
2550
+ document.querySelector('#network-tau-in').innerHTML =
2551
+ formatNumber(netinfo.tao_in, root_symbol_html);
2552
+
2553
+ // Reformat items in "Your Stakes" table
2554
+ document.querySelectorAll('#stakes-table-body .formatted-number').forEach(element => {
2555
+ const value = parseFloat(element.dataset.value);
2556
+ const symbol = element.dataset.symbol;
2557
+ element.innerHTML = formatNumber(value, symbol);
2558
+ });
2559
+ } catch (error) {
2560
+ console.error('Error updating numbers:', error);
2561
+ }
2562
+ }
2563
+
2564
+ /**
2565
+ * Format a numeric value into either:
2566
+ * - a short format (e.g. 1.23k, 3.45m) if verboseNumbers==false
2567
+ * - a more precise format (1,234.5678) if verboseNumbers==true
2568
+ * @param {number} num The numeric value to format.
2569
+ * @param {string} symbol A short suffix or currency symbol (e.g. 'τ') that we append.
2570
+ */
2571
+ function formatNumber(num, symbol = '') {
2572
+ if (num === undefined || num === null || isNaN(num)) {
2573
+ return '0.00 ' + `<span style="color: #FF9900">${symbol}</span>`;
2574
+ }
2575
+ num = parseFloat(num);
2576
+ if (num === 0) {
2577
+ return '0.00 ' + `<span style="color: #FF9900">${symbol}</span>`;
2578
+ }
2579
+
2580
+ // If user requested verbose
2581
+ if (verboseNumbers) {
2582
+ return num.toLocaleString('en-US', {
2583
+ minimumFractionDigits: 4,
2584
+ maximumFractionDigits: 4
2585
+ }) + ' ' + `<span style="color: #FF9900">${symbol}</span>`;
2586
+ }
2587
+
2588
+ // Otherwise show short scale for large numbers
2589
+ const absNum = Math.abs(num);
2590
+ if (absNum >= 1000) {
2591
+ const suffixes = ['', 'k', 'm', 'b', 't'];
2592
+ const magnitude = Math.min(4, Math.floor(Math.log10(absNum) / 3));
2593
+ const scaledNum = num / Math.pow(10, magnitude * 3);
2594
+ return scaledNum.toFixed(2) + suffixes[magnitude] + ' ' +
2595
+ `<span style="color: #FF9900">${symbol}</span>`;
2596
+ } else {
2597
+ // For small numbers <1000, just show 4 decimals
2598
+ return num.toFixed(4) + ' ' + `<span style="color: #FF9900">${symbol}</span>`;
2599
+ }
2600
+ }
2601
+
2602
+ /**
2603
+ * Truncates a string address into the format "ABC..XYZ" for a bit more readability
2604
+ * @param {string} address
2605
+ * @returns {string} truncated address form
2606
+ */
2607
+ function truncateAddress(address) {
2608
+ if (!address || address.length <= 7) {
2609
+ return address; // no need to truncate if very short
2610
+ }
2611
+ return `${address.substring(0, 3)}..${address.substring(address.length - 3)}`;
2612
+ }
2613
+
2614
+ /**
2615
+ * Format a number in compact notation (K, M, B) for tile display
2616
+ */
2617
+ function formatTileNumbers(num) {
2618
+ if (num >= 1000000000) {
2619
+ return (num / 1000000000).toFixed(1) + 'B';
2620
+ } else if (num >= 1000000) {
2621
+ return (num / 1000000).toFixed(1) + 'M';
2622
+ } else if (num >= 1000) {
2623
+ return (num / 1000).toFixed(1) + 'K';
2624
+ } else {
2625
+ return num.toFixed(1);
2626
+ }
2627
+ }
2628
+
2629
+
2630
+ /* ===================== Table Sorting and Filtering Functions ===================== */
2631
+ /**
2632
+ * Switches the Metagraph or Stakes table from sorting ascending to descending on a column, and vice versa.
2633
+ * @param {HTMLTableElement} table The table element itself
2634
+ * @param {number} columnIndex The column index to sort by
2635
+ * @param {HTMLTableHeaderCellElement} header The <th> element clicked
2636
+ * @param {boolean} forceDescending If true and no existing sort marker, will do a descending sort by default
2637
+ */
2638
+ function sortTable(table, columnIndex, header, forceDescending = false) {
2639
+ const tbody = table.querySelector('tbody');
2640
+ const rows = Array.from(tbody.querySelectorAll('tr'));
2641
+
2642
+ // If forcing descending and the header has no 'data-sort', default to 'desc'
2643
+ let isDescending;
2644
+ if (forceDescending && !header.hasAttribute('data-sort')) {
2645
+ isDescending = true;
2646
+ } else {
2647
+ isDescending = header.getAttribute('data-sort') !== 'desc';
2648
+ }
2649
+
2650
+ // Clear data-sort from all headers in the table
2651
+ table.querySelectorAll('th').forEach(th => {
2652
+ th.removeAttribute('data-sort');
2653
+ });
2654
+ // Mark the clicked header with new direction
2655
+ header.setAttribute('data-sort', isDescending ? 'desc' : 'asc');
2656
+
2657
+ // Sort numerically
2658
+ rows.sort((rowA, rowB) => {
2659
+ const cellA = rowA.cells[columnIndex];
2660
+ const cellB = rowB.cells[columnIndex];
2661
+
2662
+ // Attempt to parse float from data-value or fallback to textContent
2663
+ let valueA = parseFloat(cellA.getAttribute('data-value')) ||
2664
+ parseFloat(cellA.textContent.replace(/[^\\d.-]/g, '')) ||
2665
+ 0;
2666
+ let valueB = parseFloat(cellB.getAttribute('data-value')) ||
2667
+ parseFloat(cellB.textContent.replace(/[^\\d.-]/g, '')) ||
2668
+ 0;
2669
+
2670
+ return isDescending ? (valueB - valueA) : (valueA - valueB);
2671
+ });
2672
+
2673
+ // Reinsert sorted rows
2674
+ tbody.innerHTML = '';
2675
+ rows.forEach(row => tbody.appendChild(row));
2676
+ }
2677
+
2678
+ /**
2679
+ * Adds sortable behavior to certain columns in the "stakes-table" or "network-table".
2680
+ * Called after these tables are created in showSubnetPage().
2681
+ */
2682
+ function initializeSorting() {
2683
+ const networkTable = document.querySelector('.network-table');
2684
+ if (networkTable) {
2685
+ initializeTableSorting(networkTable);
2686
+ }
2687
+ const stakesTable = document.querySelector('.stakes-table');
2688
+ if (stakesTable) {
2689
+ initializeTableSorting(stakesTable);
2690
+ }
2691
+ }
2692
+
2693
+ /**
2694
+ * Helper function that attaches sort handlers to appropriate columns in a table.
2695
+ * @param {HTMLTableElement} table The table element to set up sorting for.
2696
+ */
2697
+ function initializeTableSorting(table) {
2698
+ const headers = table.querySelectorAll('th');
2699
+ headers.forEach((header, index) => {
2700
+ // We only want some columns to be sortable, as in original code
2701
+ if (table.classList.contains('stakes-table') && index >= 1 && index <= 5) {
2702
+ header.classList.add('sortable');
2703
+ header.addEventListener('click', () => {
2704
+ sortTable(table, index, header, true);
2705
+ });
2706
+ } else if (table.classList.contains('network-table') && index < 6) {
2707
+ header.classList.add('sortable');
2708
+ header.addEventListener('click', () => {
2709
+ sortTable(table, index, header, true);
2710
+ });
2711
+ }
2712
+ });
2713
+ }
2714
+
2715
+ /**
2716
+ * Filters rows in the Metagraph table by name, hotkey, or coldkey.
2717
+ * Invoked by the oninput event of the #network-search field.
2718
+ * @param {string} searchValue The substring typed by the user.
2719
+ */
2720
+ function filterNetworkTable(searchValue) {
2721
+ const searchTerm = searchValue.toLowerCase().trim();
2722
+ const rows = document.querySelectorAll('.network-table tbody tr');
2723
+
2724
+ rows.forEach(row => {
2725
+ const nameCell = row.querySelector('.identity-cell');
2726
+ const hotkeyContainer = row.querySelector('.hotkey-container[data-full-address]');
2727
+ const coldkeyContainer = row.querySelectorAll('.hotkey-container[data-full-address]')[1];
2728
+
2729
+ const name = nameCell ? nameCell.textContent.toLowerCase() : '';
2730
+ const hotkey = hotkeyContainer ? hotkeyContainer.getAttribute('data-full-address').toLowerCase() : '';
2731
+ const coldkey= coldkeyContainer ? coldkeyContainer.getAttribute('data-full-address').toLowerCase() : '';
2732
+
2733
+ const matches = (name.includes(searchTerm) || hotkey.includes(searchTerm) || coldkey.includes(searchTerm));
2734
+ row.style.display = matches ? '' : 'none';
2735
+ });
2736
+ }
2737
+
2738
+
2739
+ /* ===================== Network Visualization Functions ===================== */
2740
+ /**
2741
+ * Initializes the network visualization on the canvas element.
2742
+ */
2743
+ function initNetworkVisualization() {
2744
+ try {
2745
+ const canvas = document.getElementById('network-canvas');
2746
+ if (!canvas) {
2747
+ console.error('Canvas element (#network-canvas) not found');
2748
+ return;
2749
+ }
2750
+ const ctx = canvas.getContext('2d');
2751
+
2752
+ const subnet = window.initialData.subnets.find(s => s.netuid === window.currentSubnet);
2753
+ if (!subnet) {
2754
+ console.error('Could not find subnet data for netuid:', window.currentSubnet);
2755
+ return;
2756
+ }
2757
+ const numNeurons = subnet.metagraph_info.num_uids;
2758
+ const nodes = [];
2759
+
2760
+ // Randomly place nodes, each with a small velocity
2761
+ for (let i = 0; i < numNeurons; i++) {
2762
+ nodes.push({
2763
+ x: Math.random() * canvas.width,
2764
+ y: Math.random() * canvas.height,
2765
+ radius: 2,
2766
+ vx: (Math.random() - 0.5) * 0.5,
2767
+ vy: (Math.random() - 0.5) * 0.5
2768
+ });
2769
+ }
2770
+
2771
+ // Animation loop
2772
+ function animate() {
2773
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
2774
+
2775
+ ctx.beginPath();
2776
+ ctx.strokeStyle = 'rgba(255, 153, 0, 0.2)';
2777
+ for (let i = 0; i < nodes.length; i++) {
2778
+ for (let j = i + 1; j < nodes.length; j++) {
2779
+ const dx = nodes[i].x - nodes[j].x;
2780
+ const dy = nodes[i].y - nodes[j].y;
2781
+ const distance = Math.sqrt(dx * dx + dy * dy);
2782
+ if (distance < 30) {
2783
+ ctx.moveTo(nodes[i].x, nodes[i].y);
2784
+ ctx.lineTo(nodes[j].x, nodes[j].y);
2785
+ }
2786
+ }
2787
+ }
2788
+ ctx.stroke();
2789
+
2790
+ nodes.forEach(node => {
2791
+ node.x += node.vx;
2792
+ node.y += node.vy;
2793
+
2794
+ // Bounce them off the edges
2795
+ if (node.x <= 0 || node.x >= canvas.width) node.vx *= -1;
2796
+ if (node.y <= 0 || node.y >= canvas.height) node.vy *= -1;
2797
+
2798
+ ctx.beginPath();
2799
+ ctx.fillStyle = '#FF9900';
2800
+ ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2);
2801
+ ctx.fill();
2802
+ });
2803
+
2804
+ requestAnimationFrame(animate);
2805
+ }
2806
+ animate();
2807
+ } catch (error) {
2808
+ console.error('Error in network visualization:', error);
2809
+ }
2810
+ }
2811
+
2812
+
2813
+ /* ===================== Tile View Functions ===================== */
2814
+ /**
2815
+ * Toggles between the tile view and table view of subnets.
2816
+ */
2817
+ function toggleTileView() {
2818
+ const showTiles = document.getElementById('show-tiles').checked;
2819
+ const tilesContainer = document.getElementById('subnet-tiles-container');
2820
+ const tableContainer = document.querySelector('.subnets-table-container');
2821
+
2822
+ if (showTiles) {
2823
+ // Show tiles, hide table
2824
+ tilesContainer.style.display = 'flex';
2825
+ tableContainer.style.display = 'none';
2826
+
2827
+ // Generate tiles if they don't exist yet
2828
+ if (tilesContainer.children.length === 0) {
2829
+ generateSubnetTiles();
2830
+ }
2831
+
2832
+ // Apply current filters to the tiles
2833
+ filterSubnets();
2834
+ } else {
2835
+ // Show table, hide tiles
2836
+ tilesContainer.style.display = 'none';
2837
+ tableContainer.style.display = 'block';
2838
+ }
2839
+ }
2840
+
2841
+ /**
2842
+ * Generates the subnet tiles based on the initialData.
2843
+ */
2844
+ function generateSubnetTiles() {
2845
+ const tilesContainer = document.getElementById('subnet-tiles-container');
2846
+ tilesContainer.innerHTML = ''; // Clear existing tiles
2847
+
2848
+ // Sort subnets by market cap (descending)
2849
+ const sortedSubnets = [...window.initialData.subnets].sort((a, b) => b.market_cap - a.market_cap);
2850
+
2851
+ sortedSubnets.forEach(subnet => {
2852
+ const isStaked = subnet.your_stakes && subnet.your_stakes.length > 0;
2853
+ const marketCapFormatted = formatTileNumbers(subnet.market_cap);
2854
+
2855
+ const tile = document.createElement('div');
2856
+ tile.className = `subnet-tile ${isStaked ? 'staked' : ''}`;
2857
+ tile.onclick = () => showSubnetPage(subnet.netuid);
2858
+
2859
+ // Calculate background intensity based on market cap relative to max
2860
+ const maxMarketCap = sortedSubnets[0].market_cap;
2861
+ const intensity = Math.max(5, Math.min(15, 5 + (subnet.market_cap / maxMarketCap) * 10));
2862
+
2863
+ tile.innerHTML = `
2864
+ <span class="tile-netuid">${subnet.netuid}</span>
2865
+ <span class="tile-symbol">${subnet.symbol}</span>
2866
+ <span class="tile-name">${subnet.name}</span>
2867
+ <span class="tile-market-cap">${marketCapFormatted} ${root_symbol_html}</span>
2868
+ `;
2869
+
2870
+ // Set background intensity
2871
+ tile.style.background = `rgba(255, 255, 255, 0.0${intensity.toFixed(0)})`;
2872
+
2873
+ tilesContainer.appendChild(tile);
2874
+ });
2875
+ }
2876
+ """