Compare commits
7 Commits
ed5a34438a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 48785992d2 | |||
| 6c75fe55d0 | |||
| 742b0662a7 | |||
| ae91163168 | |||
| 525d1bc9e0 | |||
| 26b69c6b55 | |||
| 8e0aaecaa9 |
3
.env.example
Normal file
3
.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
# Dashboard auth for external clients (non-RFC1918 source IPs)
|
||||
DASHBOARD_AUTH_USERNAME=admin
|
||||
DASHBOARD_AUTH_PASSWORD=change-me-now
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,3 +1,9 @@
|
||||
# SSL certificates (auto-generated or user-provided)
|
||||
/certs/
|
||||
|
||||
# Environment file
|
||||
.env
|
||||
|
||||
# ElectrumX server data
|
||||
/electrumx-data/
|
||||
|
||||
@@ -37,3 +43,4 @@ __pycache__/
|
||||
/daemon/palladium-cli
|
||||
/daemon/palladium-tx
|
||||
/daemon/palladium-wallet
|
||||
/daemon/palladium-qt
|
||||
|
||||
14
DASHBOARD.md
14
DASHBOARD.md
@@ -154,8 +154,14 @@ curl http://localhost:8080/api/health | jq
|
||||
|
||||
## Security Note
|
||||
|
||||
The dashboard is exposed on `0.0.0.0:8080` making it accessible from your network. If you're running this on a public server, consider:
|
||||
The dashboard is exposed on `0.0.0.0:8080`.
|
||||
Requests from localhost/LAN private ranges are allowed directly.
|
||||
Requests from public/external IPs require HTTP Basic Auth.
|
||||
|
||||
1. Using a reverse proxy (nginx) with authentication
|
||||
2. Restricting access with firewall rules
|
||||
3. Using HTTPS with SSL certificates
|
||||
Set credentials with:
|
||||
|
||||
```bash
|
||||
# .env (copy from .env.example)
|
||||
DASHBOARD_AUTH_USERNAME=admin
|
||||
DASHBOARD_AUTH_PASSWORD=change-me-now
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM lukechilds/electrumx
|
||||
|
||||
# Install curl (needed by entrypoint for RPC calls and IP detection)
|
||||
RUN apk add --no-cache curl || apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
|
||||
RUN apk add --no-cache curl openssl || apt-get update && apt-get install -y --no-install-recommends curl openssl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy Palladium coin definition and patch ElectrumX
|
||||
COPY electrumx-patch/coins_plm.py /tmp/coins_plm.py
|
||||
@@ -28,34 +28,6 @@ for target in [
|
||||
print('>> Patched ElectrumX with Palladium coin classes')
|
||||
PATCH
|
||||
|
||||
RUN mkdir -p /certs && \
|
||||
cat >/certs/openssl.cnf <<'EOF' && \
|
||||
openssl req -x509 -nodes -newkey rsa:4096 -days 3650 \
|
||||
-keyout /certs/server.key -out /certs/server.crt \
|
||||
-config /certs/openssl.cnf && \
|
||||
chmod 600 /certs/server.key && chmod 644 /certs/server.crt
|
||||
[req]
|
||||
distinguished_name = dn
|
||||
x509_extensions = v3_req
|
||||
prompt = no
|
||||
|
||||
[dn]
|
||||
C = IT
|
||||
ST = -
|
||||
L = -
|
||||
O = ElectrumX
|
||||
CN = plm.local
|
||||
|
||||
[v3_req]
|
||||
keyUsage = keyEncipherment, dataEncipherment, digitalSignature
|
||||
extendedKeyUsage = serverAuth
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[alt_names]
|
||||
DNS.1 = plm.local
|
||||
IP.1 = 127.0.0.1
|
||||
EOF
|
||||
|
||||
ENV SSL_CERTFILE=/certs/server.crt
|
||||
ENV SSL_KEYFILE=/certs/server.key
|
||||
|
||||
|
||||
133
README.md
133
README.md
@@ -8,12 +8,13 @@ Everything runs in Docker containers - no need to install dependencies on your h
|
||||
|
||||
## What You Get
|
||||
|
||||
- **Palladium Full Node** (palladiumd) - Runs in Docker with full blockchain sync
|
||||
- **Palladium Full Node** (palladiumd) - Runs in Docker with full blockchain sync ([binary setup](daemon/README.md))
|
||||
- **ElectrumX Server** - Pre-configured for Palladium network with automatic indexing
|
||||
- **Web Dashboard** - Professional monitoring interface with real-time statistics, peer views, and Electrum server discovery
|
||||
- **Web Dashboard** - Professional monitoring interface with real-time statistics, peer views, and Electrum server discovery ([quick start](DASHBOARD.md) | [technical docs](web-dashboard/README.md))
|
||||
- **Automatic RPC Configuration** - ElectrumX reads credentials directly from palladium.conf
|
||||
- **Self-Signed SSL Certificates** - Secure connections ready out-of-the-box
|
||||
- **Production Ready** - Includes restart policies and dashboard health endpoint
|
||||
- **Self-Signed SSL Certificates** - Auto-generated on first startup, persisted in `./certs/`
|
||||
- **Public IP Auto-Detection** - Automatically configures REPORT_SERVICES and SSL certificate SAN
|
||||
- **Production Ready** - Includes restart policies, health endpoint, and Basic Auth for external dashboard access
|
||||
|
||||
---
|
||||
|
||||
@@ -40,21 +41,32 @@ palladium-stack/
|
||||
│ ├── palladiumd # Node daemon (required)
|
||||
│ ├── palladium-cli # CLI tool (required)
|
||||
│ ├── palladium-tx # Transaction tool (optional)
|
||||
│ └── palladium-wallet # Wallet tool (optional)
|
||||
│ ├── palladium-wallet # Wallet tool (optional)
|
||||
│ └── README.md # Binary download instructions
|
||||
├── .palladium/
|
||||
│ ├── palladium.conf # Node configuration (edit this!)
|
||||
│ ├── blocks/ # Blockchain blocks (auto-generated)
|
||||
│ ├── chainstate/ # Blockchain state (auto-generated)
|
||||
│ └── ... # Other runtime data (auto-generated)
|
||||
├── certs/ # SSL certificates (auto-generated on first run)
|
||||
│ ├── server.crt # Self-signed certificate
|
||||
│ └── server.key # Private key
|
||||
├── electrumx-data/ # ElectrumX database (auto-generated)
|
||||
├── electrumx-patch/
|
||||
│ └── coins_plm.py # Palladium coin definition for ElectrumX
|
||||
├── web-dashboard/ # Web monitoring dashboard
|
||||
│ ├── app.py # Flask backend API
|
||||
│ ├── templates/ # HTML templates
|
||||
│ └── static/ # CSS and JavaScript
|
||||
│ ├── static/ # CSS and JavaScript
|
||||
│ └── README.md # Dashboard technical docs
|
||||
├── Dockerfile.palladium-node # Builds Palladium node container
|
||||
├── Dockerfile.electrumx # Builds ElectrumX server container
|
||||
├── Dockerfile.dashboard # Builds web dashboard container
|
||||
└── docker-compose.yml # Main orchestration file
|
||||
├── docker-compose.yml # Main orchestration file
|
||||
├── entrypoint.sh # ElectrumX startup (auto-config, SSL, IP detection)
|
||||
├── test-server.py # ElectrumX protocol test client
|
||||
├── .env.example # Environment variables template
|
||||
└── DASHBOARD.md # Dashboard quick start guide
|
||||
```
|
||||
|
||||
**Palladium Full Node:** [palladium-coin/palladiumcore](https://github.com/palladium-coin/palladiumcore)
|
||||
@@ -126,37 +138,15 @@ cd palladium-stack
|
||||
|
||||
### Step 2: Get Palladium Binaries
|
||||
|
||||
**IMPORTANT:** Download binaries matching your system architecture.
|
||||
**IMPORTANT:** Download binaries matching your system architecture in `daemon/`.
|
||||
|
||||
#### Option A: Download from Official Release
|
||||
|
||||
1. Go to: [palladium-coin/palladiumcore/releases](https://github.com/palladium-coin/palladiumcore/releases)
|
||||
|
||||
2. Download the correct version:
|
||||
- **Linux x64**: `palladium-x.x.x-x86_64-linux-gnu.tar.gz`
|
||||
- **Linux ARM64**: `palladium-x.x.x-aarch64-linux-gnu.tar.gz`
|
||||
|
||||
3. Extract and copy binaries:
|
||||
```bash
|
||||
tar -xzf palladium-*.tar.gz
|
||||
mkdir -p daemon
|
||||
cp palladium-*/bin/palladiumd daemon/
|
||||
cp palladium-*/bin/palladium-cli daemon/
|
||||
chmod +x daemon/*
|
||||
```
|
||||
|
||||
#### Verify Installation
|
||||
|
||||
```bash
|
||||
ls -lh daemon/
|
||||
# Should show: palladiumd, palladium-cli (both executable)
|
||||
```
|
||||
See [daemon/README.md](daemon/README.md) for detailed instructions.
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Configure Network and Router
|
||||
|
||||
#### A. Configure RPC Credentials
|
||||
#### 3.1 Configure RPC Credentials
|
||||
|
||||
Open the configuration file:
|
||||
|
||||
@@ -172,7 +162,7 @@ rpcpassword=your_password # ← Use a strong password!
|
||||
|
||||
Save and close (`Ctrl+X`, then `Y`, then `Enter`).
|
||||
|
||||
#### B. Router Port Forwarding (Required for Public Access)
|
||||
#### 3.2 Router Port Forwarding (Required for Public Access)
|
||||
|
||||
For your ElectrumX server to be accessible from the internet, you **must** configure port forwarding on your router.
|
||||
|
||||
@@ -242,11 +232,31 @@ For your ElectrumX server to be accessible from the internet, you **must** confi
|
||||
**Security Notes:**
|
||||
- Only forward port **8080** if you want the dashboard accessible from internet (not recommended without authentication)
|
||||
- Consider using a VPN for dashboard access instead
|
||||
- External dashboard clients (public IPs) require Basic Auth. Configure `DASHBOARD_AUTH_USERNAME` and `DASHBOARD_AUTH_PASSWORD` in `.env` (see `.env.example`).
|
||||
- Ports **50001** and **50002** need to be public for Electrum wallets to connect
|
||||
- Port **2333** is required for the node to sync with the Palladium network
|
||||
|
||||
---
|
||||
|
||||
#### 3.3: (Optional) Configure Dashboard Authentication
|
||||
|
||||
If you plan to expose the dashboard to the internet (port 8080), configure Basic Auth credentials:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
nano .env
|
||||
```
|
||||
|
||||
Set strong credentials:
|
||||
```bash
|
||||
DASHBOARD_AUTH_USERNAME=admin
|
||||
DASHBOARD_AUTH_PASSWORD=a-strong-random-password
|
||||
```
|
||||
|
||||
LAN clients (private IPs) can access the dashboard without authentication. External clients (public IPs) will be prompted for these credentials automatically.
|
||||
|
||||
---
|
||||
|
||||
### Step 4: (Optional) Copy Existing Blockchain Data
|
||||
|
||||
If you have a synced Palladium blockchain, copy it to speed up initial sync:
|
||||
@@ -270,7 +280,7 @@ docker compose up -d
|
||||
**What happens:**
|
||||
1. Builds three Docker images: `palladium-node`, `electrumx-server`, and `palladium-dashboard`
|
||||
2. Starts Palladium node first
|
||||
3. Starts ElectrumX (waits for node to be ready)
|
||||
3. Starts ElectrumX (waits for node to be ready, auto-generates SSL certificates in `./certs/` if not present)
|
||||
4. Starts Web Dashboard (connects to both services)
|
||||
|
||||
**First build takes 5-10 minutes.**
|
||||
@@ -328,6 +338,8 @@ The dashboard shows:
|
||||
|
||||
## Web Dashboard Features
|
||||
|
||||
See also: [DASHBOARD.md](DASHBOARD.md) for quick start | [web-dashboard/README.md](web-dashboard/README.md) for technical details
|
||||
|
||||
### Main Dashboard (http://localhost:8080)
|
||||
|
||||
**System Monitoring:**
|
||||
@@ -389,6 +401,10 @@ The dashboard shows:
|
||||
- Host
|
||||
- TCP Port
|
||||
- SSL Port
|
||||
- TCP Reachable (Yes/No)
|
||||
- SSL Reachable (Yes/No)
|
||||
|
||||
Servers are filtered by genesis hash to show only peers on the same network (mainnet or testnet).
|
||||
|
||||
**Auto-refresh:** Every 10 seconds
|
||||
|
||||
@@ -430,6 +446,25 @@ curl http://<your-public-ip>:8080
|
||||
python test-server.py <your-public-ip>:50002
|
||||
```
|
||||
|
||||
### REST API Endpoints
|
||||
|
||||
The dashboard exposes a REST API for programmatic access:
|
||||
|
||||
| Endpoint | Description |
|
||||
|----------|-------------|
|
||||
| `GET /api/health` | Service health check (palladium + electrumx status) |
|
||||
| `GET /api/system/resources` | CPU, memory, and disk usage |
|
||||
| `GET /api/palladium/info` | Node info (blockchain, network, mining, mempool) |
|
||||
| `GET /api/palladium/peers` | Detailed peer list with traffic stats |
|
||||
| `GET /api/palladium/blocks/recent` | Last 10 blocks |
|
||||
| `GET /api/electrumx/stats` | ElectrumX version, uptime, DB size, active servers |
|
||||
| `GET /api/electrumx/servers` | Discovered ElectrumX peers with reachability |
|
||||
|
||||
```bash
|
||||
# Example
|
||||
curl http://localhost:8080/api/health | jq
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Details
|
||||
@@ -454,8 +489,6 @@ Key settings in `.palladium/palladium.conf`:
|
||||
| `port=2333` | Default | P2P network port (mainnet) |
|
||||
| `rpcport=2332` | Default | RPC port (mainnet) |
|
||||
|
||||
**Important:** current `docker-compose.yml` starts `palladiumd` with command-line `-rpcallowip=0.0.0.0/0`, which overrides `rpcallowip` values in `palladium.conf`. Keep this in mind for security hardening.
|
||||
|
||||
**ZeroMQ Ports (optional):**
|
||||
- `28332` - Block hash notifications
|
||||
- `28333` - Transaction hash notifications
|
||||
@@ -474,10 +507,12 @@ environment:
|
||||
# RPC credentials automatically read from palladium.conf
|
||||
```
|
||||
|
||||
**Automatic Configuration:**
|
||||
- ElectrumX reads RPC credentials from mounted `palladium.conf`
|
||||
- No need to manually configure `DAEMON_URL`
|
||||
- Single source of truth for credentials
|
||||
**Automatic Configuration (via `entrypoint.sh`):**
|
||||
- **RPC credentials**: Read automatically from mounted `palladium.conf` — no need to configure `DAEMON_URL`
|
||||
- **Public IP detection**: Discovers your public IP and sets `REPORT_SERVICES` for peer announcement
|
||||
- **SSL certificates**: Auto-generated on first startup in `./certs/` with SAN including localhost and public IP (see [Security > SSL Certificates](#production-deployment))
|
||||
- **TX stats patching**: Queries the live node for `TX_COUNT` / `TX_COUNT_HEIGHT` and patches the ElectrumX coin definition at startup
|
||||
- Single source of truth for credentials across all services
|
||||
|
||||
---
|
||||
|
||||
@@ -679,13 +714,19 @@ docker compose build --no-cache
|
||||
```
|
||||
|
||||
3. **SSL Certificates:**
|
||||
- Default uses self-signed certificates
|
||||
- For production, use valid SSL certificates (Let's Encrypt)
|
||||
- Self-signed certificates are auto-generated on first startup in `./certs/`
|
||||
- The certificate includes localhost and the auto-detected public IP in its SAN
|
||||
- To use your own certificates (e.g. Let's Encrypt), place `server.crt` and `server.key` in `./certs/` before starting
|
||||
|
||||
4. **Dashboard Access:**
|
||||
- Consider adding authentication
|
||||
- Use VPN for remote access
|
||||
- Or restrict to local network only
|
||||
- LAN clients (RFC1918 private IPs) can access without authentication
|
||||
- External clients (public IPs) require HTTP Basic Auth automatically
|
||||
- Configure credentials in `.env` (copy from `.env.example`):
|
||||
```bash
|
||||
DASHBOARD_AUTH_USERNAME=admin
|
||||
DASHBOARD_AUTH_PASSWORD=a-strong-random-password
|
||||
```
|
||||
- Consider using a VPN instead of exposing port 8080 publicly
|
||||
|
||||
5. **Regular Updates:**
|
||||
```bash
|
||||
@@ -743,7 +784,7 @@ environment:
|
||||
|
||||
## Notes
|
||||
|
||||
* **Data Persistence:** All data stored in `./.palladium/` and `./electrumx-data/`
|
||||
* **Data Persistence:** All data stored in `./.palladium/`, `./electrumx-data/`, and `./certs/`
|
||||
* **Backup:** Regularly backup `.palladium/wallet.dat` if you store funds
|
||||
* **Network Switch:** Always clear ElectrumX database when switching networks
|
||||
* **Updates:** Check for Palladium Core updates regularly
|
||||
|
||||
@@ -4,9 +4,7 @@ This directory must contain the pre-compiled Palladium Core binaries used by the
|
||||
|
||||
## Download
|
||||
|
||||
Download the latest release from the official repository:
|
||||
|
||||
**https://github.com/palladium-coin/palladiumcore/releases**
|
||||
Download from the official release repository: **https://github.com/palladium-coin/palladiumcore/releases/latest**
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -15,8 +13,8 @@ Choose the correct archive for your platform:
|
||||
|
||||
| Host Architecture | Archive to download | Common hardware |
|
||||
|-------------------|-------------------------|-------------------------------------|
|
||||
| `x86_64` | `x86_64-linux-gnu.tar.gz` | Standard PCs, most VPS/cloud servers |
|
||||
| `aarch64` | `aarch64-linux-gnu.tar.gz` | Single-board computers (Raspberry Pi 4/5, Orange Pi, etc.) |
|
||||
| `x86_64` | `palladium-linux-x86_64.tar.gz` | Standard PCs, most VPS/cloud servers |
|
||||
| `aarch64` | `palladium-linux-aarch64.tar.gz` | Single-board computers (Raspberry Pi 4/5, Orange Pi, etc.) |
|
||||
|
||||
To check your host architecture:
|
||||
|
||||
@@ -38,14 +36,28 @@ daemon/
|
||||
|
||||
## Quick setup
|
||||
|
||||
```bash
|
||||
# Example for aarch64 (Raspberry Pi)
|
||||
tar xzf palladiumcore-*-aarch64-linux-gnu.tar.gz
|
||||
cp palladiumcore-*/bin/palladium{d,-cli,-tx,-wallet} daemon/
|
||||
### Example for x86_64 (VPS/PC)
|
||||
|
||||
# Example for x86_64 (VPS/PC)
|
||||
tar xzf palladiumcore-*-x86_64-linux-gnu.tar.gz
|
||||
cp palladiumcore-*/bin/palladium{d,-cli,-tx,-wallet} daemon/
|
||||
```bash
|
||||
cd daemon
|
||||
wget https://github.com/palladium-coin/palladiumcore/releases/latest/download/palladium-linux-x86_64.tar.gz
|
||||
tar -xzf palladium-linux-x86_64.tar.gz
|
||||
cd linux-x86_64
|
||||
mv palladium* ..
|
||||
cd ..
|
||||
rm -rf linux-x86_64/ && rm palladium-linux-x86_64.tar.gz
|
||||
```
|
||||
|
||||
### Example for aarch64 (Raspberry Pi)
|
||||
|
||||
```bash
|
||||
cd daemon
|
||||
wget https://github.com/palladium-coin/palladiumcore/releases/latest/download/palladium-linux-aarch64.tar.gz
|
||||
tar -xzf palladium-linux-aarch64.tar.gz
|
||||
cd linux-aarch64
|
||||
mv palladium* ..
|
||||
cd ..
|
||||
rm -rf linux-aarch64/ && rm palladium-linux-aarch64.tar.gz
|
||||
```
|
||||
|
||||
After placing the binaries, rebuild the node image:
|
||||
|
||||
Binary file not shown.
@@ -7,11 +7,11 @@ services:
|
||||
container_name: palladium-node
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "0.0.0.0:2332:2332" # RPC port mainnet (accessible from network)
|
||||
- "127.0.0.1:2332:2332" # RPC port mainnet (host-local only)
|
||||
- "0.0.0.0:2333:2333" # P2P port mainnet (accessible from network)
|
||||
- "0.0.0.0:28332:28332" # ZMQ hashblock (accessible from network)
|
||||
- "0.0.0.0:28334:28334" # ZMQ rawblock (accessible from network)
|
||||
- "0.0.0.0:28335:28335" # ZMQ rawtx (accessible from network)
|
||||
- "127.0.0.1:28332:28332" # ZMQ hashblock (host-local debug only)
|
||||
- "127.0.0.1:28334:28334" # ZMQ rawblock (host-local debug only)
|
||||
- "127.0.0.1:28335:28335" # ZMQ rawtx (host-local debug only)
|
||||
# For testnet, expose: 12332 (RPC) and 12333 (P2P)
|
||||
|
||||
volumes:
|
||||
@@ -22,8 +22,6 @@ services:
|
||||
palladiumd
|
||||
-conf=/root/.palladium/palladium.conf
|
||||
-datadir=/root/.palladium
|
||||
-rpcbind=0.0.0.0
|
||||
-rpcallowip=0.0.0.0/0
|
||||
-daemon=0
|
||||
-printtoconsole=1
|
||||
|
||||
@@ -74,6 +72,7 @@ services:
|
||||
volumes:
|
||||
- ./electrumx-data:/data
|
||||
- ./.palladium/palladium.conf:/palladium-config/palladium.conf:ro
|
||||
- ./certs:/certs
|
||||
|
||||
dashboard:
|
||||
build:
|
||||
@@ -93,7 +92,9 @@ services:
|
||||
PALLADIUM_RPC_PORT: "2332"
|
||||
ELECTRUMX_RPC_HOST: "electrumx"
|
||||
ELECTRUMX_RPC_PORT: "8000"
|
||||
DASHBOARD_AUTH_USERNAME: "${DASHBOARD_AUTH_USERNAME:-admin}"
|
||||
DASHBOARD_AUTH_PASSWORD: "${DASHBOARD_AUTH_PASSWORD:-change-me-now}"
|
||||
|
||||
volumes:
|
||||
- ./.palladium/palladium.conf:/palladium-config/palladium.conf:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
|
||||
@@ -70,6 +70,61 @@ echo "DAEMON_URL: http://${RPC_USER}:***@palladiumd:${RPC_PORT}/"
|
||||
echo "REPORT_SERVICES: ${REPORT_SERVICES:-not set}"
|
||||
echo "=========================================="
|
||||
|
||||
# ── SSL certificate generation (skip if certs already exist) ──
|
||||
if [ ! -f /certs/server.crt ] || [ ! -f /certs/server.key ]; then
|
||||
echo "SSL certificates not found, generating self-signed certificate..."
|
||||
|
||||
# Collect SAN entries
|
||||
SAN="DNS.1 = localhost"
|
||||
SAN_IDX=1
|
||||
IP_IDX=1
|
||||
SAN="${SAN}\nIP.${IP_IDX} = 127.0.0.1"
|
||||
|
||||
# Try to detect public IP for SAN
|
||||
for url in https://icanhazip.com https://ifconfig.me https://api.ipify.org; do
|
||||
DETECTED_IP=$(curl -sf --max-time 5 "$url" 2>/dev/null | tr -d '[:space:]')
|
||||
if [ -n "$DETECTED_IP" ]; then
|
||||
IP_IDX=$((IP_IDX + 1))
|
||||
SAN="${SAN}\nIP.${IP_IDX} = ${DETECTED_IP}"
|
||||
echo ">> Including public IP in certificate SAN: ${DETECTED_IP}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
cat >/tmp/openssl.cnf <<SSLEOF
|
||||
[req]
|
||||
distinguished_name = dn
|
||||
x509_extensions = v3_req
|
||||
prompt = no
|
||||
|
||||
[dn]
|
||||
C = IT
|
||||
ST = -
|
||||
L = -
|
||||
O = PalladiumStack
|
||||
CN = localhost
|
||||
|
||||
[v3_req]
|
||||
keyUsage = keyEncipherment, dataEncipherment, digitalSignature
|
||||
extendedKeyUsage = serverAuth
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[alt_names]
|
||||
$(echo -e "$SAN")
|
||||
SSLEOF
|
||||
|
||||
openssl req -x509 -nodes -newkey rsa:4096 -days 3650 \
|
||||
-keyout /certs/server.key -out /certs/server.crt \
|
||||
-config /tmp/openssl.cnf 2>/dev/null
|
||||
|
||||
chmod 600 /certs/server.key
|
||||
chmod 644 /certs/server.crt
|
||||
rm -f /tmp/openssl.cnf
|
||||
echo ">> SSL certificate generated successfully"
|
||||
else
|
||||
echo ">> Using existing SSL certificates from /certs/"
|
||||
fi
|
||||
|
||||
# Update TX_COUNT / TX_COUNT_HEIGHT in coins.py from the live node
|
||||
echo "Fetching chain tx stats from palladiumd..."
|
||||
TX_STATS=$(curl -sf --user "${RPC_USER}:${RPC_PASSWORD}" \
|
||||
|
||||
@@ -11,6 +11,10 @@ import os
|
||||
import time
|
||||
import copy
|
||||
import threading
|
||||
import ssl
|
||||
import ipaddress
|
||||
import base64
|
||||
import hmac
|
||||
from datetime import datetime
|
||||
import psutil
|
||||
import socket
|
||||
@@ -18,6 +22,76 @@ import socket
|
||||
app = Flask(__name__)
|
||||
CORS(app)
|
||||
|
||||
TRUSTED_CLIENT_NETWORKS = (
|
||||
ipaddress.ip_network('127.0.0.0/8'),
|
||||
ipaddress.ip_network('10.0.0.0/8'),
|
||||
ipaddress.ip_network('172.16.0.0/12'),
|
||||
ipaddress.ip_network('192.168.0.0/16'),
|
||||
ipaddress.ip_network('::1/128'),
|
||||
)
|
||||
|
||||
|
||||
def is_trusted_client_ip(ip_text):
|
||||
"""Allow direct access for localhost and private RFC1918 LAN clients."""
|
||||
if not ip_text:
|
||||
return False
|
||||
try:
|
||||
ip_obj = ipaddress.ip_address(ip_text.strip())
|
||||
if getattr(ip_obj, 'ipv4_mapped', None):
|
||||
ip_obj = ip_obj.ipv4_mapped
|
||||
return any(ip_obj in network for network in TRUSTED_CLIENT_NETWORKS)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def unauthorized_response():
|
||||
response = jsonify({'error': 'Authentication required'})
|
||||
response.status_code = 401
|
||||
response.headers['WWW-Authenticate'] = 'Basic realm="Palladium Dashboard"'
|
||||
return response
|
||||
|
||||
|
||||
@app.before_request
|
||||
def enforce_external_auth():
|
||||
"""
|
||||
Localhost and LAN clients can access directly.
|
||||
External clients must authenticate via Basic Auth credentials from env vars.
|
||||
"""
|
||||
client_ip = request.remote_addr or ''
|
||||
if is_trusted_client_ip(client_ip):
|
||||
return None
|
||||
|
||||
expected_user = os.getenv('DASHBOARD_AUTH_USERNAME', '').strip()
|
||||
expected_pass = os.getenv('DASHBOARD_AUTH_PASSWORD', '').strip()
|
||||
if not expected_user or not expected_pass:
|
||||
return jsonify({'error': 'Dashboard auth is not configured'}), 503
|
||||
|
||||
auth_header = request.headers.get('Authorization', '')
|
||||
if not auth_header.startswith('Basic '):
|
||||
return unauthorized_response()
|
||||
|
||||
try:
|
||||
decoded = base64.b64decode(auth_header.split(' ', 1)[1]).decode('utf-8')
|
||||
username, password = decoded.split(':', 1)
|
||||
except Exception:
|
||||
return unauthorized_response()
|
||||
|
||||
user_ok = hmac.compare_digest(username, expected_user)
|
||||
pass_ok = hmac.compare_digest(password, expected_pass)
|
||||
if not (user_ok and pass_ok):
|
||||
return unauthorized_response()
|
||||
return None
|
||||
|
||||
|
||||
@app.after_request
|
||||
def disable_api_cache(response):
|
||||
"""Prevent stale API payloads from browser/proxy caches."""
|
||||
if request.path.startswith('/api/'):
|
||||
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
||||
response.headers['Pragma'] = 'no-cache'
|
||||
response.headers['Expires'] = '0'
|
||||
return response
|
||||
|
||||
# Configuration
|
||||
PALLADIUM_RPC_HOST = os.getenv('PALLADIUM_RPC_HOST', 'palladiumd')
|
||||
PALLADIUM_RPC_PORT = int(os.getenv('PALLADIUM_RPC_PORT', '2332'))
|
||||
@@ -64,7 +138,49 @@ def parse_addnode_hosts(conf_path='/palladium-config/palladium.conf'):
|
||||
return hosts
|
||||
|
||||
|
||||
def probe_electrum_server(host, port=50001, timeout=1.2):
|
||||
def parse_services_ports(services):
|
||||
"""Extract TCP/SSL ports from SERVICES string (e.g. tcp://0.0.0.0:50001,ssl://0.0.0.0:50002)."""
|
||||
tcp_port = None
|
||||
ssl_port = None
|
||||
for item in services.split(','):
|
||||
item = item.strip()
|
||||
if item.startswith('tcp://') and ':' in item:
|
||||
try:
|
||||
tcp_port = int(item.rsplit(':', 1)[1])
|
||||
except ValueError:
|
||||
pass
|
||||
if item.startswith('ssl://') and ':' in item:
|
||||
try:
|
||||
ssl_port = int(item.rsplit(':', 1)[1])
|
||||
except ValueError:
|
||||
pass
|
||||
return tcp_port, ssl_port
|
||||
|
||||
|
||||
def get_electrumx_service_ports():
|
||||
"""
|
||||
Resolve ElectrumX service ports dynamically.
|
||||
Priority:
|
||||
1) SERVICES env from electrumx container
|
||||
2) local SERVICES env (if provided)
|
||||
"""
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
['docker', 'exec', 'electrumx-server', 'sh', '-c', 'printf "%s" "${SERVICES:-}"'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=2
|
||||
)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return parse_services_ports(result.stdout.strip())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return parse_services_ports(os.getenv('SERVICES', ''))
|
||||
|
||||
|
||||
def probe_electrum_server(host, port, timeout=2.0):
|
||||
"""Check if an Electrum server is reachable and speaking protocol on host:port"""
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
@@ -76,23 +192,148 @@ def probe_electrum_server(host, port=50001, timeout=1.2):
|
||||
"method": "server.version",
|
||||
"params": ["palladium-dashboard", "1.4"]
|
||||
}
|
||||
sock.send((json.dumps(request) + '\n').encode())
|
||||
response = sock.recv(4096).decode()
|
||||
sock.sendall((json.dumps(request) + '\n').encode())
|
||||
|
||||
data = b""
|
||||
for _ in range(6):
|
||||
chunk = sock.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
data += chunk
|
||||
candidate = data.decode(errors='ignore')
|
||||
candidate = candidate.split("\n", 1)[0].strip()
|
||||
if candidate:
|
||||
try:
|
||||
payload = json.loads(candidate)
|
||||
sock.close()
|
||||
return 'result' in payload
|
||||
except Exception:
|
||||
pass
|
||||
sock.close()
|
||||
data = json.loads(response)
|
||||
if 'result' in data:
|
||||
line = data.split(b"\n", 1)[0].decode(errors='ignore').strip() if data else ""
|
||||
payload = json.loads(line) if line else {}
|
||||
if 'result' in payload:
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def probe_electrum_server_ssl(host, port, timeout=2.0):
|
||||
"""Check if an Electrum SSL server is reachable on host:port (self-signed allowed)."""
|
||||
try:
|
||||
raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
raw_sock.settimeout(timeout)
|
||||
raw_sock.connect((host, port))
|
||||
|
||||
context = ssl.create_default_context()
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
ssl_sock = context.wrap_socket(raw_sock, server_hostname=host)
|
||||
ssl_sock.settimeout(timeout)
|
||||
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 101,
|
||||
"method": "server.version",
|
||||
"params": ["palladium-dashboard", "1.4"]
|
||||
}
|
||||
ssl_sock.sendall((json.dumps(request) + '\n').encode())
|
||||
data = b""
|
||||
for _ in range(6):
|
||||
chunk = ssl_sock.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
data += chunk
|
||||
candidate = data.decode(errors='ignore')
|
||||
candidate = candidate.split("\n", 1)[0].strip()
|
||||
if candidate:
|
||||
try:
|
||||
payload = json.loads(candidate)
|
||||
ssl_sock.close()
|
||||
return 'result' in payload
|
||||
except Exception:
|
||||
pass
|
||||
ssl_sock.close()
|
||||
line = data.split(b"\n", 1)[0].decode(errors='ignore').strip() if data else ""
|
||||
payload = json.loads(line) if line else {}
|
||||
if 'result' in payload:
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def get_electrum_server_genesis(host, tcp_port=None, ssl_port=None, timeout=2.0):
|
||||
"""Return peer genesis_hash via server.features, trying TCP first then SSL."""
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 102,
|
||||
"method": "server.features",
|
||||
"params": []
|
||||
}
|
||||
|
||||
if tcp_port:
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(timeout)
|
||||
sock.connect((host, tcp_port))
|
||||
sock.sendall((json.dumps(request) + '\n').encode())
|
||||
data = b""
|
||||
for _ in range(6):
|
||||
chunk = sock.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
data += chunk
|
||||
candidate = data.decode(errors='ignore').split("\n", 1)[0].strip()
|
||||
if candidate:
|
||||
payload = json.loads(candidate)
|
||||
genesis = (payload.get('result') or {}).get('genesis_hash')
|
||||
sock.close()
|
||||
return genesis
|
||||
sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if ssl_port:
|
||||
try:
|
||||
raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
raw_sock.settimeout(timeout)
|
||||
raw_sock.connect((host, ssl_port))
|
||||
context = ssl.create_default_context()
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
ssl_sock = context.wrap_socket(raw_sock, server_hostname=host)
|
||||
ssl_sock.settimeout(timeout)
|
||||
ssl_sock.sendall((json.dumps(request) + '\n').encode())
|
||||
data = b""
|
||||
for _ in range(6):
|
||||
chunk = ssl_sock.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
data += chunk
|
||||
candidate = data.decode(errors='ignore').split("\n", 1)[0].strip()
|
||||
if candidate:
|
||||
payload = json.loads(candidate)
|
||||
genesis = (payload.get('result') or {}).get('genesis_hash')
|
||||
ssl_sock.close()
|
||||
return genesis
|
||||
ssl_sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def is_electrumx_reachable(timeout=1.0):
|
||||
"""Fast ElectrumX liveness check used by /api/health"""
|
||||
tcp_port, _ = get_electrumx_service_ports()
|
||||
if not tcp_port:
|
||||
return False
|
||||
try:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(timeout)
|
||||
sock.connect((ELECTRUMX_RPC_HOST, 50001))
|
||||
sock.connect((ELECTRUMX_RPC_HOST, tcp_port))
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 999,
|
||||
@@ -209,11 +450,14 @@ def get_electrumx_stats(include_addnode_probes=False):
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
|
||||
local_tcp_port, local_ssl_port = get_electrumx_service_ports()
|
||||
|
||||
stats = {
|
||||
'server_version': 'Unknown',
|
||||
'protocol_min': '',
|
||||
'protocol_max': '',
|
||||
'genesis_hash': '',
|
||||
'genesis_hash_full': '',
|
||||
'hash_function': '',
|
||||
'pruning': None,
|
||||
'sessions': 0,
|
||||
@@ -225,8 +469,8 @@ def get_electrumx_stats(include_addnode_probes=False):
|
||||
'subs': 0,
|
||||
'uptime': 0,
|
||||
'db_size': 0,
|
||||
'tcp_port': 50001,
|
||||
'ssl_port': 50002,
|
||||
'tcp_port': str(local_tcp_port) if local_tcp_port else None,
|
||||
'ssl_port': str(local_ssl_port) if local_ssl_port else None,
|
||||
'server_ip': 'Unknown'
|
||||
}
|
||||
|
||||
@@ -252,9 +496,11 @@ def get_electrumx_stats(include_addnode_probes=False):
|
||||
|
||||
# Get server features via Electrum protocol
|
||||
try:
|
||||
if not local_tcp_port:
|
||||
raise RuntimeError("SERVICES tcp port not configured")
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(5)
|
||||
sock.connect((ELECTRUMX_RPC_HOST, 50001))
|
||||
sock.connect((ELECTRUMX_RPC_HOST, local_tcp_port))
|
||||
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
@@ -270,20 +516,35 @@ def get_electrumx_stats(include_addnode_probes=False):
|
||||
data = json.loads(response)
|
||||
if 'result' in data:
|
||||
result = data['result']
|
||||
full_genesis = result.get('genesis_hash', '')
|
||||
stats['server_version'] = result.get('server_version', 'Unknown')
|
||||
stats['protocol_min'] = result.get('protocol_min', '')
|
||||
stats['protocol_max'] = result.get('protocol_max', '')
|
||||
stats['genesis_hash'] = result.get('genesis_hash', '')[:16] + '...'
|
||||
stats['genesis_hash_full'] = full_genesis
|
||||
stats['genesis_hash'] = (full_genesis[:16] + '...') if full_genesis else ''
|
||||
stats['hash_function'] = result.get('hash_function', '')
|
||||
stats['pruning'] = result.get('pruning')
|
||||
except Exception as e:
|
||||
print(f"ElectrumX protocol error: {e}")
|
||||
|
||||
# Fallback: derive expected network genesis from local Palladium node
|
||||
# if Electrum server.features is temporarily unavailable.
|
||||
if not stats.get('genesis_hash_full'):
|
||||
try:
|
||||
local_genesis = palladium_rpc_call('getblockhash', [0])
|
||||
if isinstance(local_genesis, str) and local_genesis:
|
||||
stats['genesis_hash_full'] = local_genesis
|
||||
stats['genesis_hash'] = local_genesis[:16] + '...'
|
||||
except Exception as e:
|
||||
print(f"Local genesis fallback error: {e}")
|
||||
|
||||
# Get peers discovered by ElectrumX
|
||||
try:
|
||||
if not local_tcp_port:
|
||||
raise RuntimeError("SERVICES tcp port not configured")
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(5)
|
||||
sock.connect((ELECTRUMX_RPC_HOST, 50001))
|
||||
sock.connect((ELECTRUMX_RPC_HOST, local_tcp_port))
|
||||
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
@@ -315,7 +576,9 @@ def get_electrumx_stats(include_addnode_probes=False):
|
||||
peers.append({
|
||||
'host': host,
|
||||
'tcp_port': tcp_port,
|
||||
'ssl_port': ssl_port
|
||||
'ssl_port': ssl_port,
|
||||
'tcp_reachable': None,
|
||||
'ssl_reachable': None
|
||||
})
|
||||
|
||||
stats['active_servers'] = peers
|
||||
@@ -325,25 +588,35 @@ def get_electrumx_stats(include_addnode_probes=False):
|
||||
|
||||
# Keep peers list without self for dashboard card count
|
||||
try:
|
||||
merged = []
|
||||
seen = set()
|
||||
merged_by_host = {}
|
||||
self_host = (stats.get('server_ip') or '').strip()
|
||||
for peer in (stats.get('active_servers') or []):
|
||||
host = (peer.get('host') or '').strip()
|
||||
tcp_port = str(peer.get('tcp_port') or '50001')
|
||||
tcp_port = str(peer.get('tcp_port')) if peer.get('tcp_port') else None
|
||||
ssl_port = str(peer.get('ssl_port')) if peer.get('ssl_port') else None
|
||||
if not host:
|
||||
continue
|
||||
if self_host and host == self_host:
|
||||
continue
|
||||
key = f"{host}:{tcp_port}"
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
merged.append({
|
||||
'host': host,
|
||||
'tcp_port': tcp_port,
|
||||
'ssl_port': peer.get('ssl_port')
|
||||
})
|
||||
existing = merged_by_host.get(host)
|
||||
if not existing:
|
||||
merged_by_host[host] = {
|
||||
'host': host,
|
||||
'tcp_port': tcp_port,
|
||||
'ssl_port': ssl_port,
|
||||
'tcp_reachable': peer.get('tcp_reachable'),
|
||||
'ssl_reachable': peer.get('ssl_reachable')
|
||||
}
|
||||
else:
|
||||
if not existing.get('tcp_port') and tcp_port:
|
||||
existing['tcp_port'] = tcp_port
|
||||
if not existing.get('ssl_port') and ssl_port:
|
||||
existing['ssl_port'] = ssl_port
|
||||
if existing.get('tcp_reachable') is None and peer.get('tcp_reachable') is not None:
|
||||
existing['tcp_reachable'] = peer.get('tcp_reachable')
|
||||
if existing.get('ssl_reachable') is None and peer.get('ssl_reachable') is not None:
|
||||
existing['ssl_reachable'] = peer.get('ssl_reachable')
|
||||
merged = list(merged_by_host.values())
|
||||
stats['active_servers'] = merged
|
||||
stats['active_servers_count'] = len(merged)
|
||||
except Exception as e:
|
||||
@@ -355,32 +628,85 @@ def get_electrumx_stats(include_addnode_probes=False):
|
||||
addnode_hosts = parse_addnode_hosts()
|
||||
extra_servers = []
|
||||
for host in addnode_hosts:
|
||||
if probe_electrum_server(host, 50001, timeout=0.5):
|
||||
tcp_ok = probe_electrum_server(host, local_tcp_port, timeout=2.0) if local_tcp_port else False
|
||||
ssl_ok = probe_electrum_server_ssl(host, local_ssl_port, timeout=2.0) if local_ssl_port else False
|
||||
if tcp_ok or ssl_ok:
|
||||
extra_servers.append({
|
||||
'host': host,
|
||||
'tcp_port': '50001',
|
||||
'ssl_port': None
|
||||
'tcp_port': str(local_tcp_port) if tcp_ok and local_tcp_port else None,
|
||||
'ssl_port': str(local_ssl_port) if ssl_ok and local_ssl_port else None,
|
||||
'tcp_reachable': tcp_ok,
|
||||
'ssl_reachable': ssl_ok
|
||||
})
|
||||
|
||||
merged = []
|
||||
seen = set()
|
||||
merged_by_host = {}
|
||||
self_host = (stats.get('server_ip') or '').strip()
|
||||
for peer in (stats.get('active_servers') or []) + extra_servers:
|
||||
host = (peer.get('host') or '').strip()
|
||||
tcp_port = str(peer.get('tcp_port') or '50001')
|
||||
tcp_port = str(peer.get('tcp_port')) if peer.get('tcp_port') else None
|
||||
ssl_port = str(peer.get('ssl_port')) if peer.get('ssl_port') else None
|
||||
if not host:
|
||||
continue
|
||||
if self_host and host == self_host:
|
||||
continue
|
||||
key = f"{host}:{tcp_port}"
|
||||
if key in seen:
|
||||
existing = merged_by_host.get(host)
|
||||
if not existing:
|
||||
merged_by_host[host] = {
|
||||
'host': host,
|
||||
'tcp_port': tcp_port,
|
||||
'ssl_port': ssl_port,
|
||||
'tcp_reachable': peer.get('tcp_reachable'),
|
||||
'ssl_reachable': peer.get('ssl_reachable')
|
||||
}
|
||||
else:
|
||||
if not existing.get('tcp_port') and tcp_port:
|
||||
existing['tcp_port'] = tcp_port
|
||||
if not existing.get('ssl_port') and ssl_port:
|
||||
existing['ssl_port'] = ssl_port
|
||||
if existing.get('tcp_reachable') is None and peer.get('tcp_reachable') is not None:
|
||||
existing['tcp_reachable'] = peer.get('tcp_reachable')
|
||||
if existing.get('ssl_reachable') is None and peer.get('ssl_reachable') is not None:
|
||||
existing['ssl_reachable'] = peer.get('ssl_reachable')
|
||||
merged = list(merged_by_host.values())
|
||||
|
||||
# Probe merged list so summary can report both TCP and SSL reachability
|
||||
for peer in merged:
|
||||
host = peer.get('host')
|
||||
if not host:
|
||||
continue
|
||||
seen.add(key)
|
||||
merged.append({
|
||||
'host': host,
|
||||
'tcp_port': tcp_port,
|
||||
'ssl_port': peer.get('ssl_port')
|
||||
})
|
||||
peer_tcp_port = int(peer.get('tcp_port')) if str(peer.get('tcp_port', '')).isdigit() else local_tcp_port
|
||||
peer_ssl_port = int(peer.get('ssl_port')) if str(peer.get('ssl_port', '')).isdigit() else local_ssl_port
|
||||
|
||||
if peer.get('tcp_reachable') is None and peer_tcp_port:
|
||||
peer['tcp_reachable'] = probe_electrum_server(host, peer_tcp_port, timeout=2.0)
|
||||
if peer.get('ssl_reachable') is None and peer_ssl_port:
|
||||
peer['ssl_reachable'] = probe_electrum_server_ssl(host, peer_ssl_port, timeout=2.0)
|
||||
if peer.get('tcp_reachable') is True and not peer.get('tcp_port') and peer_tcp_port:
|
||||
peer['tcp_port'] = str(peer_tcp_port)
|
||||
if peer.get('ssl_reachable') is True and not peer.get('ssl_port') and peer_ssl_port:
|
||||
peer['ssl_port'] = str(peer_ssl_port)
|
||||
|
||||
# Keep only peers matching local Electrum network (same genesis hash).
|
||||
expected_genesis = (stats.get('genesis_hash_full') or '').strip().lower()
|
||||
if expected_genesis:
|
||||
filtered = []
|
||||
for peer in merged:
|
||||
host = peer.get('host')
|
||||
if not host:
|
||||
continue
|
||||
if peer.get('tcp_reachable') is not True and peer.get('ssl_reachable') is not True:
|
||||
continue
|
||||
peer_tcp_port = int(peer.get('tcp_port')) if str(peer.get('tcp_port', '')).isdigit() else None
|
||||
peer_ssl_port = int(peer.get('ssl_port')) if str(peer.get('ssl_port', '')).isdigit() else None
|
||||
peer_genesis = get_electrum_server_genesis(
|
||||
host,
|
||||
tcp_port=peer_tcp_port,
|
||||
ssl_port=peer_ssl_port,
|
||||
timeout=2.0
|
||||
)
|
||||
if peer_genesis and peer_genesis.strip().lower() == expected_genesis:
|
||||
filtered.append(peer)
|
||||
merged = filtered
|
||||
|
||||
stats['active_servers'] = merged
|
||||
stats['active_servers_count'] = len(merged)
|
||||
@@ -439,9 +765,11 @@ def get_electrumx_stats(include_addnode_probes=False):
|
||||
|
||||
# Count active connections (TCP sessions)
|
||||
try:
|
||||
if not local_tcp_port:
|
||||
raise RuntimeError("SERVICES tcp port not configured")
|
||||
result = subprocess.run(
|
||||
['docker', 'exec', 'electrumx-server', 'sh', '-c',
|
||||
'netstat -an 2>/dev/null | grep ":50001.*ESTABLISHED" | wc -l'],
|
||||
f'netstat -an 2>/dev/null | grep ":{local_tcp_port}.*ESTABLISHED" | wc -l'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=2
|
||||
@@ -558,12 +886,12 @@ def electrumx_stats():
|
||||
try:
|
||||
stats = get_electrumx_stats_cached(include_addnode_probes=False)
|
||||
if stats:
|
||||
# If fast path reports no servers, reuse full servers cache if available.
|
||||
if (stats.get('active_servers_count') or 0) == 0:
|
||||
heavy_stats = get_electrumx_stats_cached(include_addnode_probes=True)
|
||||
if heavy_stats and (heavy_stats.get('active_servers_count') or 0) > 0:
|
||||
stats['active_servers'] = heavy_stats.get('active_servers', [])
|
||||
stats['active_servers_count'] = heavy_stats.get('active_servers_count', 0)
|
||||
# Keep dashboard card aligned with /api/electrumx/servers:
|
||||
# always prefer the full discovery view for active servers.
|
||||
heavy_stats = get_electrumx_stats_cached(include_addnode_probes=True)
|
||||
if heavy_stats:
|
||||
stats['active_servers'] = heavy_stats.get('active_servers', [])
|
||||
stats['active_servers_count'] = heavy_stats.get('active_servers_count', 0)
|
||||
|
||||
# Get additional info from logs if available
|
||||
try:
|
||||
@@ -601,11 +929,6 @@ def electrumx_servers():
|
||||
return jsonify({'error': 'Cannot connect to ElectrumX'}), 500
|
||||
|
||||
servers = stats.get('active_servers') or []
|
||||
if len(servers) == 0:
|
||||
# Fallback to fast discovery results if full probing is temporarily empty.
|
||||
fast_stats = get_electrumx_stats_cached(include_addnode_probes=False)
|
||||
if fast_stats:
|
||||
servers = fast_stats.get('active_servers') or []
|
||||
return jsonify({
|
||||
'servers': servers,
|
||||
'total': len(servers),
|
||||
|
||||
@@ -217,10 +217,10 @@ async function updateElectrumXStats() {
|
||||
document.getElementById('serverIP').textContent = data.stats.server_ip || '--';
|
||||
|
||||
// TCP Port
|
||||
document.getElementById('tcpPort').textContent = data.stats.tcp_port || 50001;
|
||||
document.getElementById('tcpPort').textContent = data.stats.tcp_port || '--';
|
||||
|
||||
// SSL Port
|
||||
document.getElementById('sslPort').textContent = data.stats.ssl_port || 50002;
|
||||
document.getElementById('sslPort').textContent = data.stats.ssl_port || '--';
|
||||
|
||||
// Active servers from peer discovery
|
||||
const activeServers = Array.isArray(data.stats.active_servers) ? data.stats.active_servers : [];
|
||||
|
||||
@@ -20,9 +20,8 @@ async function updateElectrumServers() {
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (servers.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="3" class="loading">No active servers found</td></tr>';
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="loading">No active servers found</td></tr>';
|
||||
document.getElementById('totalServers').textContent = '0';
|
||||
document.getElementById('tcpReachable').textContent = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -32,17 +31,17 @@ async function updateElectrumServers() {
|
||||
<td class="peer-addr">${server.host || '--'}</td>
|
||||
<td>${server.tcp_port || '--'}</td>
|
||||
<td>${server.ssl_port || '--'}</td>
|
||||
<td>${server.tcp_reachable === true ? 'Yes' : 'No'}</td>
|
||||
<td>${server.ssl_reachable === true ? 'Yes' : 'No'}</td>
|
||||
`;
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
document.getElementById('totalServers').textContent = String(servers.length);
|
||||
const tcpCount = servers.filter(s => !!s.tcp_port).length;
|
||||
document.getElementById('tcpReachable').textContent = String(tcpCount);
|
||||
} catch (error) {
|
||||
console.error('Error fetching Electrum servers:', error);
|
||||
document.getElementById('electrumServersTable').innerHTML =
|
||||
'<tr><td colspan="3" class="loading">Error loading servers</td></tr>';
|
||||
'<tr><td colspan="5" class="loading">Error loading servers</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,10 +33,6 @@
|
||||
<div class="stat-label">Total Active Servers</div>
|
||||
<div class="stat-value" id="totalServers">--</div>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">TCP 50001 Reachable</div>
|
||||
<div class="stat-value" id="tcpReachable">--</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,10 +50,12 @@
|
||||
<th>Host</th>
|
||||
<th>TCP Port</th>
|
||||
<th>SSL Port</th>
|
||||
<th>TCP Reachable</th>
|
||||
<th>SSL Reachable</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="electrumServersTable">
|
||||
<tr><td colspan="3" class="loading">Loading servers...</td></tr>
|
||||
<tr><td colspan="5" class="loading">Loading servers...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -70,6 +68,6 @@
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', filename='electrum_servers.js') }}?v=1"></script>
|
||||
<script src="{{ url_for('static', filename='electrum_servers.js') }}?v=3"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user