Browse Source

explorer/python: Add stats and wasm contracts

x 6 tháng trước cách đây
mục cha
commit
26febf3c98

+ 138 - 1
bin/explorer/python/explorer.py

@@ -196,14 +196,151 @@ async def search():
         except JsonRpcError:
             pass
 
+    # Try as contract ID (base58)
+    try:
+        contract = await rpc.call("get_contract", params=[query])
+        return redirect(url_for("get_contract", contract_id=query))
+    except JsonRpcError:
+        pass
+
     # Nothing found
     return await render_template(
         "error.html",
         network=app.config["NETWORK"],
         error_code="Not Found",
-        error=f"No block or transaction found for: {query}"
+        error=f"No block, transaction, or contract found for: {query}"
     ), 404
 
 
+def format_bytes(size: int) -> str:
+    """Format byte size with appropriate unit."""
+    if size >= 1024 * 1024:
+        return f"{size / (1024 * 1024):.2f} MB"
+    elif size >= 1024:
+        return f"{size / 1024:.2f} KB"
+    else:
+        return f"{size} bytes"
+
+
+@app.route("/contract/<contract_id>")
+async def get_contract(contract_id: str):
+    current_difficulty = await rpc.call("current_difficulty", params=[])
+    current_height = await rpc.call("current_height", params=[])
+    hashrate = await rpc.call("get_hashrate", params=[])
+    contract = await rpc.call("get_contract", params=[contract_id])
+
+    contract["wasm_size_formatted"] = format_bytes(int(contract["wasm_size"]))
+
+    return await render_template(
+        "contract.html",
+        network=app.config["NETWORK"],
+        current_difficulty=current_difficulty[0],
+        current_height=current_height,
+        hashrate=format_hashrate(hashrate),
+        contract=contract,
+    )
+
+
+@app.route("/contracts")
+async def list_contracts():
+    current_difficulty = await rpc.call("current_difficulty", params=[])
+    current_height = await rpc.call("current_height", params=[])
+    hashrate = await rpc.call("get_hashrate", params=[])
+    contracts = await rpc.call("list_contracts", params=[])
+    contract_count = await rpc.call("contract_count", params=[])
+
+    for contract in contracts:
+        contract["wasm_size_formatted"] = format_bytes(int(contract["wasm_size"]))
+
+    return await render_template(
+        "contracts.html",
+        network=app.config["NETWORK"],
+        current_difficulty=current_difficulty[0],
+        current_height=current_height,
+        hashrate=format_hashrate(hashrate),
+        contracts=contracts,
+        contract_count=contract_count,
+    )
+
+
+@app.route("/stats")
+async def stats():
+    current_difficulty = await rpc.call("current_difficulty", params=[])
+    current_height = await rpc.call("current_height", params=[])
+    hashrate = await rpc.call("get_hashrate", params=[])
+    stats_data = await rpc.call("get_stats", params=[])
+
+    return await render_template(
+        "stats.html",
+        network=app.config["NETWORK"],
+        current_difficulty=current_difficulty[0],
+        current_height=current_height,
+        hashrate=format_hashrate(hashrate),
+        stats=stats_data,
+    )
+
+
+@app.route("/stats/daily_tx_chart.png")
+async def daily_tx_chart():
+    """Generate daily average transactions chart as PNG using matplotlib."""
+    import io
+    import matplotlib
+    matplotlib.use('Agg')  # Non-interactive backend
+    import matplotlib.pyplot as plt
+    import matplotlib.dates as mdates
+    from datetime import datetime, timezone
+
+    stats_data = await rpc.call("get_stats", params=[])
+    daily_stats = stats_data.get("daily_stats", [])
+
+    # Filter to last 90 days
+    if daily_stats:
+        max_day = max(d["day"] for d in daily_stats)
+        daily_stats = [d for d in daily_stats if d["day"] >= max_day - 90]
+
+    # Create figure with dark theme
+    plt.style.use('dark_background')
+    fig, ax = plt.subplots(figsize=(12, 4), dpi=100)
+    fig.patch.set_facecolor('#0d1117')
+    ax.set_facecolor('#0d1117')
+
+    if daily_stats:
+        # Convert day numbers to dates
+        dates = [datetime.fromtimestamp(d["day"] * 86400, tz=timezone.utc) for d in daily_stats]
+        values = [d["avg_tx"] for d in daily_stats]
+
+        ax.fill_between(dates, values, alpha=0.3, color='#6366f1')
+        ax.plot(dates, values, color='#6366f1', linewidth=2)
+
+        # Format x-axis
+        ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %d'))
+        ax.xaxis.set_major_locator(mdates.DayLocator(interval=7))
+        plt.xticks(rotation=45, ha='right')
+
+        ax.set_ylabel('Avg TX per Block', color='#9ca3af')
+        ax.tick_params(colors='#9ca3af')
+        ax.spines['bottom'].set_color('#30363d')
+        ax.spines['left'].set_color('#30363d')
+        ax.spines['top'].set_visible(False)
+        ax.spines['right'].set_visible(False)
+        ax.grid(True, alpha=0.2, color='#30363d')
+    else:
+        ax.text(0.5, 0.5, 'No data available', ha='center', va='center',
+                transform=ax.transAxes, color='#9ca3af', fontsize=14)
+        ax.set_xlim(0, 1)
+        ax.set_ylim(0, 1)
+
+    plt.tight_layout()
+
+    # Save to bytes buffer
+    buf = io.BytesIO()
+    plt.savefig(buf, format='png', facecolor='#0d1117', edgecolor='none')
+    plt.close(fig)
+    buf.seek(0)
+
+    from quart import Response
+    return Response(buf.getvalue(), mimetype='image/png')
+
+
 if __name__ == "__main__":
     app.run(host="127.0.0.1", port=5000, debug=True)

+ 3 - 0
bin/explorer/python/requirements.txt

@@ -0,0 +1,3 @@
+quart>=0.20.0
+hypercorn>=0.18.0
+matplotlib>=3.10.0

+ 2 - 1
bin/explorer/python/templates/block.html

@@ -12,7 +12,8 @@
       <div class="logo">DarkFi {{ network }} Blocks</div>
       <nav class="nav">
         <a href="/" class="active">Explorer</a>
-        <a href="#">Stats</a>
+        <a href="/contracts">Contracts</a>
+        <a href="/stats">Stats</a>
         <a href="https://dark.fi/book/">Docs</a>
         <a href="https://codeberg.org/darkrenaissance/darkfi/">Code</a>
       </nav>

+ 124 - 0
bin/explorer/python/templates/contract.html

@@ -0,0 +1,124 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>DarkFi Blocks - Contract</title>
+  <link rel="stylesheet" href="/static/layout.css">
+</head>
+<body>
+  <header class="header">
+    <div class="container flex justify-between items-center">
+      <div class="logo">DarkFi {{ network }} Blocks</div>
+      <nav class="nav">
+        <a href="/">Explorer</a>
+        <a href="/contracts">Contracts</a>
+        <a href="/stats">Stats</a>
+        <a href="https://dark.fi/book/">Docs</a>
+        <a href="https://codeberg.org/darkrenaissance/darkfi/">Code</a>
+      </nav>
+    </div>
+  </header>
+
+  <main class="container section">
+
+    <form action="/search" method="GET" class="input-group mb-lg" style="max-width: 600px;">
+      <input type="search" name="q" class="input" placeholder="Search by block height, hash, transaction, or contract ID...">
+      <button type="submit" class="btn">Search</button>
+    </form>
+
+    <div class="stats-grid mb-lg">
+      <div class="stat-card">
+        <div class="label">Difficulty</div>
+        <div class="value">{{ current_difficulty }}</div>
+      </div>
+      <div class="stat-card">
+        <div class="label">Height</div>
+        <div class="value">{{ current_height }}</div>
+      </div>
+      <div class="stat-card">
+        <div class="label">Hashrate</div>
+        <div class="value">{{ hashrate|default('n/a') }}</div>
+      </div>
+      <div class="stat-card">
+        <div class="label">Emission</div>
+        <div class="value">n/a</div>
+      </div>
+    </div>
+
+    <div class="block-title mb-lg">
+      <h1>Contract</h1>
+      <span class="block-hash">{{ contract.contract_id }}</span>
+    </div>
+
+    <div class="table-section mb-lg">
+      <div class="table-header">
+        <h3>Contract Info</h3>
+        {% if contract.locked %}
+        <span class="badge badge-locked">Locked</span>
+        {% else %}
+        <span class="badge badge-unlocked">Unlocked</span>
+        {% endif %}
+      </div>
+      <div class="table-wrapper">
+        <table class="info-table">
+          <tbody>
+            <tr>
+              <td class="info-label">Contract ID</td>
+              <td class="hash">{{ contract.contract_id }}</td>
+            </tr>
+            <tr>
+              <td class="info-label">Status</td>
+              <td>
+                {% if contract.locked %}
+                <span class="status-locked">🔒 Locked</span>
+                {% else %}
+                <span class="status-unlocked">🔓 Unlocked</span>
+                {% endif %}
+              </td>
+            </tr>
+            <tr>
+              <td class="info-label">WASM Size</td>
+              <td>{{ contract.wasm_size_formatted }} ({{ contract.wasm_size }} bytes)</td>
+            </tr>
+            <tr>
+              <td class="info-label">Deploy Block</td>
+              <td><a href="/block/{{ contract.deploy_block|int }}">{{ contract.deploy_block|int }}</a></td>
+            </tr>
+            <tr>
+              <td class="info-label">Deploy Transaction</td>
+              <td><a href="/tx/{{ contract.deploy_tx }}" class="hash">{{ contract.deploy_tx }}</a></td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+
+  </main>
+
+  <footer class="footer">
+    <div class="container">
+      <a href="https://dark.fi">Let there be Dark</a> · Code licensed under AGPL-3
+    </div>
+  </footer>
+
+  <style>
+    .badge-locked {
+      background-color: #dc3545;
+      color: white;
+    }
+    .badge-unlocked {
+      background-color: #28a745;
+      color: white;
+    }
+    .status-locked {
+      color: #dc3545;
+      font-weight: bold;
+    }
+    .status-unlocked {
+      color: #28a745;
+      font-weight: bold;
+    }
+  </style>
+</body>
+</html>

+ 107 - 0
bin/explorer/python/templates/contracts.html

@@ -0,0 +1,107 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>DarkFi Blocks - Contracts</title>
+  <link rel="stylesheet" href="/static/layout.css">
+</head>
+<body>
+  <header class="header">
+    <div class="container flex justify-between items-center">
+      <div class="logo">DarkFi {{ network }} Blocks</div>
+      <nav class="nav">
+        <a href="/">Explorer</a>
+        <a href="/contracts" class="active">Contracts</a>
+        <a href="/stats">Stats</a>
+        <a href="https://dark.fi/book/">Docs</a>
+        <a href="https://codeberg.org/darkrenaissance/darkfi/">Code</a>
+      </nav>
+    </div>
+  </header>
+
+  <main class="container section">
+
+    <form action="/search" method="GET" class="input-group mb-lg" style="max-width: 600px;">
+      <input type="search" name="q" class="input" placeholder="Search by block height, hash, transaction, or contract ID...">
+      <button type="submit" class="btn">Search</button>
+    </form>
+
+    <div class="stats-grid mb-lg">
+      <div class="stat-card">
+        <div class="label">Difficulty</div>
+        <div class="value">{{ current_difficulty }}</div>
+      </div>
+      <div class="stat-card">
+        <div class="label">Height</div>
+        <div class="value">{{ current_height }}</div>
+      </div>
+      <div class="stat-card">
+        <div class="label">Hashrate</div>
+        <div class="value">{{ hashrate|default('n/a') }}</div>
+      </div>
+      <div class="stat-card">
+        <div class="label">Contracts</div>
+        <div class="value">{{ contract_count|int }}</div>
+      </div>
+    </div>
+
+    <div class="table-section">
+      <div class="table-header">
+        <h3>Deployed Contracts</h3>
+        <span class="badge">{{ contract_count|int }} Total</span>
+      </div>
+      <div class="table-wrapper">
+        <table>
+          <thead>
+            <tr>
+              <th>Contract ID</th>
+              <th>Status</th>
+              <th>WASM Size</th>
+              <th>Deploy Block</th>
+            </tr>
+          </thead>
+          <tbody>
+            {% for contract in contracts %}
+            <tr>
+              <td><a href="/contract/{{ contract.contract_id }}" class="hash">{{ contract.contract_id }}</a></td>
+              <td>
+                {% if contract.locked %}
+                <span class="status-locked">🔒 Locked</span>
+                {% else %}
+                <span class="status-unlocked">🔓 Unlocked</span>
+                {% endif %}
+              </td>
+              <td>{{ contract.wasm_size_formatted }}</td>
+              <td><a href="/block/{{ contract.deploy_block|int }}">{{ contract.deploy_block|int }}</a></td>
+            </tr>
+            {% else %}
+            <tr>
+              <td colspan="4" style="text-align: center; color: var(--color-muted);">No contracts deployed yet</td>
+            </tr>
+            {% endfor %}
+          </tbody>
+        </table>
+      </div>
+    </div>
+
+  </main>
+
+  <footer class="footer">
+    <div class="container">
+      <a href="https://dark.fi">Let there be Dark</a> · Code licensed under AGPL-3
+    </div>
+  </footer>
+
+  <style>
+    .status-locked {
+      color: #dc3545;
+      font-weight: bold;
+    }
+    .status-unlocked {
+      color: #28a745;
+      font-weight: bold;
+    }
+  </style>
+</body>
+</html>

+ 2 - 1
bin/explorer/python/templates/error.html

@@ -12,7 +12,8 @@
       <div class="logo">>DarkFi {{ network }} Blocks</div>
       <nav class="nav">
         <a href="/">Explorer</a>
-        <a href="#">Stats</a>
+        <a href="/contracts">Contracts</a>
+        <a href="/stats">Stats</a>
         <a href="https://dark.fi/book/">Docs</a>
         <a href="https://codeberg.org/darkrenaissance/darkfi/">Code</a>
       </nav>

+ 2 - 1
bin/explorer/python/templates/index.html

@@ -12,7 +12,8 @@
       <div class="logo">DarkFi {{ network }} Blocks</div>
       <nav class="nav">
         <a href="/" class="active">Explorer</a>
-        <a href="#">Stats</a>
+        <a href="/contracts">Contracts</a>
+        <a href="/stats">Stats</a>
         <a href="https://dark.fi/book/">Docs</a>
         <a href="https://codeberg.org/darkrenaissance/darkfi/">Code</a>
       </nav>

+ 148 - 0
bin/explorer/python/templates/stats.html

@@ -0,0 +1,148 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>DarkFi Blocks - Statistics</title>
+  <link rel="stylesheet" href="/static/layout.css">
+</head>
+<body>
+  <header class="header">
+    <div class="container flex justify-between items-center">
+      <div class="logo">DarkFi {{ network }} Blocks</div>
+      <nav class="nav">
+        <a href="/">Explorer</a>
+        <a href="/contracts">Contracts</a>
+        <a href="/stats" class="active">Stats</a>
+        <a href="https://dark.fi/book/">Docs</a>
+        <a href="https://codeberg.org/darkrenaissance/darkfi/">Code</a>
+      </nav>
+    </div>
+  </header>
+
+  <main class="container section">
+
+    <form action="/search" method="GET" class="input-group mb-lg" style="max-width: 600px;">
+      <input type="search" name="q" class="input" placeholder="Search by block height, hash, transaction, or contract ID...">
+      <button type="submit" class="btn">Search</button>
+    </form>
+
+    <div class="stats-grid mb-lg">
+      <div class="stat-card">
+        <div class="label">Difficulty</div>
+        <div class="value">{{ current_difficulty }}</div>
+      </div>
+      <div class="stat-card">
+        <div class="label">Height</div>
+        <div class="value">{{ current_height }}</div>
+      </div>
+      <div class="stat-card">
+        <div class="label">Hashrate</div>
+        <div class="value">{{ hashrate|default('n/a') }}</div>
+      </div>
+      <div class="stat-card">
+        <div class="label">Emission</div>
+        <div class="value">n/a</div>
+      </div>
+    </div>
+
+    <h1 class="mb-lg">Blockchain Statistics</h1>
+
+    <div class="table-section mb-lg">
+      <div class="table-header">
+        <h3>Daily Average Transactions per Block (Last 90 Days)</h3>
+      </div>
+      <div class="chart-container" style="text-align: center; padding: 1rem;">
+        <img src="/stats/daily_tx_chart.png" alt="Daily Average Transactions Chart" style="max-width: 100%; height: auto;">
+      </div>
+    </div>
+
+    <div class="table-section mb-lg">
+      <div class="table-header">
+        <h3>Transactions per Block (Excluding Coinbase)</h3>
+      </div>
+      <div class="table-wrapper">
+        <table>
+          <thead>
+            <tr>
+              <th>Period</th>
+              <th>Avg TX/Block</th>
+              <th>Empty Blocks %</th>
+              <th>Total Transactions</th>
+              <th>Blocks</th>
+            </tr>
+          </thead>
+          <tbody>
+            <tr>
+              <td>Last Day</td>
+              <td>{{ "%.2f"|format(stats.tx_per_block.last_day.avg_tx) }}</td>
+              <td>{{ "%.1f"|format(stats.tx_per_block.last_day.empty_pct) }}%</td>
+              <td>{{ stats.tx_per_block.last_day.total_tx|int }}</td>
+              <td>{{ stats.tx_per_block.last_day.block_count|int }}</td>
+            </tr>
+            <tr>
+              <td>Last Week</td>
+              <td>{{ "%.2f"|format(stats.tx_per_block.last_week.avg_tx) }}</td>
+              <td>{{ "%.1f"|format(stats.tx_per_block.last_week.empty_pct) }}%</td>
+              <td>{{ stats.tx_per_block.last_week.total_tx|int }}</td>
+              <td>{{ stats.tx_per_block.last_week.block_count|int }}</td>
+            </tr>
+            <tr>
+              <td>Last Month</td>
+              <td>{{ "%.2f"|format(stats.tx_per_block.last_month.avg_tx) }}</td>
+              <td>{{ "%.1f"|format(stats.tx_per_block.last_month.empty_pct) }}%</td>
+              <td>{{ stats.tx_per_block.last_month.total_tx|int }}</td>
+              <td>{{ stats.tx_per_block.last_month.block_count|int }}</td>
+            </tr>
+            <tr>
+              <td>Last Year</td>
+              <td>{{ "%.2f"|format(stats.tx_per_block.last_year.avg_tx) }}</td>
+              <td>{{ "%.1f"|format(stats.tx_per_block.last_year.empty_pct) }}%</td>
+              <td>{{ stats.tx_per_block.last_year.total_tx|int }}</td>
+              <td>{{ stats.tx_per_block.last_year.block_count|int }}</td>
+            </tr>
+          </tbody>
+        </table>
+      </div>
+    </div>
+
+    <div class="table-section mb-lg">
+      <div class="table-header">
+        <h3>Blockchain Growth by Month</h3>
+      </div>
+      <div class="table-wrapper" style="max-height: 400px; overflow-y: auto;">
+        <table>
+          <thead>
+            <tr>
+              <th>Month</th>
+              <th>Monthly Growth (MB)</th>
+              <th>Cumulative Size (MB)</th>
+            </tr>
+          </thead>
+          <tbody>
+            {% for month in stats.monthly_growth|reverse %}
+            <tr>
+              <td>{{ month.year|int }}-{{ "%02d"|format(month.month|int) }}</td>
+              <td>{{ "%.2f"|format(month.size_mb) }}</td>
+              <td>{{ "%.2f"|format(month.cumulative_mb) }}</td>
+            </tr>
+            {% else %}
+            <tr>
+              <td colspan="3" style="text-align: center; color: var(--color-muted);">No data available</td>
+            </tr>
+            {% endfor %}
+          </tbody>
+        </table>
+      </div>
+    </div>
+
+  </main>
+
+  <footer class="footer">
+    <div class="container">
+      <a href="https://dark.fi">Let there be Dark</a> · Code licensed under AGPL-3
+    </div>
+  </footer>
+
+</body>
+</html>

+ 3 - 2
bin/explorer/python/templates/tx.html

@@ -11,8 +11,9 @@
     <div class="container flex justify-between items-center">
       <div class="logo">DarkFi {{ network }} Blocks</div>
       <nav class="nav">
-        <a href="/" class="active">Explorer</a>
-        <a href="#">Stats</a>
+        <a href="/">Explorer</a>
+        <a href="/contracts">Contracts</a>
+        <a href="/stats">Stats</a>
         <a href="https://dark.fi/book/">Docs</a>
         <a href="https://codeberg.org/darkrenaissance/darkfi/">Code</a>
       </nav>