Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions node/explorer_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""
RustChain Block Explorer Routes
Serves the real-time WebSocket block explorer at /explorer
"""
import os
from flask import Blueprint, send_from_directory, jsonify, request

explorer_bp = Blueprint('explorer', __name__)

# Path to the site/explorer directory (relative to node/ dir → ../../site/explorer)
NODE_DIR = os.path.dirname(os.path.abspath(__file__))
SITE_EXPLORER_DIR = os.path.normpath(os.path.join(NODE_DIR, '..', 'site', 'explorer'))


@explorer_bp.route('/explorer')
def explorer_index():
"""Serve the main explorer page."""
return send_from_directory(SITE_EXPLORER_DIR, 'index.html')


@explorer_bp.route('/explorer/<path:filename>')
def explorer_static(filename):
"""Serve static assets for the explorer (JS, CSS, images)."""
return send_from_directory(SITE_EXPLORER_DIR, filename)


@explorer_bp.route('/api/explorer/status')
def explorer_status():
"""Return the explorer WebSocket endpoint info."""
# Determine WS URL based on request
scheme = 'wss' if request.is_secure else 'ws'
host = request.host.split(':')[0]
ws_port = os.environ.get('EXPLORER_WS_PORT', '5001')
ws_url = os.environ.get('EXPLORER_WS_URL', f'{scheme}://{host}:{ws_port}')

return jsonify({
'status': 'ok',
'explorer_version': '1.0.0',
'ws_endpoint': f'{ws_url}/ws/feed',
'events': [
'new_block - emitted when a new slot/block is detected',
'attestation - emitted when a miner submits an attestation',
'epoch_settlement - emitted when epoch advances',
],
'site_explorer_dir': SITE_EXPLORER_DIR,
})


def register_explorer_routes(app):
"""Register the explorer blueprint with the Flask app."""
app.register_blueprint(explorer_bp)
print("[EXPLORER] Block explorer routes registered at /explorer")
print(f"[EXPLORER] Serving static files from: {SITE_EXPLORER_DIR}")
29 changes: 29 additions & 0 deletions node/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,35 @@
except Exception as e:
print(f"[RIP-306] SophiaCore init failed: {e}")

# RustChain Block Explorer Routes
try:
from explorer_routes import register_explorer_routes
register_explorer_routes(app)
print("[EXPLORER] Block explorer available at /explorer")
except ImportError as e:
print(f"[EXPLORER] explorer_routes not available: {e}")
except Exception as e:
print(f"[EXPLORER] Explorer routes failed: {e}")

# WebSocket Feed (Socket.IO real-time events)
# Can run standalone or integrated. If running standalone on port 5001,
# the explorer connects directly. If integrated, we try to init here.
_websocket_integrated = False
try:
# Try to import websocket_feed and integrate it
sys.path.insert(0, os.path.join(base_dir, '..'))
from websocket_feed import ws_bp, socketio, start_event_poller, HAVE_SOCKETIO
if HAVE_SOCKETIO:
socketio.init_app(app, cors_allowed_origins="*", async_mode="threading")
app.register_blueprint(ws_bp)
start_event_poller()
_websocket_integrated = True
print("[WS-FEED] Socket.IO WebSocket feed integrated at /ws/feed")
except ImportError as e:
print(f"[WS-FEED] websocket_feed not available: {e}")
except Exception as e:
print(f"[WS-FEED] WebSocket feed integration failed: {e}")

# Expose the app for gunicorn
application = app

Expand Down
125 changes: 125 additions & 0 deletions site/explorer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# RustChain Block Explorer — Real-Time WebSocket Feed

Live block explorer for the RustChain blockchain with real-time WebSocket updates.

**Bounty:** #2295 — RustChain Block Explorer: Real-Time WebSocket Feed (75 RTC)
**Wallet:** `C4c7r9WPsnEe6CUfegMU9M7ReHD1pWg8qeSfTBoRcLbg`

## Features

- **Real-time block feed** — new blocks appear instantly as they are mined
- **Live attestation stream** — miner attestations stream in real-time
- **Epoch settlement notifications** — visual alert when epoch advances
- **Connection status indicator** — shows connected/reconnecting/offline state
- **Auto-reconnect** — exponential backoff reconnection on disconnect
- **Activity sparkline** — visualizes event rate over time
- **Raw event log** — full event stream for debugging

## Architecture

```
Browser (explorer.js)
|
| WebSocket / Socket.IO
v
RustChain Node (wsgi.py)
|
+-- websocket_feed.py (Socket.IO /ws/feed)
| |
| +-- EventBus (thread-safe pub/sub)
| |
| +-- Poller thread (polls /epoch, /api/miners every 5s)
|
+-- explorer_routes.py (/explorer/*)
|
+-- site/explorer/ (static HTML/CSS/JS)
```

## Endpoints

| Endpoint | Description |
|---|---|
| `GET /explorer` | Main explorer page |
| `GET /explorer/<file>` | Static assets (JS, CSS) |
| `WS /ws/feed` | Socket.IO WebSocket feed |
| `GET /ws/feed/status` | Feed connection status |
| `GET /api/explorer/status` | Explorer configuration info |

## WebSocket Events

| Event | Description | Payload |
|---|---|---|
| `new_block` | New slot/block detected | `{slot, epoch, timestamp}` |
| `attestation` | Miner attestation received | `{miner, arch, multiplier, timestamp}` |
| `epoch_settlement` | Epoch advanced | `{epoch, new_epoch, total_rtc, miners}` |

## Running

### As part of the node (integrated):

```bash
# The explorer is auto-registered when running wsgi.py
gunicorn -w 4 -b 0.0.0.0:8099 wsgi:app --timeout 120

# Open in browser:
# http://localhost:8099/explorer
```

### Standalone WebSocket feed:

```bash
# Run just the WebSocket feed server
cd Rustchain
python websocket_feed.py --port 5001 --node https://your-node-url

# Connect the explorer to a different WS endpoint:
# http://localhost:8099/explorer?ws=ws://localhost:5001
```

### Nginx Configuration

Add to your nginx server block:

```nginx
# Explorer UI
location /explorer/ {
proxy_pass http://127.0.0.1:8099/explorer/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}

# WebSocket feed
location /ws/feed {
proxy_pass http://127.0.0.1:8099/ws/feed;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
```

## Development

### File Structure

```
site/explorer/
index.html — Main explorer page
explorer.js — Vanilla JS WebSocket client
styles.css — Explorer styling

node/
explorer_routes.py — Flask blueprint serving /explorer
wsgi.py — Registers explorer + websocket_feed

websocket_feed.py — Socket.IO real-time event feed (root of repo)
```

## Tech Stack

- **Frontend:** Vanilla JS, Socket.IO client, Canvas sparkline
- **Backend:** Python Flask + Flask-SocketIO + threading
- **WebSocket:** Socket.IO protocol over pure WebSocket
- **Styling:** Custom CSS (dark terminal theme)
Loading
Loading