diff --git a/.github/workflows/e2e-subtensor-tests.yml b/.github/workflows/e2e-subtensor-tests.yml
index 241abdc7c..6b0db3f26 100644
--- a/.github/workflows/e2e-subtensor-tests.yml
+++ b/.github/workflows/e2e-subtensor-tests.yml
@@ -88,7 +88,6 @@ jobs:
uv venv .venv
source .venv/bin/activate
uv pip install .[dev]
- uv pip install pytest
- name: Download Cached Docker Image
uses: actions/download-artifact@v4
diff --git a/.github/workflows/ruff-formatter.yml b/.github/workflows/ruff-formatter.yml
new file mode 100644
index 000000000..93166c304
--- /dev/null
+++ b/.github/workflows/ruff-formatter.yml
@@ -0,0 +1,42 @@
+name: Ruff Formatter Check
+permissions:
+ contents: read
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened, edited]
+
+jobs:
+ ruff:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.9.13"]
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Set up caching for Ruff virtual environment
+ id: cache-ruff
+ uses: actions/cache@v4
+ with:
+ path: .venv
+ key: v2-pypi-py-ruff-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }}
+ restore-keys: |
+ v2-pypi-py-ruff-${{ matrix.python-version }}-
+
+ - name: Set up Ruff virtual environment if cache is missed
+ if: steps.cache-ruff.outputs.cache-hit != 'true'
+ run: |
+ python -m venv .venv
+ .venv/bin/python -m pip install ruff==0.11.5
+
+ - name: Ruff format check
+ run: |
+ .venv/bin/ruff format --diff bittensor_cli
+ .venv/bin/ruff format --diff tests
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 890241007..92ea41cf4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+## 9.5.0 /2025-06-02
+
+## What's Changed
+* Replace PyWry by @thewhaleking in https://github.com/opentensor/btcli/pull/472
+* Remove fuzzywuzzy by @thewhaleking in https://github.com/opentensor/btcli/pull/473
+* Add ruff formatter by @thewhaleking in https://github.com/opentensor/btcli/pull/474
+
+**Full Changelog**: https://github.com/opentensor/btcli/compare/v9.4.4...v9.5.0
+
## 9.4.4 /2025-04-29
## What's Changed
diff --git a/bittensor_cli/cli.py b/bittensor_cli/cli.py
index 62f0fb1d9..f1947a3e1 100755
--- a/bittensor_cli/cli.py
+++ b/bittensor_cli/cli.py
@@ -67,8 +67,6 @@
prompt_for_identity,
validate_uri,
prompt_for_subnet_identity,
- print_linux_dependency_message,
- is_linux,
validate_rate_tolerance,
)
@@ -4875,9 +4873,6 @@ def subnets_price(
if all_netuids and not json_output:
html_output = True
- if html_output and is_linux():
- print_linux_dependency_message()
-
return self._run_command(
price.price(
self.initialize_chain(network),
@@ -5655,7 +5650,7 @@ def view_dashboard(
help="Coldkey SS58 address to view dashboard for",
),
use_wry: bool = typer.Option(
- False, "--use-wry", help="Use PyWry instead of browser window"
+ False, "--use-wry", "--html", help="Display output in browser window."
),
save_file: bool = typer.Option(
False, "--save-file", "--save", help="Save the dashboard HTML file"
@@ -5668,12 +5663,10 @@ def view_dashboard(
Display html dashboard with subnets list, stake, and neuron information.
"""
self.verbosity_handler(quiet, verbose)
- if use_wry and is_linux():
- print_linux_dependency_message()
if use_wry and save_file:
- print_error("Cannot save file when using PyWry.")
- raise typer.Exit()
+ print_error("Cannot save file when using browser output.")
+ return
if save_file:
if not dashboard_path:
diff --git a/bittensor_cli/src/bittensor/templates/main-filters.j2 b/bittensor_cli/src/bittensor/templates/main-filters.j2
new file mode 100644
index 000000000..275f012a0
--- /dev/null
+++ b/bittensor_cli/src/bittensor/templates/main-filters.j2
@@ -0,0 +1,24 @@
+
+
\ No newline at end of file
diff --git a/bittensor_cli/src/bittensor/templates/main-header.j2 b/bittensor_cli/src/bittensor/templates/main-header.j2
new file mode 100644
index 000000000..dadb6f33d
--- /dev/null
+++ b/bittensor_cli/src/bittensor/templates/main-header.j2
@@ -0,0 +1,36 @@
+{#
+ vars:
+ wallet_info.coldkey, truncated_coldkey, wallet_info.balance, root_symbol_html, wallet_info.total_ideal_stake_value,
+ slippage_percentage, wallet_info.total_slippage_value, block_number
+#}
+
+
\ No newline at end of file
diff --git a/bittensor_cli/src/bittensor/templates/neuron-details.j2 b/bittensor_cli/src/bittensor/templates/neuron-details.j2
new file mode 100644
index 000000000..9a2964afc
--- /dev/null
+++ b/bittensor_cli/src/bittensor/templates/neuron-details.j2
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+ Hotkey:
+
+
+
+ Coldkey:
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bittensor_cli/src/bittensor/templates/price-multi.j2 b/bittensor_cli/src/bittensor/templates/price-multi.j2
new file mode 100644
index 000000000..0cb64b65b
--- /dev/null
+++ b/bittensor_cli/src/bittensor/templates/price-multi.j2
@@ -0,0 +1,113 @@
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+ All
+ {% for netuid in sorted_subnet_keys %}
+ S{{ netuid }}
+ {% endfor %}
+
+
+
+
+
\ No newline at end of file
diff --git a/bittensor_cli/src/bittensor/templates/price-single.j2 b/bittensor_cli/src/bittensor/templates/price-single.j2
new file mode 100644
index 000000000..39a61113c
--- /dev/null
+++ b/bittensor_cli/src/bittensor/templates/price-single.j2
@@ -0,0 +1,99 @@
+
+
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bittensor_cli/src/bittensor/templates/subnet-details-header.j2 b/bittensor_cli/src/bittensor/templates/subnet-details-header.j2
new file mode 100644
index 000000000..efef90e35
--- /dev/null
+++ b/bittensor_cli/src/bittensor/templates/subnet-details-header.j2
@@ -0,0 +1,49 @@
+
\ No newline at end of file
diff --git a/bittensor_cli/src/bittensor/templates/subnet-details.j2 b/bittensor_cli/src/bittensor/templates/subnet-details.j2
new file mode 100644
index 000000000..ef0e591e8
--- /dev/null
+++ b/bittensor_cli/src/bittensor/templates/subnet-details.j2
@@ -0,0 +1,32 @@
+{# TODO: This may be unused. #}
+
+
+
+
+
+
+ Price
+
+
+
+ Market Cap
+
+
+
+ Emission Rate
+
+
+
+ Your Total Stake
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bittensor_cli/src/bittensor/templates/subnet-metrics.j2 b/bittensor_cli/src/bittensor/templates/subnet-metrics.j2
new file mode 100644
index 000000000..f27d3e362
--- /dev/null
+++ b/bittensor_cli/src/bittensor/templates/subnet-metrics.j2
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
+ Hotkey
+ Amount
+ Value
+ Value (w/ slippage)
+ Alpha emission
+ Tao emission
+ Registered
+ Actions
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/bittensor_cli/src/bittensor/templates/subnets-table.j2 b/bittensor_cli/src/bittensor/templates/subnets-table.j2
new file mode 100644
index 000000000..fe4dca335
--- /dev/null
+++ b/bittensor_cli/src/bittensor/templates/subnets-table.j2
@@ -0,0 +1,28 @@
+
+
+
+
+ Subnet
+ Price
+ Market Cap
+ Your Stake
+ Emission
+ Status
+
+
+
+ {% for subnet in subnets %}
+ {% set total_your_stake = subnet.your_stakes|sum(attribute="amount") %}
+ {% set stake_status = 'Staked' if total_your_stake > 0 else 'Not Staked' %}
+
+ {{subnet.netuid}} - {{subnet.name}}
+
+
+
+
+ {{stake_status}}
+
+ {% endfor %}
+
+
+
\ No newline at end of file
diff --git a/bittensor_cli/src/bittensor/templates/view.css b/bittensor_cli/src/bittensor/templates/view.css
new file mode 100644
index 000000000..438578f70
--- /dev/null
+++ b/bittensor_cli/src/bittensor/templates/view.css
@@ -0,0 +1,1058 @@
+/* ===================== Base Styles & Typography ===================== */
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Noto+Sans:wght@400;500;600&display=swap');
+
+body {
+ font-family: 'Inter', 'Noto Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Arial Unicode MS', sans-serif;
+ margin: 0;
+ padding: 24px;
+ background: #000000;
+ color: #ffffff;
+}
+
+input, button, select {
+ font-family: inherit;
+ font-feature-settings: normal;
+}
+
+/* ===================== Main Page Header ===================== */
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 24px;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 12px;
+ margin-bottom: 24px;
+ backdrop-filter: blur(10px);
+}
+
+.wallet-info {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.wallet-name {
+ font-size: 1.1em;
+ font-weight: 500;
+ color: #FF9900;
+}
+
+.wallet-address-container {
+ position: relative;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.wallet-address {
+ font-size: 0.9em;
+ color: rgba(255, 255, 255, 0.5);
+ font-family: monospace;
+ transition: color 0.2s ease;
+}
+
+.wallet-address-container:hover .wallet-address {
+ color: rgba(255, 255, 255, 0.8);
+}
+
+.copy-indicator {
+ background: rgba(255, 153, 0, 0.1);
+ color: rgba(255, 153, 0, 0.8);
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-size: 0.7em;
+ transition: all 0.2s ease;
+ opacity: 0;
+}
+
+.wallet-address-container:hover .copy-indicator {
+ opacity: 1;
+ background: rgba(255, 153, 0, 0.2);
+}
+
+.wallet-address-container.copied .copy-indicator {
+ opacity: 1;
+ background: rgba(255, 153, 0, 0.3);
+ color: #FF9900;
+}
+
+.stake-metrics {
+ display: flex;
+ gap: 24px;
+ align-items: center;
+}
+
+.stake-metric {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 2px;
+ position: relative;
+ padding: 8px 16px;
+ border-radius: 8px;
+ transition: all 0.2s ease;
+}
+
+.stake-metric:hover {
+ background: rgba(255, 153, 0, 0.05);
+}
+
+.stake-metric .metric-label {
+ font-size: 0.8em;
+ color: rgba(255, 255, 255, 0.6);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.stake-metric .metric-value {
+ font-size: 1.1em;
+ font-weight: 500;
+ color: #FF9900;
+ font-feature-settings: "tnum";
+ font-variant-numeric: tabular-nums;
+}
+
+.slippage-value {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.slippage-detail {
+ font-size: 0.8em;
+ color: rgba(255, 255, 255, 0.5);
+}
+
+/* ===================== Main Page Filters ===================== */
+.filters-section {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin: 24px 0;
+ gap: 16px;
+}
+
+.search-box input {
+ padding: 10px 16px;
+ border-radius: 8px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ background: rgba(255, 255, 255, 0.03);
+ color: rgba(255, 255, 255, 0.7);
+ width: 240px;
+ font-size: 0.9em;
+ transition: all 0.2s ease;
+}
+.search-box input::placeholder {
+ color: rgba(255, 255, 255, 0.4);
+}
+
+.search-box input:focus {
+ outline: none;
+ border-color: rgba(255, 153, 0, 0.5);
+ background: rgba(255, 255, 255, 0.06);
+ color: rgba(255, 255, 255, 0.9);
+}
+
+.filter-toggles {
+ display: flex;
+ gap: 16px;
+}
+
+.filter-toggles label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 0.9em;
+ cursor: pointer;
+ user-select: none;
+}
+
+/* Checkbox styling for both main page and subnet page */
+.filter-toggles input[type="checkbox"],
+.toggle-label input[type="checkbox"] {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ width: 18px;
+ height: 18px;
+ border: 2px solid rgba(255, 153, 0, 0.3);
+ border-radius: 4px;
+ background: rgba(0, 0, 0, 0.2);
+ cursor: pointer;
+ position: relative;
+ transition: all 0.2s ease;
+}
+
+.filter-toggles input[type="checkbox"]:hover,
+.toggle-label input[type="checkbox"]:hover {
+ border-color: #FF9900;
+}
+
+.filter-toggles input[type="checkbox"]:checked,
+.toggle-label input[type="checkbox"]:checked {
+ background: #FF9900;
+ border-color: #FF9900;
+}
+
+.filter-toggles input[type="checkbox"]:checked::after,
+.toggle-label input[type="checkbox"]:checked::after {
+ content: '';
+ position: absolute;
+ left: 5px;
+ top: 2px;
+ width: 4px;
+ height: 8px;
+ border: solid #000;
+ border-width: 0 2px 2px 0;
+ transform: rotate(45deg);
+}
+
+.filter-toggles label:hover,
+.toggle-label:hover {
+ color: rgba(255, 255, 255, 0.9);
+}
+.disabled-label {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+.add-stake-button {
+ padding: 10px 20px;
+ font-size: 0.8rem;
+}
+.export-csv-button {
+ padding: 10px 20px;
+ font-size: 0.8rem;
+}
+.button-group {
+ display: flex;
+ gap: 8px;
+}
+
+/* ===================== Main Page Subnet Table ===================== */
+.subnets-table-container {
+ background: rgba(255, 255, 255, 0.02);
+ border-radius: 12px;
+ overflow: hidden;
+}
+
+.subnets-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.95em;
+}
+
+.subnets-table th {
+ background: rgba(255, 255, 255, 0.05);
+ font-weight: 500;
+ text-align: left;
+ padding: 16px;
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.subnets-table td {
+ padding: 14px 16px;
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
+}
+
+.subnet-row {
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+}
+
+.subnet-row:hover {
+ background: rgba(255, 255, 255, 0.05);
+}
+
+.subnet-name {
+ color: #ffffff;
+ font-weight: 500;
+ font-size: 0.95em;
+}
+
+.price, .market-cap, .your-stake, .emission {
+ font-family: 'Inter', monospace;
+ font-size: 1.0em;
+ font-feature-settings: "tnum";
+ font-variant-numeric: tabular-nums;
+ letter-spacing: 0.01em;
+ white-space: nowrap;
+}
+
+.stake-status {
+ font-size: 0.85em;
+ padding: 4px 8px;
+ border-radius: 4px;
+ background: rgba(255, 255, 255, 0.05);
+}
+
+.stake-status.staked {
+ background: rgba(255, 153, 0, 0.1);
+ color: #FF9900;
+}
+
+.subnets-table th.sortable {
+ cursor: pointer;
+ position: relative;
+ padding-right: 20px;
+}
+
+.subnets-table th.sortable:hover {
+ color: #FF9900;
+}
+
+.subnets-table th[data-sort] {
+ color: #FF9900;
+}
+
+/* ===================== Subnet Tiles View ===================== */
+.subnet-tiles-container {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ gap: 1rem;
+ padding: 1rem;
+}
+
+.subnet-tile {
+ width: clamp(75px, 6vw, 600px);
+ height: clamp(75px, 6vw, 600px);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 8px;
+ position: relative;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ overflow: hidden;
+ font-size: clamp(0.6rem, 1vw, 1.4rem);
+}
+
+.tile-netuid {
+ position: absolute;
+ top: 0.4em;
+ left: 0.4em;
+ font-size: 0.7em;
+ color: rgba(255, 255, 255, 0.6);
+}
+
+.tile-symbol {
+ font-size: 1.6em;
+ margin-bottom: 0.4em;
+ color: #FF9900;
+}
+
+.tile-name {
+ display: block;
+ width: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-size: 1em;
+ text-align: center;
+ color: rgba(255, 255, 255, 0.9);
+ margin: 0 0.4em;
+}
+
+.tile-market-cap {
+ font-size: 0.9em;
+ color: rgba(255, 255, 255, 0.5);
+ margin-top: 2px;
+}
+
+.subnet-tile:hover {
+ transform: translateY(-2px);
+ box-shadow:
+ 0 0 12px rgba(255, 153, 0, 0.6),
+ 0 0 24px rgba(255, 153, 0, 0.3);
+ background: rgba(255, 255, 255, 0.08);
+}
+
+.subnet-tile.staked {
+ border: 1px solid rgba(255, 153, 0, 0.3);
+}
+
+.subnet-tile.staked::before {
+ content: '';
+ position: absolute;
+ top: 0.4em;
+ right: 0.4em;
+ width: 0.5em;
+ height: 0.5em;
+ border-radius: 50%;
+ background: #FF9900;
+}
+
+/* ===================== Subnet Detail Page Header ===================== */
+.subnet-header {
+ padding: 16px;
+ border-radius: 12px;
+ margin-bottom: 0px;
+}
+
+.subnet-header h2 {
+ margin: 0;
+ font-size: 1.3em;
+}
+
+.subnet-price {
+ font-size: 1.3em;
+ color: #FF9900;
+}
+
+.subnet-title-row {
+ display: grid;
+ grid-template-columns: 300px 1fr 300px;
+ align-items: start;
+ margin: 0;
+ position: relative;
+ min-height: 60px;
+}
+
+.title-price {
+ grid-column: 1;
+ padding-top: 0;
+ margin-top: -10px;
+}
+
+.header-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ margin-bottom: 16px;
+}
+
+.toggle-group {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ align-items: flex-end;
+}
+
+.toggle-label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ color: rgba(255, 255, 255, 0.7);
+ font-size: 0.9em;
+ cursor: pointer;
+ user-select: none;
+}
+
+.back-button {
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ color: rgba(255, 255, 255, 0.8);
+ padding: 8px 16px;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: 0.9em;
+ transition: all 0.2s ease;
+ margin-bottom: 16px;
+}
+
+.back-button:hover {
+ background: rgba(255, 255, 255, 0.1);
+ border-color: rgba(255, 255, 255, 0.2);
+}
+
+/* ===================== Network Visualization ===================== */
+.network-visualization-container {
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
+ top: -50px;
+ width: 700px;
+ height: 80px;
+ z-index: 1;
+}
+
+.network-visualization {
+ width: 700px;
+ height: 80px;
+ position: relative;
+}
+
+#network-canvas {
+ background: transparent;
+ position: relative;
+ z-index: 1;
+}
+
+/* Gradient behind visualization */
+.network-visualization::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 100%;
+ background: linear-gradient(to bottom, rgba(0, 0, 0, 0.95) 0%, rgba(0, 0, 0, 0.8) 100%);
+ z-index: 0;
+ pointer-events: none;
+}
+
+/* ===================== Subnet Detail Metrics ===================== */
+.network-metrics {
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ gap: 12px;
+ margin: 0;
+ margin-top: 16px;
+}
+
+/* Base card styles - applied to both network and metric cards */
+.network-card, .metric-card {
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 8px;
+ padding: 12px 16px;
+ min-height: 50px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 4px;
+}
+
+/* Separate styling for moving price value */
+#network-moving-price {
+ color: #FF9900;
+}
+
+.metrics-section {
+ margin-top: 0px;
+ margin-bottom: 16px;
+}
+
+.metrics-group {
+ display: grid;
+ grid-template-columns: repeat(5, 1fr);
+ gap: 12px;
+ margin: 0;
+ margin-top: 2px;
+}
+
+.market-metrics .metric-card {
+ background: rgba(255, 255, 255, 0.05);
+ min-height: 70px;
+}
+
+.metric-label {
+ font-size: 0.85em;
+ color: rgba(255, 255, 255, 0.7);
+ margin: 0;
+}
+
+.metric-value {
+ font-size: 1.2em;
+ line-height: 1.3;
+ margin: 0;
+}
+
+/* Add status colors */
+.registration-status {
+ color: #2ECC71;
+}
+
+.registration-status.closed {
+ color: #ff4444; /* Red color for closed status */
+}
+
+.cr-status {
+ color: #2ECC71;
+}
+
+.cr-status.disabled {
+ color: #ff4444; /* Red color for disabled status */
+}
+
+/* ===================== Stakes Table ===================== */
+.stakes-container {
+ margin-top: 24px;
+ padding: 0 24px;
+}
+
+.stakes-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16px;
+}
+
+.stakes-header h3 {
+ font-size: 1.2em;
+ color: #ffffff;
+ margin: 0;
+}
+
+.stakes-table-container {
+ background: rgba(255, 255, 255, 0.02);
+ border-radius: 12px;
+ overflow: hidden;
+ margin-bottom: 24px;
+ width: 100%;
+}
+
+.stakes-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.stakes-table th {
+ background: rgba(255, 255, 255, 0.05);
+ padding: 16px;
+ text-align: left;
+ font-weight: 500;
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.stakes-table td {
+ padding: 16px;
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
+}
+
+.stakes-table tr {
+ transition: background-color 0.2s ease;
+}
+
+.stakes-table tr:nth-child(even) {
+ background: rgba(255, 255, 255, 0.02);
+}
+
+.stakes-table tr:hover {
+ background: transparent;
+}
+
+.no-stakes-row td {
+ text-align: center;
+ padding: 32px;
+ color: rgba(255, 255, 255, 0.5);
+}
+
+/* Table styles consistency */
+.stakes-table th, .network-table th {
+ background: rgba(255, 255, 255, 0.05);
+ padding: 16px;
+ text-align: left;
+ font-weight: 500;
+ color: rgba(255, 255, 255, 0.7);
+ transition: color 0.2s ease;
+}
+
+/* Sortable columns */
+.stakes-table th.sortable, .network-table th.sortable {
+ cursor: pointer;
+}
+
+/* Active sort column - only change color */
+.stakes-table th.sortable[data-sort], .network-table th.sortable[data-sort] {
+ color: #FF9900;
+}
+
+/* Hover effects - only change color */
+.stakes-table th.sortable:hover, .network-table th.sortable:hover {
+ color: #FF9900;
+}
+
+/* Remove hover background from table rows */
+.stakes-table tr:hover {
+ background: transparent;
+}
+
+/* ===================== Network Table ===================== */
+.network-table-container {
+ margin-top: 60px;
+ position: relative;
+ z-index: 2;
+ background: rgba(0, 0, 0, 0.8);
+}
+
+.network-table {
+ width: 100%;
+ border-collapse: collapse;
+ table-layout: fixed;
+}
+
+.network-table th {
+ background: rgba(255, 255, 255, 0.05);
+ padding: 16px;
+ text-align: left;
+ font-weight: 500;
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.network-table td {
+ padding: 16px;
+ border-top: 1px solid rgba(255, 255, 255, 0.05);
+}
+
+.network-table tr {
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+}
+
+.network-table tr:hover {
+ background-color: rgba(255, 255, 255, 0.05);
+}
+
+.network-table tr:nth-child(even) {
+ background-color: rgba(255, 255, 255, 0.02);
+}
+
+.network-table tr:nth-child(even):hover {
+ background-color: rgba(255, 255, 255, 0.05);
+}
+
+.network-search-container {
+ display: flex;
+ align-items: center;
+ margin-bottom: 16px;
+ padding: 0 16px;
+}
+
+.network-search {
+ width: 100%;
+ padding: 12px 16px;
+ border: 1px solid rgba(255, 153, 0, 0.2);
+ border-radius: 8px;
+ background: rgba(0, 0, 0, 0.2);
+ color: #ffffff;
+ font-size: 0.95em;
+ transition: all 0.2s ease;
+}
+
+.network-search:focus {
+ outline: none;
+ border-color: rgba(255, 153, 0, 0.5);
+ background: rgba(0, 0, 0, 0.3);
+ caret-color: #FF9900;
+}
+
+.network-search::placeholder {
+ color: rgba(255, 255, 255, 0.3);
+}
+
+/* ===================== Cell Styles & Formatting ===================== */
+.hotkey-cell {
+ max-width: 200px;
+ position: relative;
+}
+
+.hotkey-container {
+ position: relative;
+ display: inline-block;
+ max-width: 100%;
+}
+
+.hotkey-identity, .truncated-address {
+ color: rgba(255, 255, 255, 0.8);
+ display: inline-block;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.copy-button {
+ position: absolute;
+ top: -20px; /* Position above the text */
+ right: 0;
+ background: rgba(255, 153, 0, 0.1);
+ color: rgba(255, 255, 255, 0.6);
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-size: 0.7em;
+ cursor: pointer;
+ opacity: 0;
+ transition: all 0.2s ease;
+ transform: translateY(5px);
+}
+
+.hotkey-container:hover .copy-button {
+ opacity: 1;
+ transform: translateY(0);
+}
+
+.copy-button:hover {
+ background: rgba(255, 153, 0, 0.2);
+ color: #FF9900;
+}
+
+.address-cell {
+ max-width: 150px;
+ position: relative;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.address-container {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ position: relative;
+}
+
+.address-container:hover::after {
+ content: 'Click to copy';
+ position: absolute;
+ right: 0;
+ top: 50%;
+ transform: translateY(-50%);
+ background: rgba(255, 153, 0, 0.1);
+ color: #FF9900;
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-size: 0.8em;
+ opacity: 0.8;
+}
+
+.truncated-address {
+ font-family: monospace;
+ color: rgba(255, 255, 255, 0.8);
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.truncated-address:hover {
+ color: #FF9900;
+}
+
+.registered-yes {
+ color: #FF9900;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.registered-no {
+ color: #ff4444;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.manage-button {
+ background: rgba(255, 153, 0, 0.1);
+ border: 1px solid rgba(255, 153, 0, 0.2);
+ color: #FF9900;
+ padding: 6px 12px;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.manage-button:hover {
+ background: rgba(255, 153, 0, 0.2);
+ transform: translateY(-1px);
+}
+
+.hotkey-identity {
+ display: inline-block;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: #FF9900;
+}
+
+.identity-cell {
+ max-width: 700px;
+ font-size: 0.90em;
+ letter-spacing: -0.2px;
+ color: #FF9900;
+}
+
+.per-day {
+ font-size: 0.75em;
+ opacity: 0.7;
+ margin-left: 4px;
+}
+
+/* ===================== Neuron Detail Panel ===================== */
+#neuron-detail-container {
+ background: rgba(255, 255, 255, 0.02);
+ border-radius: 12px;
+ padding: 16px;
+ margin-top: 16px;
+}
+
+.neuron-detail-header {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ margin-bottom: 16px;
+}
+
+.neuron-detail-content {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.neuron-info-top {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.neuron-keys {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ font-size: 0.9em;
+ color: rgba(255, 255, 255, 0.6);
+ font-size: 1em;
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.neuron-cards-container {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.neuron-metrics-row {
+ display: grid;
+ grid-template-columns: repeat(6, 1fr);
+ gap: 12px;
+ margin: 0;
+}
+
+.neuron-metrics-row.last-row {
+ grid-template-columns: repeat(3, 1fr);
+}
+
+/* IP Info styling */
+#neuron-ipinfo {
+ font-size: 0.85em;
+ line-height: 1.4;
+ white-space: nowrap;
+}
+
+#neuron-ipinfo .no-connection {
+ color: #ff4444;
+ font-weight: 500;
+}
+
+/* Adjust metric card for IP info to accommodate multiple lines */
+.neuron-cards-container .metric-card:has(#neuron-ipinfo) {
+ min-height: 85px;
+}
+
+/* ===================== Subnet Page Color Overrides ===================== */
+/* Subnet page specific style */
+.subnet-page .metric-card-title,
+.subnet-page .network-card-title {
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.subnet-page .metric-card .metric-value,
+.subnet-page .metric-value {
+ color: white;
+}
+
+/* Green values */
+.subnet-page .validator-true,
+.subnet-page .active-yes,
+.subnet-page .registration-open,
+.subnet-page .cr-enabled,
+.subnet-page .ip-info {
+ color: #FF9900;
+}
+
+/* Red values */
+.subnet-page .validator-false,
+.subnet-page .active-no,
+.subnet-page .registration-closed,
+.subnet-page .cr-disabled,
+.subnet-page .ip-na {
+ color: #ff4444;
+}
+
+/* Keep symbols green in subnet page */
+.subnet-page .symbol {
+ color: #FF9900;
+}
+
+/* ===================== Responsive Styles ===================== */
+@media (max-width: 1200px) {
+ .stakes-table {
+ display: block;
+ overflow-x: auto;
+ }
+
+ .network-metrics {
+ grid-template-columns: repeat(3, 1fr);
+ }
+}
+
+@media (min-width: 1201px) {
+ .network-metrics {
+ grid-template-columns: repeat(5, 1fr);
+ }
+}
+/* ===== Splash Screen ===== */
+#splash-screen {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: #000000;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 999999;
+ opacity: 1;
+ transition: opacity 1s ease;
+}
+
+#splash-screen.fade-out {
+ opacity: 0;
+}
+
+.splash-content {
+ text-align: center;
+ color: #FF9900;
+ opacity: 0;
+ animation: fadeIn 1.2s ease forwards;
+}
+@keyframes fadeIn {
+ 0% {
+ opacity: 0;
+ transform: scale(0.97);
+ }
+ 100% {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+/* Title & text styling */
+.title-row {
+ display: flex;
+ align-items: baseline;
+ gap: 1rem;
+}
+
+.splash-title {
+ font-size: 2.4rem;
+ margin: 0;
+ padding: 0;
+ font-weight: 600;
+ color: #FF9900;
+}
+
+.beta-text {
+ font-size: 0.9rem;
+ color: #FF9900;
+ background: rgba(255, 153, 0, 0.1);
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-weight: 500;
+}
\ No newline at end of file
diff --git a/bittensor_cli/src/bittensor/templates/view.j2 b/bittensor_cli/src/bittensor/templates/view.j2
new file mode 100644
index 000000000..a206096c9
--- /dev/null
+++ b/bittensor_cli/src/bittensor/templates/view.j2
@@ -0,0 +1,43 @@
+
+
+
+
+ Bittensor CLI Interface
+
+
+
+
+
+
+
+
+
+
Btcli View
+ Beta
+
+
+
+
+
+
+ {% include 'main-header.j2' %}
+ {% include 'main-filters.j2' %}
+ {% include 'subnets-table.j2' %}
+
+
+
+
+ {% include 'subnet-details-header.j2' %}
+ {% include 'subnet-metrics.j2' %}
+ {% include 'neuron-details.j2' %}
+
+
+
+
+
\ No newline at end of file
diff --git a/bittensor_cli/src/bittensor/templates/view.js b/bittensor_cli/src/bittensor/templates/view.js
new file mode 100644
index 000000000..1b51130f9
--- /dev/null
+++ b/bittensor_cli/src/bittensor/templates/view.js
@@ -0,0 +1,1053 @@
+/* ===================== Global Variables ===================== */
+const root_symbol_html = 'τ';
+let verboseNumbers = false;
+
+/* ===================== Clipboard Functions ===================== */
+/**
+* Copies text to clipboard and shows visual feedback
+* @param {string} text The text to copy
+* @param {HTMLElement} element Optional element to show feedback on
+*/
+function copyToClipboard(text, element) {
+ navigator.clipboard.writeText(text)
+ .then(() => {
+ const targetElement = element || (event && event.target);
+
+ if (targetElement) {
+ const copyIndicator = targetElement.querySelector('.copy-indicator');
+
+ if (copyIndicator) {
+ const originalText = copyIndicator.textContent;
+ copyIndicator.textContent = 'Copied!';
+ copyIndicator.style.color = '#FF9900';
+
+ setTimeout(() => {
+ copyIndicator.textContent = originalText;
+ copyIndicator.style.color = '';
+ }, 1000);
+ } else {
+ const originalText = targetElement.textContent;
+ targetElement.textContent = 'Copied!';
+ targetElement.style.color = '#FF9900';
+
+ setTimeout(() => {
+ targetElement.textContent = originalText;
+ targetElement.style.color = '';
+ }, 1000);
+ }
+ }
+ })
+ .catch(err => {
+ console.error('Failed to copy:', err);
+ });
+}
+
+
+/* ===================== Initialization and DOMContentLoaded Handler ===================== */
+document.addEventListener('DOMContentLoaded', function() {
+ try {
+ const initialDataElement = document.getElementById('initial-data');
+ if (!initialDataElement) {
+ throw new Error('Initial data element (#initial-data) not found.');
+ }
+ window.initialData = {
+ wallet_info: JSON.parse(initialDataElement.getAttribute('data-wallet-info')),
+ subnets: JSON.parse(initialDataElement.getAttribute('data-subnets'))
+ };
+ } catch (error) {
+ console.error('Error loading initial data:', error);
+ }
+
+ // Return to the main list of subnets.
+ const backButton = document.querySelector('.back-button');
+ if (backButton) {
+ backButton.addEventListener('click', function() {
+ // First check if neuron details are visible and close them if needed
+ const neuronDetails = document.getElementById('neuron-detail-container');
+ if (neuronDetails && neuronDetails.style.display !== 'none') {
+ closeNeuronDetails();
+ return; // Stop here, don't go back to main page yet
+ }
+
+ // Otherwise go back to main subnet list
+ document.getElementById('main-content').style.display = 'block';
+ document.getElementById('subnet-page').style.display = 'none';
+ });
+ }
+
+
+ // Splash screen logic
+ const splash = document.getElementById('splash-screen');
+ const mainContent = document.getElementById('main-content');
+ mainContent.style.display = 'none';
+
+ setTimeout(() => {
+ splash.classList.add('fade-out');
+ splash.addEventListener('transitionend', () => {
+ splash.style.display = 'none';
+ mainContent.style.display = 'block';
+ }, { once: true });
+ }, 2000);
+
+ initializeFormattedNumbers();
+
+ // Keep main page's "verbose" checkbox and the Subnet page's "verbose" checkbox in sync
+ const mainVerboseCheckbox = document.getElementById('show-verbose');
+ const subnetVerboseCheckbox = document.getElementById('verbose-toggle');
+ if (mainVerboseCheckbox && subnetVerboseCheckbox) {
+ mainVerboseCheckbox.addEventListener('change', function() {
+ subnetVerboseCheckbox.checked = this.checked;
+ toggleVerboseNumbers();
+ });
+ subnetVerboseCheckbox.addEventListener('change', function() {
+ mainVerboseCheckbox.checked = this.checked;
+ toggleVerboseNumbers();
+ });
+ }
+
+ // Initialize tile view as default
+ const tilesContainer = document.getElementById('subnet-tiles-container');
+ const tableContainer = document.querySelector('.subnets-table-container');
+
+ // Generate and show tiles
+ generateSubnetTiles();
+ tilesContainer.style.display = 'flex';
+ tableContainer.style.display = 'none';
+});
+
+/* ===================== Main Page Functions ===================== */
+/**
+* Sort the main Subnets table by the specified column index.
+* Toggles ascending/descending on each click.
+* @param {number} columnIndex Index of the column to sort.
+*/
+function sortMainTable(columnIndex) {
+ const table = document.querySelector('.subnets-table');
+ const headers = table.querySelectorAll('th');
+ const header = headers[columnIndex];
+
+ // Determine new sort direction
+ let isDescending = header.getAttribute('data-sort') !== 'desc';
+
+ // Clear sort markers on all columns, then set the new one
+ headers.forEach(th => { th.removeAttribute('data-sort'); });
+ header.setAttribute('data-sort', isDescending ? 'desc' : 'asc');
+
+ // Sort rows based on numeric value (or netuid in col 0)
+ const tbody = table.querySelector('tbody');
+ const rows = Array.from(tbody.querySelectorAll('tr'));
+ rows.sort((rowA, rowB) => {
+ const cellA = rowA.cells[columnIndex];
+ const cellB = rowB.cells[columnIndex];
+
+ // Special handling for the first column with netuid in data-value
+ if (columnIndex === 0) {
+ const netuidA = parseInt(cellA.getAttribute('data-value'), 10);
+ const netuidB = parseInt(cellB.getAttribute('data-value'), 10);
+ return isDescending ? (netuidB - netuidA) : (netuidA - netuidB);
+ }
+
+ // Otherwise parse float from data-value
+ const valueA = parseFloat(cellA.getAttribute('data-value')) || 0;
+ const valueB = parseFloat(cellB.getAttribute('data-value')) || 0;
+ return isDescending ? (valueB - valueA) : (valueA - valueB);
+ });
+
+ // Re-inject rows in sorted order
+ tbody.innerHTML = '';
+ rows.forEach(row => tbody.appendChild(row));
+}
+
+/**
+* Filters the main Subnets table rows based on user search and "Show Only Staked" checkbox.
+*/
+function filterSubnets() {
+ const searchText = document.getElementById('subnet-search').value.toLowerCase();
+ const showStaked = document.getElementById('show-staked').checked;
+ const showTiles = document.getElementById('show-tiles').checked;
+
+ // Filter table rows
+ const rows = document.querySelectorAll('.subnet-row');
+ rows.forEach(row => {
+ const name = row.querySelector('.subnet-name').textContent.toLowerCase();
+ const stakeStatus = row.querySelector('.stake-status').textContent; // "Staked" or "Not Staked"
+
+ let isVisible = name.includes(searchText);
+ if (showStaked) {
+ // If "Show only Staked" is checked, the row must have "Staked" to be visible
+ isVisible = isVisible && (stakeStatus === 'Staked');
+ }
+ row.style.display = isVisible ? '' : 'none';
+ });
+
+ // Filter tiles if they're being shown
+ if (showTiles) {
+ const tiles = document.querySelectorAll('.subnet-tile');
+ tiles.forEach(tile => {
+ const name = tile.querySelector('.tile-name').textContent.toLowerCase();
+ const netuid = tile.querySelector('.tile-netuid').textContent;
+ const isStaked = tile.classList.contains('staked');
+
+ let isVisible = name.includes(searchText) || netuid.includes(searchText);
+ if (showStaked) {
+ isVisible = isVisible && isStaked;
+ }
+ tile.style.display = isVisible ? '' : 'none';
+ });
+ }
+}
+
+
+/* ===================== Subnet Detail Page Functions ===================== */
+/**
+* Displays the Subnet page (detailed view) for the selected netuid.
+* Hides the main content and populates all the metrics / stakes / network table.
+* @param {number} netuid The netuid of the subnet to show in detail.
+*/
+function showSubnetPage(netuid) {
+ try {
+ window.currentSubnet = netuid;
+ window.scrollTo(0, 0);
+
+ const subnet = window.initialData.subnets.find(s => s.netuid === parseInt(netuid, 10));
+ if (!subnet) {
+ throw new Error(`Subnet not found for netuid: ${netuid}`);
+ }
+ window.currentSubnetSymbol = subnet.symbol;
+
+ // Insert the "metagraph" table beneath the "stakes" table in the hidden container
+ const networkTableHTML = `
+
+
+
+
+
+
+
+ Name
+ Stake Weight
+ Stake ${subnet.symbol}
+ Stake ${root_symbol_html}
+ Dividends
+ Incentive
+ Emissions /day
+ Hotkey
+ Coldkey
+
+
+
+ ${generateNetworkTableRows(subnet.metagraph_info)}
+
+
+
+ `;
+
+ // Show/hide main content vs. subnet detail
+ document.getElementById('main-content').style.display = 'none';
+ document.getElementById('subnet-page').style.display = 'block';
+
+ document.querySelector('#subnet-title').textContent = `${subnet.netuid} - ${subnet.name}`;
+ document.querySelector('#subnet-price').innerHTML = formatNumber(subnet.price, subnet.symbol);
+ document.querySelector('#subnet-market-cap').innerHTML = formatNumber(subnet.market_cap, root_symbol_html);
+ document.querySelector('#subnet-total-stake').innerHTML= formatNumber(subnet.total_stake, subnet.symbol);
+ document.querySelector('#subnet-emission').innerHTML = formatNumber(subnet.emission, root_symbol_html);
+
+
+ const metagraphInfo = subnet.metagraph_info;
+ document.querySelector('#network-alpha-in').innerHTML = formatNumber(metagraphInfo.alpha_in, subnet.symbol);
+ document.querySelector('#network-tau-in').innerHTML = formatNumber(metagraphInfo.tao_in, root_symbol_html);
+ document.querySelector('#network-moving-price').innerHTML = formatNumber(metagraphInfo.moving_price, subnet.symbol);
+
+ // Registration status
+ const registrationElement = document.querySelector('#network-registration');
+ registrationElement.textContent = metagraphInfo.registration_allowed ? 'Open' : 'Closed';
+ registrationElement.classList.toggle('closed', !metagraphInfo.registration_allowed);
+
+ // Commit-Reveal Weight status
+ const crElement = document.querySelector('#network-cr');
+ crElement.textContent = metagraphInfo.commit_reveal_weights_enabled ? 'Enabled' : 'Disabled';
+ crElement.classList.toggle('disabled', !metagraphInfo.commit_reveal_weights_enabled);
+
+ // Blocks since last step, out of tempo
+ document.querySelector('#network-blocks-since-step').innerHTML =
+ `${metagraphInfo.blocks_since_last_step}/${metagraphInfo.tempo}`;
+
+ // Number of neurons vs. max
+ document.querySelector('#network-neurons').innerHTML =
+ `${metagraphInfo.num_uids}/${metagraphInfo.max_uids}`;
+
+ // Update "Your Stakes" table
+ const stakesTableBody = document.querySelector('#stakes-table-body');
+ stakesTableBody.innerHTML = '';
+ if (subnet.your_stakes && subnet.your_stakes.length > 0) {
+ subnet.your_stakes.forEach(stake => {
+ const row = document.createElement('tr');
+ row.innerHTML = `
+
+
+ ${stake.hotkey_identity}
+
+ copy
+
+
+ ${formatNumber(stake.amount, subnet.symbol)}
+ ${formatNumber(stake.ideal_value, root_symbol_html)}
+ ${formatNumber(stake.slippage_value, root_symbol_html)} (${stake.slippage_percentage.toFixed(2)}%)
+ ${formatNumber(stake.emission, subnet.symbol + '/day')}
+ ${formatNumber(stake.tao_emission, root_symbol_html + '/day')}
+
+
+ ${stake.is_registered ? 'Yes' : 'No'}
+
+
+
+ Coming soon
+
+ `;
+ stakesTableBody.appendChild(row);
+ });
+ } else {
+ // If no user stake in this subnet
+ stakesTableBody.innerHTML = `
+
+ No stakes found for this subnet
+
+ `;
+ }
+
+ // Remove any previously injected network table then add the new one
+ const existingNetworkTable = document.querySelector('.network-table-container');
+ if (existingNetworkTable) {
+ existingNetworkTable.remove();
+ }
+ document.querySelector('.stakes-table-container').insertAdjacentHTML('afterend', networkTableHTML);
+
+ // Format the new numbers
+ initializeFormattedNumbers();
+
+ // Initialize connectivity visualization (the dots / lines "animation")
+ setTimeout(() => { initNetworkVisualization(); }, 100);
+
+ // Toggle whether we are showing the "Your Stakes" or "Metagraph" table
+ toggleStakeView();
+
+ // Initialize sorting on newly injected table columns
+ initializeSorting();
+
+ // Auto-sort by Stake descending on the network table for convenience
+ setTimeout(() => {
+ const networkTable = document.querySelector('.network-table');
+ if (networkTable) {
+ const stakeColumn = networkTable.querySelector('th:nth-child(2)');
+ if (stakeColumn) {
+ sortTable(networkTable, 1, stakeColumn, true);
+ stakeColumn.setAttribute('data-sort', 'desc');
+ }
+ }
+ }, 100);
+
+ console.log('Subnet page updated successfully');
+ } catch (error) {
+ console.error('Error updating subnet page:', error);
+ }
+}
+
+/**
+* Generates the rows for the "Neurons" table (shown when the user unchecks "Show Stakes").
+* Each row, when clicked, calls showNeuronDetails(i).
+* @param {Object} metagraphInfo The "metagraph_info" of the subnet that holds hotkeys, etc.
+*/
+function generateNetworkTableRows(metagraphInfo) {
+ const rows = [];
+ console.log('Generating network table rows with data:', metagraphInfo);
+
+ for (let i = 0; i < metagraphInfo.hotkeys.length; i++) {
+ // Subnet symbol is used to show token vs. root stake
+ const subnet = window.initialData.subnets.find(s => s.netuid === window.currentSubnet);
+ const subnetSymbol = subnet ? subnet.symbol : '';
+
+ // Possibly show hotkey/coldkey truncated for readability
+ const truncatedHotkey = truncateAddress(metagraphInfo.hotkeys[i]);
+ const truncatedColdkey = truncateAddress(metagraphInfo.coldkeys[i]);
+ const identityName = metagraphInfo.updated_identities[i] || '~';
+
+ // Root stake is being scaled by 0.18 arbitrarily here
+ const adjustedRootStake = metagraphInfo.tao_stake[i] * 0.18;
+
+ rows.push(`
+
+ ${identityName}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${truncatedHotkey}
+ copy
+
+
+
+
+ ${truncatedColdkey}
+ copy
+
+
+
+ `);
+ }
+ return rows.join('');
+}
+
+/**
+* Handles toggling between the "Your Stakes" view and the "Neurons" view on the Subnet page.
+* The "Show Stakes" checkbox (#stake-toggle) controls which table is visible.
+*/
+function toggleStakeView() {
+ const showStakes = document.getElementById('stake-toggle').checked;
+ const stakesTable = document.querySelector('.stakes-table-container');
+ const networkTable = document.querySelector('.network-table-container');
+ const sectionHeader = document.querySelector('.view-header');
+ const neuronDetails = document.getElementById('neuron-detail-container');
+ const addStakeButton = document.querySelector('.add-stake-button');
+ const exportCsvButton = document.querySelector('.export-csv-button');
+ const stakesHeader = document.querySelector('.stakes-header');
+
+ // First, close neuron details if they're open
+ if (neuronDetails && neuronDetails.style.display !== 'none') {
+ neuronDetails.style.display = 'none';
+ }
+
+ // Always show the section header and stakes header when toggling views
+ if (sectionHeader) sectionHeader.style.display = 'block';
+ if (stakesHeader) stakesHeader.style.display = 'flex';
+
+ if (showStakes) {
+ // Show the Stakes table, hide the Neurons table
+ stakesTable.style.display = 'block';
+ networkTable.style.display = 'none';
+ sectionHeader.textContent = 'Your Stakes';
+ if (addStakeButton) {
+ addStakeButton.style.display = 'none';
+ }
+ if (exportCsvButton) {
+ exportCsvButton.style.display = 'none';
+ }
+ } else {
+ // Show the Neurons table, hide the Stakes table
+ stakesTable.style.display = 'none';
+ networkTable.style.display = 'block';
+ sectionHeader.textContent = 'Metagraph';
+ if (addStakeButton) {
+ addStakeButton.style.display = 'block';
+ }
+ if (exportCsvButton) {
+ exportCsvButton.style.display = 'block';
+ }
+ }
+}
+
+/**
+* Called when you click a row in the "Neurons" table, to display more detail about that neuron.
+* This hides the "Neurons" table and shows the #neuron-detail-container.
+* @param {number} rowIndex The index of the neuron in the arrays (hotkeys, coldkeys, etc.)
+*/
+function showNeuronDetails(rowIndex) {
+ try {
+ // Hide the network table & stakes table
+ const networkTable = document.querySelector('.network-table-container');
+ if (networkTable) networkTable.style.display = 'none';
+ const stakesTable = document.querySelector('.stakes-table-container');
+ if (stakesTable) stakesTable.style.display = 'none';
+
+ // Hide the stakes header with the action buttons
+ const stakesHeader = document.querySelector('.stakes-header');
+ if (stakesHeader) stakesHeader.style.display = 'none';
+
+ // Hide the view header that says "Neurons"
+ const viewHeader = document.querySelector('.view-header');
+ if (viewHeader) viewHeader.style.display = 'none';
+
+ // Show the neuron detail panel
+ const detailContainer = document.getElementById('neuron-detail-container');
+ if (detailContainer) detailContainer.style.display = 'block';
+
+ // Pull out the current subnet
+ const subnet = window.initialData.subnets.find(s => s.netuid === window.currentSubnet);
+ if (!subnet) {
+ console.error('No subnet data for netuid:', window.currentSubnet);
+ return;
+ }
+
+ const metagraphInfo = subnet.metagraph_info;
+ const subnetSymbol = subnet.symbol || '';
+
+ // Pull axon data, for IP info
+ const axonData = metagraphInfo.processed_axons ? metagraphInfo.processed_axons[rowIndex] : null;
+ let ipInfoString;
+
+ // Update IP info card - hide header if IP info is present
+ const ipInfoCard = document.getElementById('neuron-ipinfo').closest('.metric-card');
+ if (axonData && axonData.ip !== 'N/A') {
+ // If we have valid IP info, hide the "IP Info" label
+ if (ipInfoCard && ipInfoCard.querySelector('.metric-label')) {
+ ipInfoCard.querySelector('.metric-label').style.display = 'none';
+ }
+ // Format IP info with green labels
+ ipInfoString = `IP: ${axonData.ip} ` +
+ `Port: ${axonData.port} ` +
+ `Type: ${axonData.ip_type}`;
+ } else {
+ // If no IP info, show the label
+ if (ipInfoCard && ipInfoCard.querySelector('.metric-label')) {
+ ipInfoCard.querySelector('.metric-label').style.display = 'block';
+ }
+ ipInfoString = 'N/A ';
+ }
+
+ // Basic identity and hotkey/coldkey info
+ const name = metagraphInfo.updated_identities[rowIndex] || '~';
+ const hotkey = metagraphInfo.hotkeys[rowIndex];
+ const coldkey = metagraphInfo.coldkeys[rowIndex];
+ const rank = metagraphInfo.rank ? metagraphInfo.rank[rowIndex] : 0;
+ const trust = metagraphInfo.trust ? metagraphInfo.trust[rowIndex] : 0;
+ const pruning = metagraphInfo.pruning_score ? metagraphInfo.pruning_score[rowIndex] : 0;
+ const vPermit = metagraphInfo.validator_permit ? metagraphInfo.validator_permit[rowIndex] : false;
+ const lastUpd = metagraphInfo.last_update ? metagraphInfo.last_update[rowIndex] : 0;
+ const consensus = metagraphInfo.consensus ? metagraphInfo.consensus[rowIndex] : 0;
+ const regBlock = metagraphInfo.block_at_registration ? metagraphInfo.block_at_registration[rowIndex] : 0;
+ const active = metagraphInfo.active ? metagraphInfo.active[rowIndex] : false;
+
+ // Update UI fields
+ document.getElementById('neuron-name').textContent = name;
+ document.getElementById('neuron-name').style.color = '#FF9900';
+
+ document.getElementById('neuron-hotkey').textContent = hotkey;
+ document.getElementById('neuron-coldkey').textContent = coldkey;
+ document.getElementById('neuron-trust').textContent = trust.toFixed(4);
+ document.getElementById('neuron-pruning-score').textContent = pruning.toFixed(4);
+
+ // Validator
+ const validatorElem = document.getElementById('neuron-validator-permit');
+ if (vPermit) {
+ validatorElem.style.color = '#2ECC71';
+ validatorElem.textContent = 'True';
+ } else {
+ validatorElem.style.color = '#ff4444';
+ validatorElem.textContent = 'False';
+ }
+
+ document.getElementById('neuron-last-update').textContent = lastUpd;
+ document.getElementById('neuron-consensus').textContent = consensus.toFixed(4);
+ document.getElementById('neuron-reg-block').textContent = regBlock;
+ document.getElementById('neuron-ipinfo').innerHTML = ipInfoString;
+
+ const activeElem = document.getElementById('neuron-active');
+ if (active) {
+ activeElem.style.color = '#2ECC71';
+ activeElem.textContent = 'Yes';
+ } else {
+ activeElem.style.color = '#ff4444';
+ activeElem.textContent = 'No';
+ }
+
+ // Add stake data ("total_stake", "alpha_stake", "tao_stake")
+ document.getElementById('neuron-stake-total').setAttribute(
+ 'data-value', metagraphInfo.total_stake[rowIndex]
+ );
+ document.getElementById('neuron-stake-total').setAttribute(
+ 'data-symbol', subnetSymbol
+ );
+
+ document.getElementById('neuron-stake-token').setAttribute(
+ 'data-value', metagraphInfo.alpha_stake[rowIndex]
+ );
+ document.getElementById('neuron-stake-token').setAttribute(
+ 'data-symbol', subnetSymbol
+ );
+
+ // Multiply tao_stake by 0.18
+ const originalStakeRoot = metagraphInfo.tao_stake[rowIndex];
+ const calculatedStakeRoot = originalStakeRoot * 0.18;
+
+ document.getElementById('neuron-stake-root').setAttribute(
+ 'data-value', calculatedStakeRoot
+ );
+ document.getElementById('neuron-stake-root').setAttribute(
+ 'data-symbol', root_symbol_html
+ );
+ // Also set the inner text right away, so we show a correct format on load
+ document.getElementById('neuron-stake-root').innerHTML =
+ formatNumber(calculatedStakeRoot, root_symbol_html);
+
+ // Dividends, Incentive
+ document.getElementById('neuron-dividends').setAttribute(
+ 'data-value', metagraphInfo.dividends[rowIndex]
+ );
+ document.getElementById('neuron-dividends').setAttribute('data-symbol', '');
+
+ document.getElementById('neuron-incentive').setAttribute(
+ 'data-value', metagraphInfo.incentives[rowIndex]
+ );
+ document.getElementById('neuron-incentive').setAttribute('data-symbol', '');
+
+ // Emissions
+ document.getElementById('neuron-emissions').setAttribute(
+ 'data-value', metagraphInfo.emission[rowIndex]
+ );
+ document.getElementById('neuron-emissions').setAttribute('data-symbol', subnetSymbol);
+
+ // Rank
+ document.getElementById('neuron-rank').textContent = rank.toFixed(4);
+
+ // Re-run formatting so the newly updated data-values appear in numeric form
+ initializeFormattedNumbers();
+ } catch (err) {
+ console.error('Error showing neuron details:', err);
+ }
+}
+
+/**
+* Closes the neuron detail panel and goes back to whichever table was selected ("Stakes" or "Metagraph").
+*/
+function closeNeuronDetails() {
+ // Hide neuron details
+ const detailContainer = document.getElementById('neuron-detail-container');
+ if (detailContainer) detailContainer.style.display = 'none';
+
+ // Show the stakes header with action buttons
+ const stakesHeader = document.querySelector('.stakes-header');
+ if (stakesHeader) stakesHeader.style.display = 'flex';
+
+ // Show the view header again
+ const viewHeader = document.querySelector('.view-header');
+ if (viewHeader) viewHeader.style.display = 'block';
+
+ // Show the appropriate table based on toggle state
+ const showStakes = document.getElementById('stake-toggle').checked;
+ const stakesTable = document.querySelector('.stakes-table-container');
+ const networkTable = document.querySelector('.network-table-container');
+
+ if (showStakes) {
+ stakesTable.style.display = 'block';
+ networkTable.style.display = 'none';
+
+ // Hide action buttons when showing stakes
+ const addStakeButton = document.querySelector('.add-stake-button');
+ const exportCsvButton = document.querySelector('.export-csv-button');
+ if (addStakeButton) addStakeButton.style.display = 'none';
+ if (exportCsvButton) exportCsvButton.style.display = 'none';
+ } else {
+ stakesTable.style.display = 'none';
+ networkTable.style.display = 'block';
+
+ // Show action buttons when showing metagraph
+ const addStakeButton = document.querySelector('.add-stake-button');
+ const exportCsvButton = document.querySelector('.export-csv-button');
+ if (addStakeButton) addStakeButton.style.display = 'block';
+ if (exportCsvButton) exportCsvButton.style.display = 'block';
+ }
+}
+
+
+/* ===================== Number Formatting Functions ===================== */
+/**
+ * Toggles the numeric display between "verbose" and "short" notations
+ * across all .formatted-number elements on the page.
+ */
+function toggleVerboseNumbers() {
+ // We read from the main or subnet checkboxes
+ verboseNumbers =
+ document.getElementById('verbose-toggle')?.checked ||
+ document.getElementById('show-verbose')?.checked ||
+ false;
+
+ // Reformat all visible .formatted-number elements
+ document.querySelectorAll('.formatted-number').forEach(element => {
+ const value = parseFloat(element.dataset.value);
+ const symbol = element.dataset.symbol;
+ element.innerHTML = formatNumber(value, symbol);
+ });
+
+ // If we're currently on the Subnet detail page, update those numbers too
+ if (document.getElementById('subnet-page').style.display !== 'none') {
+ updateAllNumbers();
+ }
+}
+
+/**
+ * Scans all .formatted-number elements and replaces their text with
+ * the properly formatted version (short or verbose).
+ */
+function initializeFormattedNumbers() {
+ document.querySelectorAll('.formatted-number').forEach(element => {
+ const value = parseFloat(element.dataset.value);
+ const symbol = element.dataset.symbol;
+ element.innerHTML = formatNumber(value, symbol);
+ });
+}
+
+/**
+ * Called by toggleVerboseNumbers() to reformat key metrics on the Subnet page
+ * that might not be directly wrapped in .formatted-number but need to be updated anyway.
+ */
+function updateAllNumbers() {
+ try {
+ const subnet = window.initialData.subnets.find(s => s.netuid === window.currentSubnet);
+ if (!subnet) {
+ console.error('Could not find subnet data for netuid:', window.currentSubnet);
+ return;
+ }
+ // Reformat a few items in the Subnet detail header
+ document.querySelector('#subnet-market-cap').innerHTML =
+ formatNumber(subnet.market_cap, root_symbol_html);
+ document.querySelector('#subnet-total-stake').innerHTML =
+ formatNumber(subnet.total_stake, subnet.symbol);
+ document.querySelector('#subnet-emission').innerHTML =
+ formatNumber(subnet.emission, root_symbol_html);
+
+ // Reformat the Metagraph table data
+ const netinfo = subnet.metagraph_info;
+ document.querySelector('#network-alpha-in').innerHTML =
+ formatNumber(netinfo.alpha_in, subnet.symbol);
+ document.querySelector('#network-tau-in').innerHTML =
+ formatNumber(netinfo.tao_in, root_symbol_html);
+
+ // Reformat items in "Your Stakes" table
+ document.querySelectorAll('#stakes-table-body .formatted-number').forEach(element => {
+ const value = parseFloat(element.dataset.value);
+ const symbol = element.dataset.symbol;
+ element.innerHTML = formatNumber(value, symbol);
+ });
+ } catch (error) {
+ console.error('Error updating numbers:', error);
+ }
+}
+
+/**
+* Format a numeric value into either:
+* - a short format (e.g. 1.23k, 3.45m) if verboseNumbers==false
+* - a more precise format (1,234.5678) if verboseNumbers==true
+* @param {number} num The numeric value to format.
+* @param {string} symbol A short suffix or currency symbol (e.g. 'τ') that we append.
+*/
+function formatNumber(num, symbol = '') {
+ if (num === undefined || num === null || isNaN(num)) {
+ return '0.00 ' + `${symbol} `;
+ }
+ num = parseFloat(num);
+ if (num === 0) {
+ return '0.00 ' + `${symbol} `;
+ }
+
+ // If user requested verbose
+ if (verboseNumbers) {
+ return num.toLocaleString('en-US', {
+ minimumFractionDigits: 4,
+ maximumFractionDigits: 4
+ }) + ' ' + `${symbol} `;
+ }
+
+ // Otherwise show short scale for large numbers
+ const absNum = Math.abs(num);
+ if (absNum >= 1000) {
+ const suffixes = ['', 'k', 'm', 'b', 't'];
+ const magnitude = Math.min(4, Math.floor(Math.log10(absNum) / 3));
+ const scaledNum = num / Math.pow(10, magnitude * 3);
+ return scaledNum.toFixed(2) + suffixes[magnitude] + ' ' +
+ `${symbol} `;
+ } else {
+ // For small numbers <1000, just show 4 decimals
+ return num.toFixed(4) + ' ' + `${symbol} `;
+ }
+}
+
+/**
+* Truncates a string address into the format "ABC..XYZ" for a bit more readability
+* @param {string} address
+* @returns {string} truncated address form
+*/
+function truncateAddress(address) {
+ if (!address || address.length <= 7) {
+ return address; // no need to truncate if very short
+ }
+ return `${address.substring(0, 3)}..${address.substring(address.length - 3)}`;
+}
+
+/**
+* Format a number in compact notation (K, M, B) for tile display
+*/
+function formatTileNumbers(num) {
+ if (num >= 1000000000) {
+ return (num / 1000000000).toFixed(1) + 'B';
+ } else if (num >= 1000000) {
+ return (num / 1000000).toFixed(1) + 'M';
+ } else if (num >= 1000) {
+ return (num / 1000).toFixed(1) + 'K';
+ } else {
+ return num.toFixed(1);
+ }
+}
+
+
+/* ===================== Table Sorting and Filtering Functions ===================== */
+/**
+* Switches the Metagraph or Stakes table from sorting ascending to descending on a column, and vice versa.
+* @param {HTMLTableElement} table The table element itself
+* @param {number} columnIndex The column index to sort by
+* @param {HTMLTableHeaderCellElement} header The element clicked
+* @param {boolean} forceDescending If true and no existing sort marker, will do a descending sort by default
+*/
+function sortTable(table, columnIndex, header, forceDescending = false) {
+ const tbody = table.querySelector('tbody');
+ const rows = Array.from(tbody.querySelectorAll('tr'));
+
+ // If forcing descending and the header has no 'data-sort', default to 'desc'
+ let isDescending;
+ if (forceDescending && !header.hasAttribute('data-sort')) {
+ isDescending = true;
+ } else {
+ isDescending = header.getAttribute('data-sort') !== 'desc';
+ }
+
+ // Clear data-sort from all headers in the table
+ table.querySelectorAll('th').forEach(th => {
+ th.removeAttribute('data-sort');
+ });
+ // Mark the clicked header with new direction
+ header.setAttribute('data-sort', isDescending ? 'desc' : 'asc');
+
+ // Sort numerically
+ rows.sort((rowA, rowB) => {
+ const cellA = rowA.cells[columnIndex];
+ const cellB = rowB.cells[columnIndex];
+
+ // Attempt to parse float from data-value or fallback to textContent
+ let valueA = parseFloat(cellA.getAttribute('data-value')) ||
+ parseFloat(cellA.textContent.replace(/[^\\d.-]/g, '')) ||
+ 0;
+ let valueB = parseFloat(cellB.getAttribute('data-value')) ||
+ parseFloat(cellB.textContent.replace(/[^\\d.-]/g, '')) ||
+ 0;
+
+ return isDescending ? (valueB - valueA) : (valueA - valueB);
+ });
+
+ // Reinsert sorted rows
+ tbody.innerHTML = '';
+ rows.forEach(row => tbody.appendChild(row));
+}
+
+/**
+* Adds sortable behavior to certain columns in the "stakes-table" or "network-table".
+* Called after these tables are created in showSubnetPage().
+*/
+function initializeSorting() {
+ const networkTable = document.querySelector('.network-table');
+ if (networkTable) {
+ initializeTableSorting(networkTable);
+ }
+ const stakesTable = document.querySelector('.stakes-table');
+ if (stakesTable) {
+ initializeTableSorting(stakesTable);
+ }
+}
+
+/**
+* Helper function that attaches sort handlers to appropriate columns in a table.
+* @param {HTMLTableElement} table The table element to set up sorting for.
+*/
+function initializeTableSorting(table) {
+ const headers = table.querySelectorAll('th');
+ headers.forEach((header, index) => {
+ // We only want some columns to be sortable, as in original code
+ if (table.classList.contains('stakes-table') && index >= 1 && index <= 5) {
+ header.classList.add('sortable');
+ header.addEventListener('click', () => {
+ sortTable(table, index, header, true);
+ });
+ } else if (table.classList.contains('network-table') && index < 6) {
+ header.classList.add('sortable');
+ header.addEventListener('click', () => {
+ sortTable(table, index, header, true);
+ });
+ }
+ });
+}
+
+/**
+* Filters rows in the Metagraph table by name, hotkey, or coldkey.
+* Invoked by the oninput event of the #network-search field.
+* @param {string} searchValue The substring typed by the user.
+*/
+function filterNetworkTable(searchValue) {
+ const searchTerm = searchValue.toLowerCase().trim();
+ const rows = document.querySelectorAll('.network-table tbody tr');
+
+ rows.forEach(row => {
+ const nameCell = row.querySelector('.identity-cell');
+ const hotkeyContainer = row.querySelector('.hotkey-container[data-full-address]');
+ const coldkeyContainer = row.querySelectorAll('.hotkey-container[data-full-address]')[1];
+
+ const name = nameCell ? nameCell.textContent.toLowerCase() : '';
+ const hotkey = hotkeyContainer ? hotkeyContainer.getAttribute('data-full-address').toLowerCase() : '';
+ const coldkey= coldkeyContainer ? coldkeyContainer.getAttribute('data-full-address').toLowerCase() : '';
+
+ const matches = (name.includes(searchTerm) || hotkey.includes(searchTerm) || coldkey.includes(searchTerm));
+ row.style.display = matches ? '' : 'none';
+ });
+}
+
+
+/* ===================== Network Visualization Functions ===================== */
+/**
+* Initializes the network visualization on the canvas element.
+*/
+function initNetworkVisualization() {
+ try {
+ const canvas = document.getElementById('network-canvas');
+ if (!canvas) {
+ console.error('Canvas element (#network-canvas) not found');
+ return;
+ }
+ const ctx = canvas.getContext('2d');
+
+ const subnet = window.initialData.subnets.find(s => s.netuid === window.currentSubnet);
+ if (!subnet) {
+ console.error('Could not find subnet data for netuid:', window.currentSubnet);
+ return;
+ }
+ const numNeurons = subnet.metagraph_info.num_uids;
+ const nodes = [];
+
+ // Randomly place nodes, each with a small velocity
+ for (let i = 0; i < numNeurons; i++) {
+ nodes.push({
+ x: Math.random() * canvas.width,
+ y: Math.random() * canvas.height,
+ radius: 2,
+ vx: (Math.random() - 0.5) * 0.5,
+ vy: (Math.random() - 0.5) * 0.5
+ });
+ }
+
+ // Animation loop
+ function animate() {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ ctx.beginPath();
+ ctx.strokeStyle = 'rgba(255, 153, 0, 0.2)';
+ for (let i = 0; i < nodes.length; i++) {
+ for (let j = i + 1; j < nodes.length; j++) {
+ const dx = nodes[i].x - nodes[j].x;
+ const dy = nodes[i].y - nodes[j].y;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+ if (distance < 30) {
+ ctx.moveTo(nodes[i].x, nodes[i].y);
+ ctx.lineTo(nodes[j].x, nodes[j].y);
+ }
+ }
+ }
+ ctx.stroke();
+
+ nodes.forEach(node => {
+ node.x += node.vx;
+ node.y += node.vy;
+
+ // Bounce them off the edges
+ if (node.x <= 0 || node.x >= canvas.width) node.vx *= -1;
+ if (node.y <= 0 || node.y >= canvas.height) node.vy *= -1;
+
+ ctx.beginPath();
+ ctx.fillStyle = '#FF9900';
+ ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2);
+ ctx.fill();
+ });
+
+ requestAnimationFrame(animate);
+ }
+ animate();
+ } catch (error) {
+ console.error('Error in network visualization:', error);
+ }
+}
+
+
+/* ===================== Tile View Functions ===================== */
+/**
+* Toggles between the tile view and table view of subnets.
+ */
+function toggleTileView() {
+ const showTiles = document.getElementById('show-tiles').checked;
+ const tilesContainer = document.getElementById('subnet-tiles-container');
+ const tableContainer = document.querySelector('.subnets-table-container');
+
+ if (showTiles) {
+ // Show tiles, hide table
+ tilesContainer.style.display = 'flex';
+ tableContainer.style.display = 'none';
+
+ // Generate tiles if they don't exist yet
+ if (tilesContainer.children.length === 0) {
+ generateSubnetTiles();
+ }
+
+ // Apply current filters to the tiles
+ filterSubnets();
+ } else {
+ // Show table, hide tiles
+ tilesContainer.style.display = 'none';
+ tableContainer.style.display = 'block';
+ }
+}
+
+/**
+* Generates the subnet tiles based on the initialData.
+ */
+function generateSubnetTiles() {
+ const tilesContainer = document.getElementById('subnet-tiles-container');
+ tilesContainer.innerHTML = ''; // Clear existing tiles
+
+ // Sort subnets by market cap (descending)
+ const sortedSubnets = [...window.initialData.subnets].sort((a, b) => b.market_cap - a.market_cap);
+
+ sortedSubnets.forEach(subnet => {
+ const isStaked = subnet.your_stakes && subnet.your_stakes.length > 0;
+ const marketCapFormatted = formatTileNumbers(subnet.market_cap);
+
+ const tile = document.createElement('div');
+ tile.className = `subnet-tile ${isStaked ? 'staked' : ''}`;
+ tile.onclick = () => showSubnetPage(subnet.netuid);
+
+ // Calculate background intensity based on market cap relative to max
+ const maxMarketCap = sortedSubnets[0].market_cap;
+ const intensity = Math.max(5, Math.min(15, 5 + (subnet.market_cap / maxMarketCap) * 10));
+
+ tile.innerHTML = `
+ ${subnet.netuid}
+ ${subnet.symbol}
+ ${subnet.name}
+ ${marketCapFormatted} ${root_symbol_html}
+ `;
+
+ // Set background intensity
+ tile.style.background = `rgba(255, 255, 255, 0.0${intensity.toFixed(0)})`;
+
+ tilesContainer.appendChild(tile);
+ });
+}
\ No newline at end of file
diff --git a/bittensor_cli/src/bittensor/utils.py b/bittensor_cli/src/bittensor/utils.py
index ca95b1b45..42f4cc3be 100644
--- a/bittensor_cli/src/bittensor/utils.py
+++ b/bittensor_cli/src/bittensor/utils.py
@@ -15,7 +15,7 @@
from bittensor_wallet.utils import SS58_FORMAT
from bittensor_wallet.errors import KeyFileError, PasswordError
from bittensor_wallet import utils
-from jinja2 import Template
+from jinja2 import Template, Environment, PackageLoader, select_autoescape
from markupsafe import Markup
import numpy as np
from numpy.typing import NDArray
@@ -38,6 +38,11 @@
err_console = Console(stderr=True)
verbose_console = Console(quiet=True)
+jinja_env = Environment(
+ loader=PackageLoader("bittensor_cli", "src/bittensor/templates"),
+ autoescape=select_autoescape(),
+)
+
UnlockStatus = namedtuple("UnlockStatus", ["success", "message"])
@@ -835,11 +840,7 @@ def update_metadata_table(table_name: str, values: dict[str, str]) -> None:
"""
with DB() as (conn, cursor):
cursor.execute(
- "CREATE TABLE IF NOT EXISTS metadata ("
- "TableName TEXT, "
- "Key TEXT, "
- "Value TEXT"
- ")"
+ "CREATE TABLE IF NOT EXISTS metadata (TableName TEXT, Key TEXT, Value TEXT)"
)
conn.commit()
for key, value in values.items():
@@ -1295,37 +1296,6 @@ def get_subnet_name(subnet_info, max_length: int = 20) -> str:
return name
-def print_linux_dependency_message():
- """Prints the WebKit dependency message for Linux systems."""
- console.print("[red]This command requires WebKit dependencies on Linux.[/red]")
- console.print(
- "\nPlease make sure these packages are installed on your system for PyWry to work:"
- )
- console.print("\nArch Linux / Manjaro:")
- console.print("[green]sudo pacman -S webkit2gtk[/green]")
- console.print("\nDebian / Ubuntu:")
- console.print("[green]sudo apt install libwebkit2gtk-4.0-dev[/green]")
- console.print("\nNote for Ubuntu 24.04+ & Debian 13+:")
- console.print("You may need these additional steps to install libwebkit2gtk:")
- console.print(
- "\tCreate a new source file with: [green]sudo vim /etc/apt/sources.list.d/jammy-temp.list[/green]"
- )
- console.print(
- "\tAdd this into the file and save: [green]deb http://archive.ubuntu.com/ubuntu jammy main universe[/green]"
- )
- console.print(
- "\tUpdate the repository and install the webkit dependency: [green]sudo apt update && sudo apt install "
- "libwebkit2gtk-4.0-dev[/green]"
- )
- console.print("\nFedora / CentOS / AlmaLinux:")
- console.print("[green]sudo dnf install gtk3-devel webkit2gtk3-devel[/green]\n\n")
-
-
-def is_linux():
- """Returns True if the operating system is Linux."""
- return platform.system().lower() == "linux"
-
-
def validate_rate_tolerance(value: Optional[float]) -> Optional[float]:
"""Validates rate tolerance input"""
if value is not None:
@@ -1337,7 +1307,7 @@ def validate_rate_tolerance(value: Optional[float]) -> Optional[float]:
raise typer.BadParameter("Rate tolerance cannot be greater than 1 (100%).")
if value > 0.5:
console.print(
- f"[yellow]Warning: High rate tolerance of {value*100}% specified. "
+ f"[yellow]Warning: High rate tolerance of {value * 100}% specified. "
"This may result in unfavorable transaction execution.[/yellow]"
)
return value
diff --git a/bittensor_cli/src/commands/stake/add.py b/bittensor_cli/src/commands/stake/add.py
index a4af67fff..38fa6632b 100644
--- a/bittensor_cli/src/commands/stake/add.py
+++ b/bittensor_cli/src/commands/stake/add.py
@@ -586,7 +586,7 @@ def _define_stake_table(
if safe_staking:
table.add_column(
- f"Rate with tolerance: [blue]({rate_tolerance*100}%)[/blue]",
+ f"Rate with tolerance: [blue]({rate_tolerance * 100}%)[/blue]",
justify="center",
style=COLOR_PALETTE["POOLS"]["RATE"],
)
diff --git a/bittensor_cli/src/commands/stake/remove.py b/bittensor_cli/src/commands/stake/remove.py
index f80201e22..c7a72ffed 100644
--- a/bittensor_cli/src/commands/stake/remove.py
+++ b/bittensor_cli/src/commands/stake/remove.py
@@ -1266,7 +1266,7 @@ def _create_unstake_table(
)
if safe_staking:
table.add_column(
- f"Rate with tolerance: [blue]({rate_tolerance*100}%)[/blue]",
+ f"Rate with tolerance: [blue]({rate_tolerance * 100}%)[/blue]",
justify="center",
style=COLOR_PALETTE["POOLS"]["RATE"],
)
diff --git a/bittensor_cli/src/commands/subnets/price.py b/bittensor_cli/src/commands/subnets/price.py
index fa05b9462..461ed4325 100644
--- a/bittensor_cli/src/commands/subnets/price.py
+++ b/bittensor_cli/src/commands/subnets/price.py
@@ -1,7 +1,9 @@
import asyncio
import json
import math
-from pywry import PyWry
+import tempfile
+import webbrowser
+
from typing import TYPE_CHECKING
import plotille
@@ -14,6 +16,7 @@
get_subnet_name,
print_error,
json_console,
+ jinja_env,
)
if TYPE_CHECKING:
@@ -180,11 +183,7 @@ def _process_subnet_data(block_numbers, all_subnet_infos, netuids, all_netuids):
def _generate_html_single_subnet(
- netuid,
- data,
- block_numbers,
- interval_hours,
- log_scale,
+ netuid, data, block_numbers, interval_hours, log_scale, title: str
):
"""
Generate an HTML chart for a single subnet.
@@ -242,119 +241,22 @@ def _generate_html_single_subnet(
type="log" if log_scale else "linear",
)
- # Price change color
- price_change_class = "text-green" if stats["change_pct"] > 0 else "text-red"
- # Change sign
- sign_icon = "▲" if stats["change_pct"] > 0 else "▼"
-
- fig_dict = fig.to_dict()
- fig_json = json.dumps(fig_dict)
- html_content = f"""
-
-
-
-
- Subnet Price View
-
-
-
-
-
-
-
-
-
- """
+ fig_json = fig.to_json()
+ template = jinja_env.get_template("price-single.j2")
+ html_content = template.render(
+ fig_json=fig_json,
+ stats=stats,
+ change_pct=abs(stats["change_pct"]),
+ interval_hours=interval_hours,
+ title=title,
+ )
return html_content
-def _generate_html_multi_subnet(subnet_data, block_numbers, interval_hours, log_scale):
+def _generate_html_multi_subnet(
+ subnet_data, block_numbers, interval_hours, log_scale, title: str
+):
"""
Generate an HTML chart for multiple subnets.
"""
@@ -573,143 +475,17 @@ def build_single_subnet_annotations(netuid):
}
fig_json = fig.to_json()
- all_visibility_json = json.dumps(all_visibility)
- all_annotations_json = json.dumps(all_annotations)
-
- subnet_modes_json = {}
- for netuid, mode_data in subnet_modes.items():
- subnet_modes_json[netuid] = {
- "visible": json.dumps(mode_data["visible"]),
- "annotations": json.dumps(mode_data["annotations"]),
- }
- # We sort netuids by market cap but for buttons, they are ordered by netuid
- sorted_subnet_keys = sorted(subnet_data.keys())
- all_button_html = (
- 'All '
+ template = jinja_env.get_template("price-multi.j2")
+ html_content = template.render(
+ title=title,
+ # We sort netuids by market cap but for buttons, they are ordered by netuid
+ sorted_subnet_keys=sorted(subnet_data.keys()),
+ fig_json=fig_json,
+ all_visibility=all_visibility,
+ all_annotations=all_annotations,
+ subnet_modes=subnet_modes,
)
- subnet_buttons_html = ""
- for netuid in sorted_subnet_keys:
- subnet_buttons_html += f'S{netuid} '
-
- html_content = f"""
-
-
-
- Multi-Subnet Price Chart
-
-
-
-
-
-
-
- {all_button_html}
- {subnet_buttons_html}
-
-
-
-
-
- """
return html_content
@@ -720,7 +496,7 @@ async def _generate_html_output(
log_scale: bool = False,
):
"""
- Start PyWry and display the price chart in a window.
+ Display HTML output in browser
"""
try:
subnet_keys = list(subnet_data.keys())
@@ -730,42 +506,34 @@ async def _generate_html_output(
netuid = subnet_keys[0]
data = subnet_data[netuid]
html_content = _generate_html_single_subnet(
- netuid, data, block_numbers, interval_hours, log_scale
+ netuid,
+ data,
+ block_numbers,
+ interval_hours,
+ log_scale,
+ title=f"Subnet {netuid} Price View",
)
- title = f"Subnet {netuid} Price View"
+
else:
# Multi-subnet
html_content = _generate_html_multi_subnet(
- subnet_data, block_numbers, interval_hours, log_scale
+ subnet_data,
+ block_numbers,
+ interval_hours,
+ log_scale,
+ title="Subnets Price Chart",
)
- title = "Subnets Price Chart"
+
console.print(
- "[dark_sea_green3]Opening price chart in a window. Press Ctrl+C to close.[/dark_sea_green3]"
- )
- handler = PyWry()
- handler.send_html(
- html=html_content,
- title=title,
- width=1200,
- height=800,
+ "[dark_sea_green3]Opening price chart in a window.[/dark_sea_green3]"
)
- handler.start()
- await asyncio.sleep(5)
-
- # TODO: Improve this logic
- try:
- while True:
- if _has_exited(handler):
- break
- await asyncio.sleep(1)
- except KeyboardInterrupt:
- pass
- finally:
- if not _has_exited(handler):
- try:
- handler.close()
- except Exception:
- pass
+ with tempfile.NamedTemporaryFile(
+ "w", delete=False, suffix=".html"
+ ) as dashboard_file:
+ url = f"file://{dashboard_file.name}"
+ dashboard_file.write(html_content)
+
+ webbrowser.open(url, new=1)
except Exception as e:
print_error(f"Error generating price chart: {e}")
@@ -858,12 +626,3 @@ def color_label(text):
)
console.print(stats_text)
-
-
-def _has_exited(handler) -> bool:
- """Check if PyWry process has cleanly exited with returncode 0."""
- return (
- hasattr(handler, "runner")
- and handler.runner is not None
- and handler.runner.returncode == 0
- )
diff --git a/bittensor_cli/src/commands/sudo.py b/bittensor_cli/src/commands/sudo.py
index 87a54d520..4c8560f83 100644
--- a/bittensor_cli/src/commands/sudo.py
+++ b/bittensor_cli/src/commands/sudo.py
@@ -525,7 +525,8 @@ async def set_take_extrinsic(
if current_take_u16 < take_u16:
console.print(
- f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%[/{COLOR_PALETTE['POOLS']['RATE']}]. Increasing to [{COLOR_PALETTE['POOLS']['RATE']}]{take * 100:.2f}%."
+ f"Current take is [{COLOR_PALETTE.P.RATE}]{current_take * 100.0:.2f}%[/{COLOR_PALETTE.P.RATE}]. "
+ f"Increasing to [{COLOR_PALETTE.P.RATE}]{take * 100:.2f}%."
)
with console.status(
f":satellite: Sending decrease_take_extrinsic call on [white]{subtensor}[/white] ..."
@@ -542,7 +543,8 @@ async def set_take_extrinsic(
else:
console.print(
- f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%[/{COLOR_PALETTE['POOLS']['RATE']}]. Decreasing to [{COLOR_PALETTE['POOLS']['RATE']}]{take * 100:.2f}%."
+ f"Current take is [{COLOR_PALETTE.P.RATE}]{current_take * 100.0:.2f}%[/{COLOR_PALETTE.P.RATE}]. "
+ f"Decreasing to [{COLOR_PALETTE.P.RATE}]{take * 100:.2f}%."
)
with console.status(
f":satellite: Sending increase_take_extrinsic call on [white]{subtensor}[/white] ..."
@@ -871,7 +873,7 @@ async def get_current_take(subtensor: "SubtensorInterface", wallet: Wallet):
async def display_current_take(subtensor: "SubtensorInterface", wallet: Wallet) -> None:
current_take = await get_current_take(subtensor, wallet)
console.print(
- f"Current take is [{COLOR_PALETTE['POOLS']['RATE']}]{current_take * 100.:.2f}%"
+ f"Current take is [{COLOR_PALETTE.P.RATE}]{current_take * 100.0:.2f}%"
)
@@ -891,7 +893,9 @@ async def _do_set_take() -> bool:
)
if not len(netuids_registered) > 0:
err_console.print(
- f"Hotkey [{COLOR_PALETTE['GENERAL']['HOTKEY']}]{wallet.hotkey.ss58_address}[/{COLOR_PALETTE['GENERAL']['HOTKEY']}] is not registered to any subnet. Please register using [{COLOR_PALETTE['GENERAL']['SUBHEADING']}]`btcli subnets register`[{COLOR_PALETTE['GENERAL']['SUBHEADING']}] and try again."
+ f"Hotkey [{COLOR_PALETTE.G.HK}]{wallet.hotkey.ss58_address}[/{COLOR_PALETTE.G.HK}] is not registered to"
+ f" any subnet. Please register using [{COLOR_PALETTE.G.SUBHEAD}]`btcli subnets register`"
+ f"[{COLOR_PALETTE.G.SUBHEAD}] and try again."
)
return False
@@ -908,12 +912,12 @@ async def _do_set_take() -> bool:
else:
new_take = await get_current_take(subtensor, wallet)
console.print(
- f"New take is [{COLOR_PALETTE['POOLS']['RATE']}]{new_take * 100.:.2f}%"
+ f"New take is [{COLOR_PALETTE.P.RATE}]{new_take * 100.0:.2f}%"
)
return True
console.print(
- f"Setting take on [{COLOR_PALETTE['GENERAL']['LINKS']}]network: {subtensor.network}"
+ f"Setting take on [{COLOR_PALETTE.G.LINKS}]network: {subtensor.network}"
)
if not unlock_key(wallet, "hot").success and unlock_key(wallet, "cold").success:
diff --git a/bittensor_cli/src/commands/view.py b/bittensor_cli/src/commands/view.py
index 3262b262c..26bb3eb95 100644
--- a/bittensor_cli/src/commands/view.py
+++ b/bittensor_cli/src/commands/view.py
@@ -1,33 +1,18 @@
import asyncio
-import json
import os
import tempfile
import webbrowser
import netaddr
-from dataclasses import asdict, is_dataclass
from typing import Any
-from pywry import PyWry
from bittensor_cli.src.bittensor.balances import Balance
from bittensor_cli.src.bittensor.subtensor_interface import SubtensorInterface
-from bittensor_cli.src.bittensor.utils import console, WalletLike
+from bittensor_cli.src.bittensor.utils import console, WalletLike, jinja_env
from bittensor_wallet import Wallet
from bittensor_cli.src import defaults
-root_symbol_html = f"{ord('τ'):X};"
-
-class Encoder(json.JSONEncoder):
- """JSON encoder for serializing dataclasses and balances"""
-
- def default(self, obj):
- if is_dataclass(obj):
- return asdict(obj)
-
- elif isinstance(obj, Balance):
- return obj.tao
-
- return super().default(obj)
+ROOT_SYMBOL_HTML = f"{ord('τ'):X};"
async def display_network_dashboard(
@@ -51,30 +36,15 @@ async def display_network_dashboard(
if use_wry:
console.print(
- "[dark_sea_green3]Opening dashboard in a window. Press Ctrl+C to close.[/dark_sea_green3]"
+ "[dark_sea_green3]Opening dashboard in a window.[/dark_sea_green3]"
)
- window = PyWry()
- window.send_html(
- html=html_content,
- title="Bittensor View",
- width=1200,
- height=800,
- )
- window.start()
- await asyncio.sleep(10)
- try:
- while True:
- if _has_exited(window):
- break
- await asyncio.sleep(1)
- except KeyboardInterrupt:
- console.print("\n[yellow]Closing Bittensor View...[/yellow]")
- finally:
- if not _has_exited(window):
- try:
- window.close()
- except Exception:
- pass
+ with tempfile.NamedTemporaryFile(
+ "w", delete=False, suffix=".html"
+ ) as dashboard_file:
+ url = f"file://{dashboard_file.name}"
+ dashboard_file.write(html_content)
+
+ webbrowser.open(url, new=1)
else:
if save_file:
dir_path = os.path.expanduser(dashboard_path)
@@ -122,7 +92,7 @@ def get_identity(
hotkey_ss58: str,
identities: dict,
old_identities: dict,
- trucate_length: int = 4,
+ truncate_length: int = 4,
return_bool: bool = False,
lookup_hk: bool = True,
) -> str:
@@ -144,7 +114,7 @@ def get_identity(
if return_bool:
return False
else:
- return f"{hotkey_ss58[:trucate_length]}...{hotkey_ss58[-trucate_length:]}"
+ return f"{hotkey_ss58[:truncate_length]}...{hotkey_ss58[-truncate_length:]}"
async def fetch_subnet_data(
@@ -283,7 +253,7 @@ def process_subnet_data(raw_data: dict[str, Any]) -> dict[str, Any]:
# Add identities
for hotkey in meta_info.hotkeys:
identity = get_identity(
- hotkey, ck_hk_identities, old_identities, trucate_length=2
+ hotkey, ck_hk_identities, old_identities, truncate_length=2
)
metagraph_info["updated_identities"].append(identity)
@@ -358,324 +328,13 @@ def process_subnet_data(raw_data: dict[str, Any]) -> dict[str, Any]:
}
-def _has_exited(handler) -> bool:
- """Check if PyWry process has cleanly exited with returncode 0."""
- return (
- hasattr(handler, "runner")
- and handler.runner is not None
- and handler.runner.returncode == 0
- )
-
-
def generate_full_page(data: dict[str, Any]) -> str:
"""
Generate full HTML content for the interface.
"""
- serializable_data = {
- "wallet_info": data["wallet_info"],
- "subnets": data["subnets"],
- }
- wallet_info_json = json.dumps(
- serializable_data["wallet_info"], cls=Encoder
- ).replace("'", "'")
- subnets_json = json.dumps(serializable_data["subnets"], cls=Encoder).replace(
- "'", "'"
- )
-
- return f"""
-
-
-
-
- Bittensor CLI Interface
-
-
-
-
-
-
-
-
-
-
Btcli View
- Beta
-
-
-
-
-
-
- {generate_main_header(data["wallet_info"], data["block_number"])}
- {generate_main_filters()}
- {generate_subnets_table(data["subnets"])}
-
-
-
-
- {generate_subnet_details_header()}
- {generate_subnet_metrics()}
- {generate_neuron_details()}
-
-
-
-
-
- """
-
-
-def generate_subnet_details_header() -> str:
- """
- Generates the header section for the subnet details page,
- including the back button, toggle controls, title, and network visualization.
- """
- return """
-
- """
-
-
-def generate_subnet_metrics() -> str:
- """
- Generates the metrics section for the subnet details page,
- including market metrics and the stakes table.
- """
- return """
-
-
-
-
-
-
-
-
-
-
- Hotkey
- Amount
- Value
- Value (w/ slippage)
- Alpha emission
- Tao emission
- Registered
- Actions
-
-
-
-
-
-
-
-
- """
-
-
-def generate_neuron_details() -> str:
- """
- Generates the neuron detail container, which is hidden by default.
- This section shows detailed information for a selected neuron.
- """
- return """
-
-
-
-
-
-
-
- Hotkey:
-
-
-
- Coldkey:
-
-
-
-
-
-
-
- """
-
-
-def generate_main_header(wallet_info: dict[str, Any], block_number: int) -> str:
+ wallet_info = data["wallet_info"]
truncated_coldkey = f"{wallet_info['coldkey'][:6]}...{wallet_info['coldkey'][-6:]}"
-
+ block_number = data["block_number"]
# Calculate slippage percentage
ideal_value = wallet_info["total_ideal_stake_value"]
slippage_value = wallet_info["total_slippage_value"]
@@ -683,2263 +342,13 @@ def generate_main_header(wallet_info: dict[str, Any], block_number: int) -> str:
((ideal_value - slippage_value) / ideal_value * 100) if ideal_value > 0 else 0
)
- return f"""
-
- """
-
-
-def generate_main_filters() -> str:
- return """
-
-
- """
-
-
-def generate_subnets_table(subnets: list[dict[str, Any]]) -> str:
- rows = []
- for subnet in subnets:
- total_your_stake = sum(stake["amount"] for stake in subnet["your_stakes"])
- stake_status = (
- 'Staked '
- if total_your_stake > 0
- else 'Not Staked '
- )
- rows.append(f"""
-
- {subnet["netuid"]} - {subnet["name"]}
-
-
-
-
- {stake_status}
-
- """)
- return f"""
-
-
-
-
- Subnet
- Price
- Market Cap
- Your Stake
- Emission
- Status
-
-
-
- {"".join(rows)}
-
-
-
- """
-
-
-def generate_subnet_details_html() -> str:
- return """
-
-
-
-
-
- Price
-
-
-
- Market Cap
-
-
-
- Emission Rate
-
-
-
- Your Total Stake
-
-
-
-
-
-
- """
-
-
-def get_css_styles() -> str:
- """Get CSS styles for the interface."""
- return """
- /* ===================== Base Styles & Typography ===================== */
- @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Noto+Sans:wght@400;500;600&display=swap');
-
- body {
- font-family: 'Inter', 'Noto Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Arial Unicode MS', sans-serif;
- margin: 0;
- padding: 24px;
- background: #000000;
- color: #ffffff;
- }
-
- input, button, select {
- font-family: inherit;
- font-feature-settings: normal;
- }
-
- /* ===================== Main Page Header ===================== */
- .header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 16px 24px;
- background: rgba(255, 255, 255, 0.05);
- border-radius: 12px;
- margin-bottom: 24px;
- backdrop-filter: blur(10px);
- }
-
- .wallet-info {
- display: flex;
- flex-direction: column;
- gap: 4px;
- }
-
- .wallet-name {
- font-size: 1.1em;
- font-weight: 500;
- color: #FF9900;
- }
-
- .wallet-address-container {
- position: relative;
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- gap: 8px;
- }
-
- .wallet-address {
- font-size: 0.9em;
- color: rgba(255, 255, 255, 0.5);
- font-family: monospace;
- transition: color 0.2s ease;
- }
-
- .wallet-address-container:hover .wallet-address {
- color: rgba(255, 255, 255, 0.8);
- }
-
- .copy-indicator {
- background: rgba(255, 153, 0, 0.1);
- color: rgba(255, 153, 0, 0.8);
- padding: 2px 6px;
- border-radius: 4px;
- font-size: 0.7em;
- transition: all 0.2s ease;
- opacity: 0;
- }
-
- .wallet-address-container:hover .copy-indicator {
- opacity: 1;
- background: rgba(255, 153, 0, 0.2);
- }
-
- .wallet-address-container.copied .copy-indicator {
- opacity: 1;
- background: rgba(255, 153, 0, 0.3);
- color: #FF9900;
- }
-
- .stake-metrics {
- display: flex;
- gap: 24px;
- align-items: center;
- }
-
- .stake-metric {
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- gap: 2px;
- position: relative;
- padding: 8px 16px;
- border-radius: 8px;
- transition: all 0.2s ease;
- }
-
- .stake-metric:hover {
- background: rgba(255, 153, 0, 0.05);
- }
-
- .stake-metric .metric-label {
- font-size: 0.8em;
- color: rgba(255, 255, 255, 0.6);
- text-transform: uppercase;
- letter-spacing: 0.5px;
- }
-
- .stake-metric .metric-value {
- font-size: 1.1em;
- font-weight: 500;
- color: #FF9900;
- font-feature-settings: "tnum";
- font-variant-numeric: tabular-nums;
- }
-
- .slippage-value {
- display: flex;
- align-items: center;
- gap: 6px;
- }
-
- .slippage-detail {
- font-size: 0.8em;
- color: rgba(255, 255, 255, 0.5);
- }
-
- /* ===================== Main Page Filters ===================== */
- .filters-section {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin: 24px 0;
- gap: 16px;
- }
-
- .search-box input {
- padding: 10px 16px;
- border-radius: 8px;
- border: 1px solid rgba(255, 255, 255, 0.1);
- background: rgba(255, 255, 255, 0.03);
- color: rgba(255, 255, 255, 0.7);
- width: 240px;
- font-size: 0.9em;
- transition: all 0.2s ease;
- }
- .search-box input::placeholder {
- color: rgba(255, 255, 255, 0.4);
- }
-
- .search-box input:focus {
- outline: none;
- border-color: rgba(255, 153, 0, 0.5);
- background: rgba(255, 255, 255, 0.06);
- color: rgba(255, 255, 255, 0.9);
- }
-
- .filter-toggles {
- display: flex;
- gap: 16px;
- }
-
- .filter-toggles label {
- display: flex;
- align-items: center;
- gap: 8px;
- color: rgba(255, 255, 255, 0.7);
- font-size: 0.9em;
- cursor: pointer;
- user-select: none;
- }
-
- /* Checkbox styling for both main page and subnet page */
- .filter-toggles input[type="checkbox"],
- .toggle-label input[type="checkbox"] {
- -webkit-appearance: none;
- -moz-appearance: none;
- appearance: none;
- width: 18px;
- height: 18px;
- border: 2px solid rgba(255, 153, 0, 0.3);
- border-radius: 4px;
- background: rgba(0, 0, 0, 0.2);
- cursor: pointer;
- position: relative;
- transition: all 0.2s ease;
- }
-
- .filter-toggles input[type="checkbox"]:hover,
- .toggle-label input[type="checkbox"]:hover {
- border-color: #FF9900;
- }
-
- .filter-toggles input[type="checkbox"]:checked,
- .toggle-label input[type="checkbox"]:checked {
- background: #FF9900;
- border-color: #FF9900;
- }
-
- .filter-toggles input[type="checkbox"]:checked::after,
- .toggle-label input[type="checkbox"]:checked::after {
- content: '';
- position: absolute;
- left: 5px;
- top: 2px;
- width: 4px;
- height: 8px;
- border: solid #000;
- border-width: 0 2px 2px 0;
- transform: rotate(45deg);
- }
-
- .filter-toggles label:hover,
- .toggle-label:hover {
- color: rgba(255, 255, 255, 0.9);
- }
- .disabled-label {
- opacity: 0.5;
- cursor: not-allowed;
- }
- .add-stake-button {
- padding: 10px 20px;
- font-size: 0.8rem;
- }
- .export-csv-button {
- padding: 10px 20px;
- font-size: 0.8rem;
- }
- .button-group {
- display: flex;
- gap: 8px;
- }
-
- /* ===================== Main Page Subnet Table ===================== */
- .subnets-table-container {
- background: rgba(255, 255, 255, 0.02);
- border-radius: 12px;
- overflow: hidden;
- }
-
- .subnets-table {
- width: 100%;
- border-collapse: collapse;
- font-size: 0.95em;
- }
-
- .subnets-table th {
- background: rgba(255, 255, 255, 0.05);
- font-weight: 500;
- text-align: left;
- padding: 16px;
- color: rgba(255, 255, 255, 0.7);
- }
-
- .subnets-table td {
- padding: 14px 16px;
- border-top: 1px solid rgba(255, 255, 255, 0.05);
- }
-
- .subnet-row {
- cursor: pointer;
- transition: background-color 0.2s ease;
- }
-
- .subnet-row:hover {
- background: rgba(255, 255, 255, 0.05);
- }
-
- .subnet-name {
- color: #ffffff;
- font-weight: 500;
- font-size: 0.95em;
- }
-
- .price, .market-cap, .your-stake, .emission {
- font-family: 'Inter', monospace;
- font-size: 1.0em;
- font-feature-settings: "tnum";
- font-variant-numeric: tabular-nums;
- letter-spacing: 0.01em;
- white-space: nowrap;
- }
-
- .stake-status {
- font-size: 0.85em;
- padding: 4px 8px;
- border-radius: 4px;
- background: rgba(255, 255, 255, 0.05);
- }
-
- .stake-status.staked {
- background: rgba(255, 153, 0, 0.1);
- color: #FF9900;
- }
-
- .subnets-table th.sortable {
- cursor: pointer;
- position: relative;
- padding-right: 20px;
- }
-
- .subnets-table th.sortable:hover {
- color: #FF9900;
- }
-
- .subnets-table th[data-sort] {
- color: #FF9900;
- }
-
- /* ===================== Subnet Tiles View ===================== */
- .subnet-tiles-container {
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- gap: 1rem;
- padding: 1rem;
- }
-
- .subnet-tile {
- width: clamp(75px, 6vw, 600px);
- height: clamp(75px, 6vw, 600px);
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- background: rgba(255, 255, 255, 0.05);
- border-radius: 8px;
- position: relative;
- cursor: pointer;
- transition: all 0.2s ease;
- overflow: hidden;
- font-size: clamp(0.6rem, 1vw, 1.4rem);
- }
-
- .tile-netuid {
- position: absolute;
- top: 0.4em;
- left: 0.4em;
- font-size: 0.7em;
- color: rgba(255, 255, 255, 0.6);
- }
-
- .tile-symbol {
- font-size: 1.6em;
- margin-bottom: 0.4em;
- color: #FF9900;
- }
-
- .tile-name {
- display: block;
- width: 100%;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- font-size: 1em;
- text-align: center;
- color: rgba(255, 255, 255, 0.9);
- margin: 0 0.4em;
- }
-
- .tile-market-cap {
- font-size: 0.9em;
- color: rgba(255, 255, 255, 0.5);
- margin-top: 2px;
- }
-
- .subnet-tile:hover {
- transform: translateY(-2px);
- box-shadow:
- 0 0 12px rgba(255, 153, 0, 0.6),
- 0 0 24px rgba(255, 153, 0, 0.3);
- background: rgba(255, 255, 255, 0.08);
- }
-
- .subnet-tile.staked {
- border: 1px solid rgba(255, 153, 0, 0.3);
- }
-
- .subnet-tile.staked::before {
- content: '';
- position: absolute;
- top: 0.4em;
- right: 0.4em;
- width: 0.5em;
- height: 0.5em;
- border-radius: 50%;
- background: #FF9900;
- }
-
- /* ===================== Subnet Detail Page Header ===================== */
- .subnet-header {
- padding: 16px;
- border-radius: 12px;
- margin-bottom: 0px;
- }
-
- .subnet-header h2 {
- margin: 0;
- font-size: 1.3em;
- }
-
- .subnet-price {
- font-size: 1.3em;
- color: #FF9900;
- }
-
- .subnet-title-row {
- display: grid;
- grid-template-columns: 300px 1fr 300px;
- align-items: start;
- margin: 0;
- position: relative;
- min-height: 60px;
- }
-
- .title-price {
- grid-column: 1;
- padding-top: 0;
- margin-top: -10px;
- }
-
- .header-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- width: 100%;
- margin-bottom: 16px;
- }
-
- .toggle-group {
- display: flex;
- flex-direction: column;
- gap: 8px;
- align-items: flex-end;
- }
-
- .toggle-label {
- display: flex;
- align-items: center;
- gap: 8px;
- color: rgba(255, 255, 255, 0.7);
- font-size: 0.9em;
- cursor: pointer;
- user-select: none;
- }
-
- .back-button {
- background: rgba(255, 255, 255, 0.05);
- border: 1px solid rgba(255, 255, 255, 0.1);
- color: rgba(255, 255, 255, 0.8);
- padding: 8px 16px;
- border-radius: 8px;
- cursor: pointer;
- font-size: 0.9em;
- transition: all 0.2s ease;
- margin-bottom: 16px;
- }
-
- .back-button:hover {
- background: rgba(255, 255, 255, 0.1);
- border-color: rgba(255, 255, 255, 0.2);
- }
-
- /* ===================== Network Visualization ===================== */
- .network-visualization-container {
- position: absolute;
- left: 50%;
- transform: translateX(-50%);
- top: -50px;
- width: 700px;
- height: 80px;
- z-index: 1;
- }
-
- .network-visualization {
- width: 700px;
- height: 80px;
- position: relative;
- }
-
- #network-canvas {
- background: transparent;
- position: relative;
- z-index: 1;
- }
-
- /* Gradient behind visualization */
- .network-visualization::after {
- content: '';
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 100%;
- background: linear-gradient(to bottom, rgba(0, 0, 0, 0.95) 0%, rgba(0, 0, 0, 0.8) 100%);
- z-index: 0;
- pointer-events: none;
- }
-
- /* ===================== Subnet Detail Metrics ===================== */
- .network-metrics {
- display: grid;
- grid-template-columns: repeat(5, 1fr);
- gap: 12px;
- margin: 0;
- margin-top: 16px;
- }
-
- /* Base card styles - applied to both network and metric cards */
- .network-card, .metric-card {
- background: rgba(255, 255, 255, 0.05);
- border-radius: 8px;
- padding: 12px 16px;
- min-height: 50px;
- display: flex;
- flex-direction: column;
- justify-content: center;
- gap: 4px;
- }
-
- /* Separate styling for moving price value */
- #network-moving-price {
- color: #FF9900;
- }
-
- .metrics-section {
- margin-top: 0px;
- margin-bottom: 16px;
- }
-
- .metrics-group {
- display: grid;
- grid-template-columns: repeat(5, 1fr);
- gap: 12px;
- margin: 0;
- margin-top: 2px;
- }
-
- .market-metrics .metric-card {
- background: rgba(255, 255, 255, 0.05);
- min-height: 70px;
- }
-
- .metric-label {
- font-size: 0.85em;
- color: rgba(255, 255, 255, 0.7);
- margin: 0;
- }
-
- .metric-value {
- font-size: 1.2em;
- line-height: 1.3;
- margin: 0;
- }
-
- /* Add status colors */
- .registration-status {
- color: #2ECC71;
- }
-
- .registration-status.closed {
- color: #ff4444; /* Red color for closed status */
- }
-
- .cr-status {
- color: #2ECC71;
- }
-
- .cr-status.disabled {
- color: #ff4444; /* Red color for disabled status */
- }
-
- /* ===================== Stakes Table ===================== */
- .stakes-container {
- margin-top: 24px;
- padding: 0 24px;
- }
-
- .stakes-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 16px;
- }
-
- .stakes-header h3 {
- font-size: 1.2em;
- color: #ffffff;
- margin: 0;
- }
-
- .stakes-table-container {
- background: rgba(255, 255, 255, 0.02);
- border-radius: 12px;
- overflow: hidden;
- margin-bottom: 24px;
- width: 100%;
- }
-
- .stakes-table {
- width: 100%;
- border-collapse: collapse;
- }
-
- .stakes-table th {
- background: rgba(255, 255, 255, 0.05);
- padding: 16px;
- text-align: left;
- font-weight: 500;
- color: rgba(255, 255, 255, 0.7);
- }
-
- .stakes-table td {
- padding: 16px;
- border-top: 1px solid rgba(255, 255, 255, 0.05);
- }
-
- .stakes-table tr {
- transition: background-color 0.2s ease;
- }
-
- .stakes-table tr:nth-child(even) {
- background: rgba(255, 255, 255, 0.02);
- }
-
- .stakes-table tr:hover {
- background: transparent;
- }
-
- .no-stakes-row td {
- text-align: center;
- padding: 32px;
- color: rgba(255, 255, 255, 0.5);
- }
-
- /* Table styles consistency */
- .stakes-table th, .network-table th {
- background: rgba(255, 255, 255, 0.05);
- padding: 16px;
- text-align: left;
- font-weight: 500;
- color: rgba(255, 255, 255, 0.7);
- transition: color 0.2s ease;
- }
-
- /* Sortable columns */
- .stakes-table th.sortable, .network-table th.sortable {
- cursor: pointer;
- }
-
- /* Active sort column - only change color */
- .stakes-table th.sortable[data-sort], .network-table th.sortable[data-sort] {
- color: #FF9900;
- }
-
- /* Hover effects - only change color */
- .stakes-table th.sortable:hover, .network-table th.sortable:hover {
- color: #FF9900;
- }
-
- /* Remove hover background from table rows */
- .stakes-table tr:hover {
- background: transparent;
- }
-
- /* ===================== Network Table ===================== */
- .network-table-container {
- margin-top: 60px;
- position: relative;
- z-index: 2;
- background: rgba(0, 0, 0, 0.8);
- }
-
- .network-table {
- width: 100%;
- border-collapse: collapse;
- table-layout: fixed;
- }
-
- .network-table th {
- background: rgba(255, 255, 255, 0.05);
- padding: 16px;
- text-align: left;
- font-weight: 500;
- color: rgba(255, 255, 255, 0.7);
- }
-
- .network-table td {
- padding: 16px;
- border-top: 1px solid rgba(255, 255, 255, 0.05);
- }
-
- .network-table tr {
- cursor: pointer;
- transition: background-color 0.2s ease;
- }
-
- .network-table tr:hover {
- background-color: rgba(255, 255, 255, 0.05);
- }
-
- .network-table tr:nth-child(even) {
- background-color: rgba(255, 255, 255, 0.02);
- }
-
- .network-table tr:nth-child(even):hover {
- background-color: rgba(255, 255, 255, 0.05);
- }
-
- .network-search-container {
- display: flex;
- align-items: center;
- margin-bottom: 16px;
- padding: 0 16px;
- }
-
- .network-search {
- width: 100%;
- padding: 12px 16px;
- border: 1px solid rgba(255, 153, 0, 0.2);
- border-radius: 8px;
- background: rgba(0, 0, 0, 0.2);
- color: #ffffff;
- font-size: 0.95em;
- transition: all 0.2s ease;
- }
-
- .network-search:focus {
- outline: none;
- border-color: rgba(255, 153, 0, 0.5);
- background: rgba(0, 0, 0, 0.3);
- caret-color: #FF9900;
- }
-
- .network-search::placeholder {
- color: rgba(255, 255, 255, 0.3);
- }
-
- /* ===================== Cell Styles & Formatting ===================== */
- .hotkey-cell {
- max-width: 200px;
- position: relative;
- }
-
- .hotkey-container {
- position: relative;
- display: inline-block;
- max-width: 100%;
- }
-
- .hotkey-identity, .truncated-address {
- color: rgba(255, 255, 255, 0.8);
- display: inline-block;
- max-width: 100%;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .copy-button {
- position: absolute;
- top: -20px; /* Position above the text */
- right: 0;
- background: rgba(255, 153, 0, 0.1);
- color: rgba(255, 255, 255, 0.6);
- padding: 2px 6px;
- border-radius: 4px;
- font-size: 0.7em;
- cursor: pointer;
- opacity: 0;
- transition: all 0.2s ease;
- transform: translateY(5px);
- }
-
- .hotkey-container:hover .copy-button {
- opacity: 1;
- transform: translateY(0);
- }
-
- .copy-button:hover {
- background: rgba(255, 153, 0, 0.2);
- color: #FF9900;
- }
-
- .address-cell {
- max-width: 150px;
- position: relative;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .address-container {
- display: flex;
- align-items: center;
- cursor: pointer;
- position: relative;
- }
-
- .address-container:hover::after {
- content: 'Click to copy';
- position: absolute;
- right: 0;
- top: 50%;
- transform: translateY(-50%);
- background: rgba(255, 153, 0, 0.1);
- color: #FF9900;
- padding: 2px 6px;
- border-radius: 4px;
- font-size: 0.8em;
- opacity: 0.8;
- }
-
- .truncated-address {
- font-family: monospace;
- color: rgba(255, 255, 255, 0.8);
- overflow: hidden;
- text-overflow: ellipsis;
- }
-
- .truncated-address:hover {
- color: #FF9900;
- }
-
- .registered-yes {
- color: #FF9900;
- font-weight: 500;
- display: flex;
- align-items: center;
- gap: 4px;
- }
-
- .registered-no {
- color: #ff4444;
- font-weight: 500;
- display: flex;
- align-items: center;
- gap: 4px;
- }
-
- .manage-button {
- background: rgba(255, 153, 0, 0.1);
- border: 1px solid rgba(255, 153, 0, 0.2);
- color: #FF9900;
- padding: 6px 12px;
- border-radius: 6px;
- cursor: pointer;
- transition: all 0.2s ease;
- }
-
- .manage-button:hover {
- background: rgba(255, 153, 0, 0.2);
- transform: translateY(-1px);
- }
-
- .hotkey-identity {
- display: inline-block;
- max-width: 100%;
- overflow: hidden;
- text-overflow: ellipsis;
- color: #FF9900;
- }
-
- .identity-cell {
- max-width: 700px;
- font-size: 0.90em;
- letter-spacing: -0.2px;
- color: #FF9900;
- }
-
- .per-day {
- font-size: 0.75em;
- opacity: 0.7;
- margin-left: 4px;
- }
-
- /* ===================== Neuron Detail Panel ===================== */
- #neuron-detail-container {
- background: rgba(255, 255, 255, 0.02);
- border-radius: 12px;
- padding: 16px;
- margin-top: 16px;
- }
-
- .neuron-detail-header {
- display: flex;
- align-items: center;
- gap: 16px;
- margin-bottom: 16px;
- }
-
- .neuron-detail-content {
- display: flex;
- flex-direction: column;
- gap: 16px;
- }
-
- .neuron-info-top {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
-
- .neuron-keys {
- display: flex;
- flex-direction: column;
- gap: 4px;
- font-size: 0.9em;
- color: rgba(255, 255, 255, 0.6);
- font-size: 1em;
- color: rgba(255, 255, 255, 0.7);
- }
-
- .neuron-cards-container {
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
-
- .neuron-metrics-row {
- display: grid;
- grid-template-columns: repeat(6, 1fr);
- gap: 12px;
- margin: 0;
- }
-
- .neuron-metrics-row.last-row {
- grid-template-columns: repeat(3, 1fr);
- }
-
- /* IP Info styling */
- #neuron-ipinfo {
- font-size: 0.85em;
- line-height: 1.4;
- white-space: nowrap;
- }
-
- #neuron-ipinfo .no-connection {
- color: #ff4444;
- font-weight: 500;
- }
-
- /* Adjust metric card for IP info to accommodate multiple lines */
- .neuron-cards-container .metric-card:has(#neuron-ipinfo) {
- min-height: 85px;
- }
-
- /* ===================== Subnet Page Color Overrides ===================== */
- /* Subnet page specific style */
- .subnet-page .metric-card-title,
- .subnet-page .network-card-title {
- color: rgba(255, 255, 255, 0.7);
- }
-
- .subnet-page .metric-card .metric-value,
- .subnet-page .metric-value {
- color: white;
- }
-
- /* Green values */
- .subnet-page .validator-true,
- .subnet-page .active-yes,
- .subnet-page .registration-open,
- .subnet-page .cr-enabled,
- .subnet-page .ip-info {
- color: #FF9900;
- }
-
- /* Red values */
- .subnet-page .validator-false,
- .subnet-page .active-no,
- .subnet-page .registration-closed,
- .subnet-page .cr-disabled,
- .subnet-page .ip-na {
- color: #ff4444;
- }
-
- /* Keep symbols green in subnet page */
- .subnet-page .symbol {
- color: #FF9900;
- }
-
- /* ===================== Responsive Styles ===================== */
- @media (max-width: 1200px) {
- .stakes-table {
- display: block;
- overflow-x: auto;
- }
-
- .network-metrics {
- grid-template-columns: repeat(3, 1fr);
- }
- }
-
- @media (min-width: 1201px) {
- .network-metrics {
- grid-template-columns: repeat(5, 1fr);
- }
- }
- /* ===== Splash Screen ===== */
- #splash-screen {
- position: fixed;
- top: 0;
- left: 0;
- width: 100vw;
- height: 100vh;
- background: #000000;
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 999999;
- opacity: 1;
- transition: opacity 1s ease;
- }
-
- #splash-screen.fade-out {
- opacity: 0;
- }
-
- .splash-content {
- text-align: center;
- color: #FF9900;
- opacity: 0;
- animation: fadeIn 1.2s ease forwards;
- }
- @keyframes fadeIn {
- 0% {
- opacity: 0;
- transform: scale(0.97);
- }
- 100% {
- opacity: 1;
- transform: scale(1);
- }
- }
-
- /* Title & text styling */
- .title-row {
- display: flex;
- align-items: baseline;
- gap: 1rem;
- }
-
- .splash-title {
- font-size: 2.4rem;
- margin: 0;
- padding: 0;
- font-weight: 600;
- color: #FF9900;
- }
-
- .beta-text {
- font-size: 0.9rem;
- color: #FF9900;
- background: rgba(255, 153, 0, 0.1);
- padding: 2px 6px;
- border-radius: 4px;
- font-weight: 500;
- }
-
-
- """
-
-
-def get_javascript() -> str:
- return """
- /* ===================== Global Variables ===================== */
- const root_symbol_html = 'τ';
- let verboseNumbers = false;
-
- /* ===================== Clipboard Functions ===================== */
- /**
- * Copies text to clipboard and shows visual feedback
- * @param {string} text The text to copy
- * @param {HTMLElement} element Optional element to show feedback on
- */
- function copyToClipboard(text, element) {
- navigator.clipboard.writeText(text)
- .then(() => {
- const targetElement = element || (event && event.target);
-
- if (targetElement) {
- const copyIndicator = targetElement.querySelector('.copy-indicator');
-
- if (copyIndicator) {
- const originalText = copyIndicator.textContent;
- copyIndicator.textContent = 'Copied!';
- copyIndicator.style.color = '#FF9900';
-
- setTimeout(() => {
- copyIndicator.textContent = originalText;
- copyIndicator.style.color = '';
- }, 1000);
- } else {
- const originalText = targetElement.textContent;
- targetElement.textContent = 'Copied!';
- targetElement.style.color = '#FF9900';
-
- setTimeout(() => {
- targetElement.textContent = originalText;
- targetElement.style.color = '';
- }, 1000);
- }
- }
- })
- .catch(err => {
- console.error('Failed to copy:', err);
- });
- }
-
-
- /* ===================== Initialization and DOMContentLoaded Handler ===================== */
- document.addEventListener('DOMContentLoaded', function() {
- try {
- const initialDataElement = document.getElementById('initial-data');
- if (!initialDataElement) {
- throw new Error('Initial data element (#initial-data) not found.');
- }
- window.initialData = {
- wallet_info: JSON.parse(initialDataElement.getAttribute('data-wallet-info')),
- subnets: JSON.parse(initialDataElement.getAttribute('data-subnets'))
- };
- } catch (error) {
- console.error('Error loading initial data:', error);
- }
-
- // Return to the main list of subnets.
- const backButton = document.querySelector('.back-button');
- if (backButton) {
- backButton.addEventListener('click', function() {
- // First check if neuron details are visible and close them if needed
- const neuronDetails = document.getElementById('neuron-detail-container');
- if (neuronDetails && neuronDetails.style.display !== 'none') {
- closeNeuronDetails();
- return; // Stop here, don't go back to main page yet
- }
-
- // Otherwise go back to main subnet list
- document.getElementById('main-content').style.display = 'block';
- document.getElementById('subnet-page').style.display = 'none';
- });
- }
-
-
- // Splash screen logic
- const splash = document.getElementById('splash-screen');
- const mainContent = document.getElementById('main-content');
- mainContent.style.display = 'none';
-
- setTimeout(() => {
- splash.classList.add('fade-out');
- splash.addEventListener('transitionend', () => {
- splash.style.display = 'none';
- mainContent.style.display = 'block';
- }, { once: true });
- }, 2000);
-
- initializeFormattedNumbers();
-
- // Keep main page's "verbose" checkbox and the Subnet page's "verbose" checkbox in sync
- const mainVerboseCheckbox = document.getElementById('show-verbose');
- const subnetVerboseCheckbox = document.getElementById('verbose-toggle');
- if (mainVerboseCheckbox && subnetVerboseCheckbox) {
- mainVerboseCheckbox.addEventListener('change', function() {
- subnetVerboseCheckbox.checked = this.checked;
- toggleVerboseNumbers();
- });
- subnetVerboseCheckbox.addEventListener('change', function() {
- mainVerboseCheckbox.checked = this.checked;
- toggleVerboseNumbers();
- });
- }
-
- // Initialize tile view as default
- const tilesContainer = document.getElementById('subnet-tiles-container');
- const tableContainer = document.querySelector('.subnets-table-container');
-
- // Generate and show tiles
- generateSubnetTiles();
- tilesContainer.style.display = 'flex';
- tableContainer.style.display = 'none';
- });
-
- /* ===================== Main Page Functions ===================== */
- /**
- * Sort the main Subnets table by the specified column index.
- * Toggles ascending/descending on each click.
- * @param {number} columnIndex Index of the column to sort.
- */
- function sortMainTable(columnIndex) {
- const table = document.querySelector('.subnets-table');
- const headers = table.querySelectorAll('th');
- const header = headers[columnIndex];
-
- // Determine new sort direction
- let isDescending = header.getAttribute('data-sort') !== 'desc';
-
- // Clear sort markers on all columns, then set the new one
- headers.forEach(th => { th.removeAttribute('data-sort'); });
- header.setAttribute('data-sort', isDescending ? 'desc' : 'asc');
-
- // Sort rows based on numeric value (or netuid in col 0)
- const tbody = table.querySelector('tbody');
- const rows = Array.from(tbody.querySelectorAll('tr'));
- rows.sort((rowA, rowB) => {
- const cellA = rowA.cells[columnIndex];
- const cellB = rowB.cells[columnIndex];
-
- // Special handling for the first column with netuid in data-value
- if (columnIndex === 0) {
- const netuidA = parseInt(cellA.getAttribute('data-value'), 10);
- const netuidB = parseInt(cellB.getAttribute('data-value'), 10);
- return isDescending ? (netuidB - netuidA) : (netuidA - netuidB);
- }
-
- // Otherwise parse float from data-value
- const valueA = parseFloat(cellA.getAttribute('data-value')) || 0;
- const valueB = parseFloat(cellB.getAttribute('data-value')) || 0;
- return isDescending ? (valueB - valueA) : (valueA - valueB);
- });
-
- // Re-inject rows in sorted order
- tbody.innerHTML = '';
- rows.forEach(row => tbody.appendChild(row));
- }
-
- /**
- * Filters the main Subnets table rows based on user search and "Show Only Staked" checkbox.
- */
- function filterSubnets() {
- const searchText = document.getElementById('subnet-search').value.toLowerCase();
- const showStaked = document.getElementById('show-staked').checked;
- const showTiles = document.getElementById('show-tiles').checked;
-
- // Filter table rows
- const rows = document.querySelectorAll('.subnet-row');
- rows.forEach(row => {
- const name = row.querySelector('.subnet-name').textContent.toLowerCase();
- const stakeStatus = row.querySelector('.stake-status').textContent; // "Staked" or "Not Staked"
-
- let isVisible = name.includes(searchText);
- if (showStaked) {
- // If "Show only Staked" is checked, the row must have "Staked" to be visible
- isVisible = isVisible && (stakeStatus === 'Staked');
- }
- row.style.display = isVisible ? '' : 'none';
- });
-
- // Filter tiles if they're being shown
- if (showTiles) {
- const tiles = document.querySelectorAll('.subnet-tile');
- tiles.forEach(tile => {
- const name = tile.querySelector('.tile-name').textContent.toLowerCase();
- const netuid = tile.querySelector('.tile-netuid').textContent;
- const isStaked = tile.classList.contains('staked');
-
- let isVisible = name.includes(searchText) || netuid.includes(searchText);
- if (showStaked) {
- isVisible = isVisible && isStaked;
- }
- tile.style.display = isVisible ? '' : 'none';
- });
- }
- }
-
-
- /* ===================== Subnet Detail Page Functions ===================== */
- /**
- * Displays the Subnet page (detailed view) for the selected netuid.
- * Hides the main content and populates all the metrics / stakes / network table.
- * @param {number} netuid The netuid of the subnet to show in detail.
- */
- function showSubnetPage(netuid) {
- try {
- window.currentSubnet = netuid;
- window.scrollTo(0, 0);
-
- const subnet = window.initialData.subnets.find(s => s.netuid === parseInt(netuid, 10));
- if (!subnet) {
- throw new Error(`Subnet not found for netuid: ${netuid}`);
- }
- window.currentSubnetSymbol = subnet.symbol;
-
- // Insert the "metagraph" table beneath the "stakes" table in the hidden container
- const networkTableHTML = `
-
-
-
-
-
-
-
- Name
- Stake Weight
- Stake ${subnet.symbol}
- Stake ${root_symbol_html}
- Dividends
- Incentive
- Emissions /day
- Hotkey
- Coldkey
-
-
-
- ${generateNetworkTableRows(subnet.metagraph_info)}
-
-
-
- `;
-
- // Show/hide main content vs. subnet detail
- document.getElementById('main-content').style.display = 'none';
- document.getElementById('subnet-page').style.display = 'block';
-
- document.querySelector('#subnet-title').textContent = `${subnet.netuid} - ${subnet.name}`;
- document.querySelector('#subnet-price').innerHTML = formatNumber(subnet.price, subnet.symbol);
- document.querySelector('#subnet-market-cap').innerHTML = formatNumber(subnet.market_cap, root_symbol_html);
- document.querySelector('#subnet-total-stake').innerHTML= formatNumber(subnet.total_stake, subnet.symbol);
- document.querySelector('#subnet-emission').innerHTML = formatNumber(subnet.emission, root_symbol_html);
-
-
- const metagraphInfo = subnet.metagraph_info;
- document.querySelector('#network-alpha-in').innerHTML = formatNumber(metagraphInfo.alpha_in, subnet.symbol);
- document.querySelector('#network-tau-in').innerHTML = formatNumber(metagraphInfo.tao_in, root_symbol_html);
- document.querySelector('#network-moving-price').innerHTML = formatNumber(metagraphInfo.moving_price, subnet.symbol);
-
- // Registration status
- const registrationElement = document.querySelector('#network-registration');
- registrationElement.textContent = metagraphInfo.registration_allowed ? 'Open' : 'Closed';
- registrationElement.classList.toggle('closed', !metagraphInfo.registration_allowed);
-
- // Commit-Reveal Weight status
- const crElement = document.querySelector('#network-cr');
- crElement.textContent = metagraphInfo.commit_reveal_weights_enabled ? 'Enabled' : 'Disabled';
- crElement.classList.toggle('disabled', !metagraphInfo.commit_reveal_weights_enabled);
-
- // Blocks since last step, out of tempo
- document.querySelector('#network-blocks-since-step').innerHTML =
- `${metagraphInfo.blocks_since_last_step}/${metagraphInfo.tempo}`;
-
- // Number of neurons vs. max
- document.querySelector('#network-neurons').innerHTML =
- `${metagraphInfo.num_uids}/${metagraphInfo.max_uids}`;
-
- // Update "Your Stakes" table
- const stakesTableBody = document.querySelector('#stakes-table-body');
- stakesTableBody.innerHTML = '';
- if (subnet.your_stakes && subnet.your_stakes.length > 0) {
- subnet.your_stakes.forEach(stake => {
- const row = document.createElement('tr');
- row.innerHTML = `
-
-
- ${stake.hotkey_identity}
-
- copy
-
-
- ${formatNumber(stake.amount, subnet.symbol)}
- ${formatNumber(stake.ideal_value, root_symbol_html)}
- ${formatNumber(stake.slippage_value, root_symbol_html)} (${stake.slippage_percentage.toFixed(2)}%)
- ${formatNumber(stake.emission, subnet.symbol + '/day')}
- ${formatNumber(stake.tao_emission, root_symbol_html + '/day')}
-
-
- ${stake.is_registered ? 'Yes' : 'No'}
-
-
-
- Coming soon
-
- `;
- stakesTableBody.appendChild(row);
- });
- } else {
- // If no user stake in this subnet
- stakesTableBody.innerHTML = `
-
- No stakes found for this subnet
-
- `;
- }
-
- // Remove any previously injected network table then add the new one
- const existingNetworkTable = document.querySelector('.network-table-container');
- if (existingNetworkTable) {
- existingNetworkTable.remove();
- }
- document.querySelector('.stakes-table-container').insertAdjacentHTML('afterend', networkTableHTML);
-
- // Format the new numbers
- initializeFormattedNumbers();
+ template = jinja_env.get_template("view.j2")
- // Initialize connectivity visualization (the dots / lines "animation")
- setTimeout(() => { initNetworkVisualization(); }, 100);
-
- // Toggle whether we are showing the "Your Stakes" or "Metagraph" table
- toggleStakeView();
-
- // Initialize sorting on newly injected table columns
- initializeSorting();
-
- // Auto-sort by Stake descending on the network table for convenience
- setTimeout(() => {
- const networkTable = document.querySelector('.network-table');
- if (networkTable) {
- const stakeColumn = networkTable.querySelector('th:nth-child(2)');
- if (stakeColumn) {
- sortTable(networkTable, 1, stakeColumn, true);
- stakeColumn.setAttribute('data-sort', 'desc');
- }
- }
- }, 100);
-
- console.log('Subnet page updated successfully');
- } catch (error) {
- console.error('Error updating subnet page:', error);
- }
- }
-
- /**
- * Generates the rows for the "Neurons" table (shown when the user unchecks "Show Stakes").
- * Each row, when clicked, calls showNeuronDetails(i).
- * @param {Object} metagraphInfo The "metagraph_info" of the subnet that holds hotkeys, etc.
- */
- function generateNetworkTableRows(metagraphInfo) {
- const rows = [];
- console.log('Generating network table rows with data:', metagraphInfo);
-
- for (let i = 0; i < metagraphInfo.hotkeys.length; i++) {
- // Subnet symbol is used to show token vs. root stake
- const subnet = window.initialData.subnets.find(s => s.netuid === window.currentSubnet);
- const subnetSymbol = subnet ? subnet.symbol : '';
-
- // Possibly show hotkey/coldkey truncated for readability
- const truncatedHotkey = truncateAddress(metagraphInfo.hotkeys[i]);
- const truncatedColdkey = truncateAddress(metagraphInfo.coldkeys[i]);
- const identityName = metagraphInfo.updated_identities[i] || '~';
-
- // Root stake is being scaled by 0.18 arbitrarily here
- const adjustedRootStake = metagraphInfo.tao_stake[i] * 0.18;
-
- rows.push(`
-
- ${identityName}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ${truncatedHotkey}
- copy
-
-
-
-
- ${truncatedColdkey}
- copy
-
-
-
- `);
- }
- return rows.join('');
- }
-
- /**
- * Handles toggling between the "Your Stakes" view and the "Neurons" view on the Subnet page.
- * The "Show Stakes" checkbox (#stake-toggle) controls which table is visible.
- */
- function toggleStakeView() {
- const showStakes = document.getElementById('stake-toggle').checked;
- const stakesTable = document.querySelector('.stakes-table-container');
- const networkTable = document.querySelector('.network-table-container');
- const sectionHeader = document.querySelector('.view-header');
- const neuronDetails = document.getElementById('neuron-detail-container');
- const addStakeButton = document.querySelector('.add-stake-button');
- const exportCsvButton = document.querySelector('.export-csv-button');
- const stakesHeader = document.querySelector('.stakes-header');
-
- // First, close neuron details if they're open
- if (neuronDetails && neuronDetails.style.display !== 'none') {
- neuronDetails.style.display = 'none';
- }
-
- // Always show the section header and stakes header when toggling views
- if (sectionHeader) sectionHeader.style.display = 'block';
- if (stakesHeader) stakesHeader.style.display = 'flex';
-
- if (showStakes) {
- // Show the Stakes table, hide the Neurons table
- stakesTable.style.display = 'block';
- networkTable.style.display = 'none';
- sectionHeader.textContent = 'Your Stakes';
- if (addStakeButton) {
- addStakeButton.style.display = 'none';
- }
- if (exportCsvButton) {
- exportCsvButton.style.display = 'none';
- }
- } else {
- // Show the Neurons table, hide the Stakes table
- stakesTable.style.display = 'none';
- networkTable.style.display = 'block';
- sectionHeader.textContent = 'Metagraph';
- if (addStakeButton) {
- addStakeButton.style.display = 'block';
- }
- if (exportCsvButton) {
- exportCsvButton.style.display = 'block';
- }
- }
- }
-
- /**
- * Called when you click a row in the "Neurons" table, to display more detail about that neuron.
- * This hides the "Neurons" table and shows the #neuron-detail-container.
- * @param {number} rowIndex The index of the neuron in the arrays (hotkeys, coldkeys, etc.)
- */
- function showNeuronDetails(rowIndex) {
- try {
- // Hide the network table & stakes table
- const networkTable = document.querySelector('.network-table-container');
- if (networkTable) networkTable.style.display = 'none';
- const stakesTable = document.querySelector('.stakes-table-container');
- if (stakesTable) stakesTable.style.display = 'none';
-
- // Hide the stakes header with the action buttons
- const stakesHeader = document.querySelector('.stakes-header');
- if (stakesHeader) stakesHeader.style.display = 'none';
-
- // Hide the view header that says "Neurons"
- const viewHeader = document.querySelector('.view-header');
- if (viewHeader) viewHeader.style.display = 'none';
-
- // Show the neuron detail panel
- const detailContainer = document.getElementById('neuron-detail-container');
- if (detailContainer) detailContainer.style.display = 'block';
-
- // Pull out the current subnet
- const subnet = window.initialData.subnets.find(s => s.netuid === window.currentSubnet);
- if (!subnet) {
- console.error('No subnet data for netuid:', window.currentSubnet);
- return;
- }
-
- const metagraphInfo = subnet.metagraph_info;
- const subnetSymbol = subnet.symbol || '';
-
- // Pull axon data, for IP info
- const axonData = metagraphInfo.processed_axons ? metagraphInfo.processed_axons[rowIndex] : null;
- let ipInfoString;
-
- // Update IP info card - hide header if IP info is present
- const ipInfoCard = document.getElementById('neuron-ipinfo').closest('.metric-card');
- if (axonData && axonData.ip !== 'N/A') {
- // If we have valid IP info, hide the "IP Info" label
- if (ipInfoCard && ipInfoCard.querySelector('.metric-label')) {
- ipInfoCard.querySelector('.metric-label').style.display = 'none';
- }
- // Format IP info with green labels
- ipInfoString = `IP: ${axonData.ip} ` +
- `Port: ${axonData.port} ` +
- `Type: ${axonData.ip_type}`;
- } else {
- // If no IP info, show the label
- if (ipInfoCard && ipInfoCard.querySelector('.metric-label')) {
- ipInfoCard.querySelector('.metric-label').style.display = 'block';
- }
- ipInfoString = 'N/A ';
- }
-
- // Basic identity and hotkey/coldkey info
- const name = metagraphInfo.updated_identities[rowIndex] || '~';
- const hotkey = metagraphInfo.hotkeys[rowIndex];
- const coldkey = metagraphInfo.coldkeys[rowIndex];
- const rank = metagraphInfo.rank ? metagraphInfo.rank[rowIndex] : 0;
- const trust = metagraphInfo.trust ? metagraphInfo.trust[rowIndex] : 0;
- const pruning = metagraphInfo.pruning_score ? metagraphInfo.pruning_score[rowIndex] : 0;
- const vPermit = metagraphInfo.validator_permit ? metagraphInfo.validator_permit[rowIndex] : false;
- const lastUpd = metagraphInfo.last_update ? metagraphInfo.last_update[rowIndex] : 0;
- const consensus = metagraphInfo.consensus ? metagraphInfo.consensus[rowIndex] : 0;
- const regBlock = metagraphInfo.block_at_registration ? metagraphInfo.block_at_registration[rowIndex] : 0;
- const active = metagraphInfo.active ? metagraphInfo.active[rowIndex] : false;
-
- // Update UI fields
- document.getElementById('neuron-name').textContent = name;
- document.getElementById('neuron-name').style.color = '#FF9900';
-
- document.getElementById('neuron-hotkey').textContent = hotkey;
- document.getElementById('neuron-coldkey').textContent = coldkey;
- document.getElementById('neuron-trust').textContent = trust.toFixed(4);
- document.getElementById('neuron-pruning-score').textContent = pruning.toFixed(4);
-
- // Validator
- const validatorElem = document.getElementById('neuron-validator-permit');
- if (vPermit) {
- validatorElem.style.color = '#2ECC71';
- validatorElem.textContent = 'True';
- } else {
- validatorElem.style.color = '#ff4444';
- validatorElem.textContent = 'False';
- }
-
- document.getElementById('neuron-last-update').textContent = lastUpd;
- document.getElementById('neuron-consensus').textContent = consensus.toFixed(4);
- document.getElementById('neuron-reg-block').textContent = regBlock;
- document.getElementById('neuron-ipinfo').innerHTML = ipInfoString;
-
- const activeElem = document.getElementById('neuron-active');
- if (active) {
- activeElem.style.color = '#2ECC71';
- activeElem.textContent = 'Yes';
- } else {
- activeElem.style.color = '#ff4444';
- activeElem.textContent = 'No';
- }
-
- // Add stake data ("total_stake", "alpha_stake", "tao_stake")
- document.getElementById('neuron-stake-total').setAttribute(
- 'data-value', metagraphInfo.total_stake[rowIndex]
- );
- document.getElementById('neuron-stake-total').setAttribute(
- 'data-symbol', subnetSymbol
- );
-
- document.getElementById('neuron-stake-token').setAttribute(
- 'data-value', metagraphInfo.alpha_stake[rowIndex]
- );
- document.getElementById('neuron-stake-token').setAttribute(
- 'data-symbol', subnetSymbol
- );
-
- // Multiply tao_stake by 0.18
- const originalStakeRoot = metagraphInfo.tao_stake[rowIndex];
- const calculatedStakeRoot = originalStakeRoot * 0.18;
-
- document.getElementById('neuron-stake-root').setAttribute(
- 'data-value', calculatedStakeRoot
- );
- document.getElementById('neuron-stake-root').setAttribute(
- 'data-symbol', root_symbol_html
- );
- // Also set the inner text right away, so we show a correct format on load
- document.getElementById('neuron-stake-root').innerHTML =
- formatNumber(calculatedStakeRoot, root_symbol_html);
-
- // Dividends, Incentive
- document.getElementById('neuron-dividends').setAttribute(
- 'data-value', metagraphInfo.dividends[rowIndex]
- );
- document.getElementById('neuron-dividends').setAttribute('data-symbol', '');
-
- document.getElementById('neuron-incentive').setAttribute(
- 'data-value', metagraphInfo.incentives[rowIndex]
- );
- document.getElementById('neuron-incentive').setAttribute('data-symbol', '');
-
- // Emissions
- document.getElementById('neuron-emissions').setAttribute(
- 'data-value', metagraphInfo.emission[rowIndex]
- );
- document.getElementById('neuron-emissions').setAttribute('data-symbol', subnetSymbol);
-
- // Rank
- document.getElementById('neuron-rank').textContent = rank.toFixed(4);
-
- // Re-run formatting so the newly updated data-values appear in numeric form
- initializeFormattedNumbers();
- } catch (err) {
- console.error('Error showing neuron details:', err);
- }
- }
-
- /**
- * Closes the neuron detail panel and goes back to whichever table was selected ("Stakes" or "Metagraph").
- */
- function closeNeuronDetails() {
- // Hide neuron details
- const detailContainer = document.getElementById('neuron-detail-container');
- if (detailContainer) detailContainer.style.display = 'none';
-
- // Show the stakes header with action buttons
- const stakesHeader = document.querySelector('.stakes-header');
- if (stakesHeader) stakesHeader.style.display = 'flex';
-
- // Show the view header again
- const viewHeader = document.querySelector('.view-header');
- if (viewHeader) viewHeader.style.display = 'block';
-
- // Show the appropriate table based on toggle state
- const showStakes = document.getElementById('stake-toggle').checked;
- const stakesTable = document.querySelector('.stakes-table-container');
- const networkTable = document.querySelector('.network-table-container');
-
- if (showStakes) {
- stakesTable.style.display = 'block';
- networkTable.style.display = 'none';
-
- // Hide action buttons when showing stakes
- const addStakeButton = document.querySelector('.add-stake-button');
- const exportCsvButton = document.querySelector('.export-csv-button');
- if (addStakeButton) addStakeButton.style.display = 'none';
- if (exportCsvButton) exportCsvButton.style.display = 'none';
- } else {
- stakesTable.style.display = 'none';
- networkTable.style.display = 'block';
-
- // Show action buttons when showing metagraph
- const addStakeButton = document.querySelector('.add-stake-button');
- const exportCsvButton = document.querySelector('.export-csv-button');
- if (addStakeButton) addStakeButton.style.display = 'block';
- if (exportCsvButton) exportCsvButton.style.display = 'block';
- }
- }
-
-
- /* ===================== Number Formatting Functions ===================== */
- /**
- * Toggles the numeric display between "verbose" and "short" notations
- * across all .formatted-number elements on the page.
- */
- function toggleVerboseNumbers() {
- // We read from the main or subnet checkboxes
- verboseNumbers =
- document.getElementById('verbose-toggle')?.checked ||
- document.getElementById('show-verbose')?.checked ||
- false;
-
- // Reformat all visible .formatted-number elements
- document.querySelectorAll('.formatted-number').forEach(element => {
- const value = parseFloat(element.dataset.value);
- const symbol = element.dataset.symbol;
- element.innerHTML = formatNumber(value, symbol);
- });
-
- // If we're currently on the Subnet detail page, update those numbers too
- if (document.getElementById('subnet-page').style.display !== 'none') {
- updateAllNumbers();
- }
- }
-
- /**
- * Scans all .formatted-number elements and replaces their text with
- * the properly formatted version (short or verbose).
- */
- function initializeFormattedNumbers() {
- document.querySelectorAll('.formatted-number').forEach(element => {
- const value = parseFloat(element.dataset.value);
- const symbol = element.dataset.symbol;
- element.innerHTML = formatNumber(value, symbol);
- });
- }
-
- /**
- * Called by toggleVerboseNumbers() to reformat key metrics on the Subnet page
- * that might not be directly wrapped in .formatted-number but need to be updated anyway.
- */
- function updateAllNumbers() {
- try {
- const subnet = window.initialData.subnets.find(s => s.netuid === window.currentSubnet);
- if (!subnet) {
- console.error('Could not find subnet data for netuid:', window.currentSubnet);
- return;
- }
- // Reformat a few items in the Subnet detail header
- document.querySelector('#subnet-market-cap').innerHTML =
- formatNumber(subnet.market_cap, root_symbol_html);
- document.querySelector('#subnet-total-stake').innerHTML =
- formatNumber(subnet.total_stake, subnet.symbol);
- document.querySelector('#subnet-emission').innerHTML =
- formatNumber(subnet.emission, root_symbol_html);
-
- // Reformat the Metagraph table data
- const netinfo = subnet.metagraph_info;
- document.querySelector('#network-alpha-in').innerHTML =
- formatNumber(netinfo.alpha_in, subnet.symbol);
- document.querySelector('#network-tau-in').innerHTML =
- formatNumber(netinfo.tao_in, root_symbol_html);
-
- // Reformat items in "Your Stakes" table
- document.querySelectorAll('#stakes-table-body .formatted-number').forEach(element => {
- const value = parseFloat(element.dataset.value);
- const symbol = element.dataset.symbol;
- element.innerHTML = formatNumber(value, symbol);
- });
- } catch (error) {
- console.error('Error updating numbers:', error);
- }
- }
-
- /**
- * Format a numeric value into either:
- * - a short format (e.g. 1.23k, 3.45m) if verboseNumbers==false
- * - a more precise format (1,234.5678) if verboseNumbers==true
- * @param {number} num The numeric value to format.
- * @param {string} symbol A short suffix or currency symbol (e.g. 'τ') that we append.
- */
- function formatNumber(num, symbol = '') {
- if (num === undefined || num === null || isNaN(num)) {
- return '0.00 ' + `${symbol} `;
- }
- num = parseFloat(num);
- if (num === 0) {
- return '0.00 ' + `${symbol} `;
- }
-
- // If user requested verbose
- if (verboseNumbers) {
- return num.toLocaleString('en-US', {
- minimumFractionDigits: 4,
- maximumFractionDigits: 4
- }) + ' ' + `${symbol} `;
- }
-
- // Otherwise show short scale for large numbers
- const absNum = Math.abs(num);
- if (absNum >= 1000) {
- const suffixes = ['', 'k', 'm', 'b', 't'];
- const magnitude = Math.min(4, Math.floor(Math.log10(absNum) / 3));
- const scaledNum = num / Math.pow(10, magnitude * 3);
- return scaledNum.toFixed(2) + suffixes[magnitude] + ' ' +
- `${symbol} `;
- } else {
- // For small numbers <1000, just show 4 decimals
- return num.toFixed(4) + ' ' + `${symbol} `;
- }
- }
-
- /**
- * Truncates a string address into the format "ABC..XYZ" for a bit more readability
- * @param {string} address
- * @returns {string} truncated address form
- */
- function truncateAddress(address) {
- if (!address || address.length <= 7) {
- return address; // no need to truncate if very short
- }
- return `${address.substring(0, 3)}..${address.substring(address.length - 3)}`;
- }
-
- /**
- * Format a number in compact notation (K, M, B) for tile display
- */
- function formatTileNumbers(num) {
- if (num >= 1000000000) {
- return (num / 1000000000).toFixed(1) + 'B';
- } else if (num >= 1000000) {
- return (num / 1000000).toFixed(1) + 'M';
- } else if (num >= 1000) {
- return (num / 1000).toFixed(1) + 'K';
- } else {
- return num.toFixed(1);
- }
- }
-
-
- /* ===================== Table Sorting and Filtering Functions ===================== */
- /**
- * Switches the Metagraph or Stakes table from sorting ascending to descending on a column, and vice versa.
- * @param {HTMLTableElement} table The table element itself
- * @param {number} columnIndex The column index to sort by
- * @param {HTMLTableHeaderCellElement} header The element clicked
- * @param {boolean} forceDescending If true and no existing sort marker, will do a descending sort by default
- */
- function sortTable(table, columnIndex, header, forceDescending = false) {
- const tbody = table.querySelector('tbody');
- const rows = Array.from(tbody.querySelectorAll('tr'));
-
- // If forcing descending and the header has no 'data-sort', default to 'desc'
- let isDescending;
- if (forceDescending && !header.hasAttribute('data-sort')) {
- isDescending = true;
- } else {
- isDescending = header.getAttribute('data-sort') !== 'desc';
- }
-
- // Clear data-sort from all headers in the table
- table.querySelectorAll('th').forEach(th => {
- th.removeAttribute('data-sort');
- });
- // Mark the clicked header with new direction
- header.setAttribute('data-sort', isDescending ? 'desc' : 'asc');
-
- // Sort numerically
- rows.sort((rowA, rowB) => {
- const cellA = rowA.cells[columnIndex];
- const cellB = rowB.cells[columnIndex];
-
- // Attempt to parse float from data-value or fallback to textContent
- let valueA = parseFloat(cellA.getAttribute('data-value')) ||
- parseFloat(cellA.textContent.replace(/[^\\d.-]/g, '')) ||
- 0;
- let valueB = parseFloat(cellB.getAttribute('data-value')) ||
- parseFloat(cellB.textContent.replace(/[^\\d.-]/g, '')) ||
- 0;
-
- return isDescending ? (valueB - valueA) : (valueA - valueB);
- });
-
- // Reinsert sorted rows
- tbody.innerHTML = '';
- rows.forEach(row => tbody.appendChild(row));
- }
-
- /**
- * Adds sortable behavior to certain columns in the "stakes-table" or "network-table".
- * Called after these tables are created in showSubnetPage().
- */
- function initializeSorting() {
- const networkTable = document.querySelector('.network-table');
- if (networkTable) {
- initializeTableSorting(networkTable);
- }
- const stakesTable = document.querySelector('.stakes-table');
- if (stakesTable) {
- initializeTableSorting(stakesTable);
- }
- }
-
- /**
- * Helper function that attaches sort handlers to appropriate columns in a table.
- * @param {HTMLTableElement} table The table element to set up sorting for.
- */
- function initializeTableSorting(table) {
- const headers = table.querySelectorAll('th');
- headers.forEach((header, index) => {
- // We only want some columns to be sortable, as in original code
- if (table.classList.contains('stakes-table') && index >= 1 && index <= 5) {
- header.classList.add('sortable');
- header.addEventListener('click', () => {
- sortTable(table, index, header, true);
- });
- } else if (table.classList.contains('network-table') && index < 6) {
- header.classList.add('sortable');
- header.addEventListener('click', () => {
- sortTable(table, index, header, true);
- });
- }
- });
- }
-
- /**
- * Filters rows in the Metagraph table by name, hotkey, or coldkey.
- * Invoked by the oninput event of the #network-search field.
- * @param {string} searchValue The substring typed by the user.
- */
- function filterNetworkTable(searchValue) {
- const searchTerm = searchValue.toLowerCase().trim();
- const rows = document.querySelectorAll('.network-table tbody tr');
-
- rows.forEach(row => {
- const nameCell = row.querySelector('.identity-cell');
- const hotkeyContainer = row.querySelector('.hotkey-container[data-full-address]');
- const coldkeyContainer = row.querySelectorAll('.hotkey-container[data-full-address]')[1];
-
- const name = nameCell ? nameCell.textContent.toLowerCase() : '';
- const hotkey = hotkeyContainer ? hotkeyContainer.getAttribute('data-full-address').toLowerCase() : '';
- const coldkey= coldkeyContainer ? coldkeyContainer.getAttribute('data-full-address').toLowerCase() : '';
-
- const matches = (name.includes(searchTerm) || hotkey.includes(searchTerm) || coldkey.includes(searchTerm));
- row.style.display = matches ? '' : 'none';
- });
- }
-
-
- /* ===================== Network Visualization Functions ===================== */
- /**
- * Initializes the network visualization on the canvas element.
- */
- function initNetworkVisualization() {
- try {
- const canvas = document.getElementById('network-canvas');
- if (!canvas) {
- console.error('Canvas element (#network-canvas) not found');
- return;
- }
- const ctx = canvas.getContext('2d');
-
- const subnet = window.initialData.subnets.find(s => s.netuid === window.currentSubnet);
- if (!subnet) {
- console.error('Could not find subnet data for netuid:', window.currentSubnet);
- return;
- }
- const numNeurons = subnet.metagraph_info.num_uids;
- const nodes = [];
-
- // Randomly place nodes, each with a small velocity
- for (let i = 0; i < numNeurons; i++) {
- nodes.push({
- x: Math.random() * canvas.width,
- y: Math.random() * canvas.height,
- radius: 2,
- vx: (Math.random() - 0.5) * 0.5,
- vy: (Math.random() - 0.5) * 0.5
- });
- }
-
- // Animation loop
- function animate() {
- ctx.clearRect(0, 0, canvas.width, canvas.height);
-
- ctx.beginPath();
- ctx.strokeStyle = 'rgba(255, 153, 0, 0.2)';
- for (let i = 0; i < nodes.length; i++) {
- for (let j = i + 1; j < nodes.length; j++) {
- const dx = nodes[i].x - nodes[j].x;
- const dy = nodes[i].y - nodes[j].y;
- const distance = Math.sqrt(dx * dx + dy * dy);
- if (distance < 30) {
- ctx.moveTo(nodes[i].x, nodes[i].y);
- ctx.lineTo(nodes[j].x, nodes[j].y);
- }
- }
- }
- ctx.stroke();
-
- nodes.forEach(node => {
- node.x += node.vx;
- node.y += node.vy;
-
- // Bounce them off the edges
- if (node.x <= 0 || node.x >= canvas.width) node.vx *= -1;
- if (node.y <= 0 || node.y >= canvas.height) node.vy *= -1;
-
- ctx.beginPath();
- ctx.fillStyle = '#FF9900';
- ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2);
- ctx.fill();
- });
-
- requestAnimationFrame(animate);
- }
- animate();
- } catch (error) {
- console.error('Error in network visualization:', error);
- }
- }
-
-
- /* ===================== Tile View Functions ===================== */
- /**
- * Toggles between the tile view and table view of subnets.
- */
- function toggleTileView() {
- const showTiles = document.getElementById('show-tiles').checked;
- const tilesContainer = document.getElementById('subnet-tiles-container');
- const tableContainer = document.querySelector('.subnets-table-container');
-
- if (showTiles) {
- // Show tiles, hide table
- tilesContainer.style.display = 'flex';
- tableContainer.style.display = 'none';
-
- // Generate tiles if they don't exist yet
- if (tilesContainer.children.length === 0) {
- generateSubnetTiles();
- }
-
- // Apply current filters to the tiles
- filterSubnets();
- } else {
- // Show table, hide tiles
- tilesContainer.style.display = 'none';
- tableContainer.style.display = 'block';
- }
- }
-
- /**
- * Generates the subnet tiles based on the initialData.
- */
- function generateSubnetTiles() {
- const tilesContainer = document.getElementById('subnet-tiles-container');
- tilesContainer.innerHTML = ''; // Clear existing tiles
-
- // Sort subnets by market cap (descending)
- const sortedSubnets = [...window.initialData.subnets].sort((a, b) => b.market_cap - a.market_cap);
-
- sortedSubnets.forEach(subnet => {
- const isStaked = subnet.your_stakes && subnet.your_stakes.length > 0;
- const marketCapFormatted = formatTileNumbers(subnet.market_cap);
-
- const tile = document.createElement('div');
- tile.className = `subnet-tile ${isStaked ? 'staked' : ''}`;
- tile.onclick = () => showSubnetPage(subnet.netuid);
-
- // Calculate background intensity based on market cap relative to max
- const maxMarketCap = sortedSubnets[0].market_cap;
- const intensity = Math.max(5, Math.min(15, 5 + (subnet.market_cap / maxMarketCap) * 10));
-
- tile.innerHTML = `
- ${subnet.netuid}
- ${subnet.symbol}
- ${subnet.name}
- ${marketCapFormatted} ${root_symbol_html}
- `;
-
- // Set background intensity
- tile.style.background = `rgba(255, 255, 255, 0.0${intensity.toFixed(0)})`;
-
- tilesContainer.appendChild(tile);
- });
- }
- """
+ return template.render(
+ root_symbol_html=ROOT_SYMBOL_HTML,
+ block_number=block_number,
+ truncated_coldkey=truncated_coldkey,
+ slippage_percentage=slippage_percentage,
+ wallet_info=wallet_info,
+ subnets=data["subnets"],
+ )
diff --git a/bittensor_cli/src/commands/wallets.py b/bittensor_cli/src/commands/wallets.py
index 6b4a6f50b..40ea61218 100644
--- a/bittensor_cli/src/commands/wallets.py
+++ b/bittensor_cli/src/commands/wallets.py
@@ -9,7 +9,6 @@
from bittensor_wallet import Wallet, Keypair
from bittensor_wallet.errors import KeyFileError
from bittensor_wallet.keyfile import Keyfile
-from fuzzywuzzy import fuzz
from rich import box
from rich.align import Align
from rich.table import Column, Table
@@ -1227,16 +1226,11 @@ async def overview(
if sort_by:
column_to_sort_by: int = 0
- highest_matching_ratio: int = 0
sort_descending: bool = False # Default sort_order to ascending
for index, column in zip(range(len(table.columns)), table.columns):
- # Fuzzy match the column name. Default to the first column.
column_name = column.header.lower().replace("[white]", "")
- match_ratio = fuzz.ratio(sort_by.lower(), column_name)
- # Finds the best matching column
- if match_ratio > highest_matching_ratio:
- highest_matching_ratio = match_ratio
+ if column_name == sort_by.lower().strip():
column_to_sort_by = index
if sort_order.lower() in {"desc", "descending", "reverse"}:
diff --git a/pyproject.toml b/pyproject.toml
index 919a9d3b5..3eb900c75 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "bittensor-cli"
-version = "9.4.4"
+version = "9.5.0"
description = "Bittensor CLI"
readme = "README.md"
authors = [
@@ -20,20 +20,16 @@ dependencies = [
"backoff~=2.2.1",
"click<8.2.0", # typer.testing.CliRunner(mix_stderr=) is broken in click 8.2.0+
"GitPython>=3.0.0",
- "fuzzywuzzy~=0.18.0",
"netaddr~=1.3.0",
"numpy>=2.0.1,<3.0.0",
"Jinja2",
"pycryptodome>=3.0.0,<4.0.0",
"PyYAML~=6.0.1",
- "pytest",
- "python-Levenshtein",
"rich>=13.7,<15.0",
"scalecodec==1.2.11",
"typer>=0.12,<0.16",
"bittensor-wallet>=3.0.7",
"plotille>=5.0.0",
- "pywry>=0.6.2",
"plotly>=6.0.0",
]
@@ -41,6 +37,11 @@ dependencies = [
cuda = [
"torch>=1.13.1,<3.0",
]
+dev = [
+ "pytest",
+ "pytest-asyncio",
+ "ruff==0.11.5",
+]
[project.urls]
# more details can be found here
diff --git a/tests/e2e_tests/test_wallet_creations.py b/tests/e2e_tests/test_wallet_creations.py
index 5dd3bd63c..7b573dbba 100644
--- a/tests/e2e_tests/test_wallet_creations.py
+++ b/tests/e2e_tests/test_wallet_creations.py
@@ -94,13 +94,13 @@ def verify_key_pattern(output: str, wallet_name: str) -> Optional[str]:
match = re.search(pattern, line)
if match:
# Assert key starts with '5'
- assert match.group(1).startswith(
- "5"
- ), f"{wallet_name} should start with '5'"
+ assert match.group(1).startswith("5"), (
+ f"{wallet_name} should start with '5'"
+ )
# Assert length of key is 48 characters
- assert (
- len(match.group(1)) == 48
- ), f"Key for {wallet_name} should be 48 characters long"
+ assert len(match.group(1)) == 48, (
+ f"Key for {wallet_name} should be 48 characters long"
+ )
found = True
return match.group(1)
@@ -441,9 +441,9 @@ def test_wallet_regen(wallet_setup, capfd):
new_coldkey_mod_time = os.path.getmtime(coldkey_path)
- assert (
- initial_coldkey_mod_time != new_coldkey_mod_time
- ), "Coldkey file was not regenerated as expected"
+ assert initial_coldkey_mod_time != new_coldkey_mod_time, (
+ "Coldkey file was not regenerated as expected"
+ )
json_result = exec_command(
command="wallet",
sub_command="regen-coldkey",
@@ -502,9 +502,9 @@ def test_wallet_regen(wallet_setup, capfd):
new_coldkeypub_mod_time = os.path.getmtime(coldkeypub_path)
- assert (
- initial_coldkeypub_mod_time != new_coldkeypub_mod_time
- ), "Coldkeypub file was not regenerated as expected"
+ assert initial_coldkeypub_mod_time != new_coldkeypub_mod_time, (
+ "Coldkeypub file was not regenerated as expected"
+ )
print("Passed wallet regen_coldkeypub command ✅")
# -----------------------------
@@ -537,9 +537,9 @@ def test_wallet_regen(wallet_setup, capfd):
new_hotkey_mod_time = os.path.getmtime(hotkey_path)
- assert (
- initial_hotkey_mod_time != new_hotkey_mod_time
- ), "Hotkey file was not regenerated as expected"
+ assert initial_hotkey_mod_time != new_hotkey_mod_time, (
+ "Hotkey file was not regenerated as expected"
+ )
print("Passed wallet regen_hotkey command ✅")
@@ -593,9 +593,9 @@ def test_wallet_balance_all(local_chain, wallet_setup, capfd):
output = result.stdout
for wallet_name in wallet_names:
- assert (
- wallet_name in output
- ), f"Wallet {wallet_name} not found in balance --all output"
+ assert wallet_name in output, (
+ f"Wallet {wallet_name} not found in balance --all output"
+ )
json_results = exec_command(
"wallet",