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