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 +#} + +
+ +
+ {{ wallet_info.name }} +
+ {{ truncated_coldkey }}} + Copy +
+
+
+
+ Block + {{ block_number }} +
+
+ Balance + {{ "%.4f"|format(wallet_info.balance) }} {{ root_symbol_html }} +
+
+ Total Stake Value + {{ "%.4f"|format(wallet_info.total_ideal_stake_value) }} {{ root_symbol_html }} +
+
+ Slippage Impact + + {{ "%.2f"|format(slippage_percentage) }}% ({{ "%.4f"|format(wallet_info.total_slippage_value) }} {{ root_symbol_html }}) + +
+
+
\ 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 @@ + \ 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 }} + + + + +
+
+
+ + {% for netuid in sorted_subnet_keys %} + + {% 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 }} + + + + +
+
+
+ {{ "%.6f"|format(stats.current_price) }} {{ stats.symbol }} + 0 else "text-red" }}"> + {{ "▲" if stats.change_pct > 0 else "▼" }} {{ "%.2f"|format(change_pct) }}% + +
+
+
+ {{ interval_hours }}h High: {{ "%.6f"|format(stats.high) }} {{ stats.symbol }} +
+
+ {{ interval_hours }}h Low: {{ "%.6f"|format(stats.low) }} {{ stats.symbol }} +
+
+
+
+
Supply: {{ "%.2f"|format(stats.supply) }} {{ stats.symbol }}
+
Market Cap: {{ "%.2f"|format(stats.market_cap) }} τ
+
Emission: {{ "%.2f"|format(stats.emission) }} {{ stats.symbol }}
+
Stake: {{ "%.2f"|format(stats.stake) }} {{ stats.symbol }}
+
+
+
+ + + \ 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 @@ +
+
+ +
+ + +
+
+ +
+
+

+
+
+
+
+ +
+
+
+
+
+
Moving Price
+
+
+
+
Registration
+
+
+
+
CR Weights
+
+
+
+
Neurons
+
+
+
+
Blocks Since Step
+
+
+
+
\ 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. #} + + \ 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 @@ +
+
+
+
Market Cap
+
+
+
+
Total Stake
+
+
+
+
Alpha Reserves
+
+
+
+
Tao Reserves
+
+
+
+
Emission
+
+
+
+ +
+
+

Metagraph

+
+ + +
+
+ +
+ + + + + + + + + + + + + + + +
HotkeyAmountValueValue (w/ slippage)Alpha emissionTao emissionRegisteredActions
+
+
+
\ 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 @@ +
+ + + + + + + + + + + + + {% 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' %} + + + + + + + + + {% endfor %} + +
SubnetPriceMarket CapYour StakeEmissionStatus
{{subnet.netuid}} - {{subnet.name}}{{stake_status}}
+
\ 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' %} +
+ + + + + + + \ 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 = ` + + `; + + // 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'} + + + + + + `; + 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 - - - - -
-
-
- {stats['current_price']:.6f} {stats['symbol']} - - {sign_icon} {abs(stats['change_pct']):.2f}% - -
-
-
- {interval_hours}h High: {stats['high']:.6f} {stats['symbol']} -
-
- {interval_hours}h Low: {stats['low']:.6f} {stats['symbol']} -
-
-
-
-
Supply: {stats['supply']:.2f} {stats['symbol']}
-
Market Cap: {stats['market_cap']:.2f} τ
-
Emission: {stats['emission']:.2f} {stats['symbol']}
-
Stake: {stats['stake']:.2f} {stats['symbol']}
-
-
-
- - - - """ + 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 = ( - '' + 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' ' - - 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"&#x{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"&#x{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"])} -
- - - - - - - - """ - - -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 """ -
-
- -
- - -
-
- -
-
-

-
-
-
-
- -
-
-
-
-
-
Moving Price
-
-
-
-
Registration
-
-
-
-
CR Weights
-
-
-
-
Neurons
-
-
-
-
Blocks Since Step
-
-
-
-
- """ - - -def generate_subnet_metrics() -> str: - """ - Generates the metrics section for the subnet details page, - including market metrics and the stakes table. - """ - return """ -
-
-
-
Market Cap
-
-
-
-
Total Stake
-
-
-
-
Alpha Reserves
-
-
-
-
Tao Reserves
-
-
-
-
Emission
-
-
-
- -
-
-

Metagraph

-
- - -
-
- -
- - - - - - - - - - - - - - - -
HotkeyAmountValueValue (w/ slippage)Alpha emissionTao emissionRegisteredActions
-
-
-
- """ - - -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 """ - - """ - - -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""" -
- -
- {wallet_info["name"]} -
- {truncated_coldkey} - Copy -
-
-
-
- Block - {block_number} -
-
- Balance - {wallet_info["balance"]:.4f} {root_symbol_html} -
-
- Total Stake Value - {wallet_info["total_ideal_stake_value"]:.4f} {root_symbol_html} -
-
- Slippage Impact - - {slippage_percentage:.2f}% ({wallet_info["total_slippage_value"]:.4f} {root_symbol_html}) - -
-
-
- """ - - -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""" -
- - - - - - - - - - - - - {"".join(rows)} - -
SubnetPriceMarket CapYour StakeEmissionStatus
-
- """ - - -def generate_subnet_details_html() -> str: - return """ - - """ - - -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 = ` - - `; - - // 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'} - - - - - - `; - 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",