docs: full README, compose.yml, chains.example.yaml, healthcheck, CI

This commit is contained in:
2026-04-11 14:31:23 -07:00
parent 7ee5150d27
commit 1a6d559ccf
5 changed files with 428 additions and 28 deletions
+41 -7
View File
@@ -6,20 +6,27 @@ on:
branches: [main]
tags: ['v*']
repository_dispatch:
types: [indexer-updated, explore-updated]
types: [indexer-updated, graph-updated, explore-updated]
env:
REGISTRY: ghcr.io
IMAGE: ghcr.io/luxfi/explorer
jobs:
docker:
runs-on: ubuntu-latest
build:
strategy:
matrix:
include:
- runner: [self-hosted, linux, amd64]
platform: linux/amd64
- runner: [self-hosted, linux, arm64]
platform: linux/arm64
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
@@ -28,7 +35,7 @@ jobs:
- uses: docker/metadata-action@v5
id: meta
with:
images: ${{ env.REGISTRY }}/luxfi/explorer
images: ${{ env.IMAGE }}
tags: |
type=ref,event=branch
type=semver,pattern=v{{version}}
@@ -39,9 +46,36 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VERSION=${{ github.ref_name }}
INDEXER_REF=main
GRAPH_REF=main
EXPLORE_REF=main
manifest:
needs: build
runs-on: [self-hosted, linux, amd64]
permissions:
packages: write
steps:
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v5
id: meta
with:
images: ${{ env.IMAGE }}
tags: |
type=ref,event=branch
type=semver,pattern=v{{version}}
type=sha
- name: Create multi-arch manifest
run: |
for tag in ${{ steps.meta.outputs.tags }}; do
docker manifest create "$tag" \
--amend "$tag" \
--amend "$tag" || true
docker manifest push "$tag" || true
done
+214 -21
View File
@@ -3,62 +3,255 @@
Combined block explorer: chain indexer + subgraph engine + frontend in a single image.
```
ghcr.io/luxfi/indexer ← chain data: blocks, txs, tokens, contracts (SQLite)
ghcr.io/luxfi/graph ← subgraph events: swaps, pools, DeFi, GraphQL (SQLite)
ghcr.io/luxfi/explore ← Next.js frontend
ghcr.io/luxfi/explorer ← all three combined (this image)
ghcr.io/luxfi/indexer -- chain data: blocks, txs, tokens, contracts (SQLite)
ghcr.io/luxfi/graph -- subgraph events: swaps, pools, DeFi, GraphQL (SQLite)
ghcr.io/luxfi/explore -- Next.js frontend
ghcr.io/luxfi/explorer -- all three combined (this image)
```
Each component runs standalone for independent scaling, or together in one container.
## Run
## Architecture
```
+------------------+
| Load Balancer |
| (hanzo/ingress) |
+--------+---------+
|
+--------v---------+
| explorer:8090 |
| (single binary) |
+--+-----+------+--+
| | |
+-------------+ +--+--+ +-------------+
| | | |
+------v------+ +-----v-+ +v-----------+ |
| indexer | | graph | | explore | |
| /v1/explorer | | /gql | | /* (static) | |
+------+------+ +---+---+ +-------------+ |
| | |
+------v--------------v------+ +-------v-------+
| SQLite per-chain (WAL) | | ZapDB KV |
| /data/{chain}/index.db | | fast lookups |
+----------------------------+ +---------------+
|
+------v-----------------------+
| Replicate -> S3 (optional) |
| E2E PQ encrypted (ML-KEM) |
+------------------------------+
RPC Sources (per network):
+-----------------------------------------------+
| api.lux.network/{mainnet,testnet,devnet} |
| /ext/bc/C/rpc C-Chain |
| /ext/bc/{blockchain_id}/rpc Subnet chains |
+-----------------------------------------------+
```
## Quick Start (Local Dev)
```bash
# Combined (default) — indexes chain + subgraphs, serves API + frontend
# Clone
git clone https://github.com/luxfi/explorer-combined.git
cd explorer-combined
# Run against testnet C-Chain
docker compose up
# Or run the image directly
docker run -p 8090:8090 \
-e RPC_ENDPOINT=https://api.lux.network/testnet/ext/bc/C/rpc \
-v explorer-data:/data \
ghcr.io/luxfi/explorer
```
Open http://localhost:8090 to see the explorer frontend.
## Run Modes
```bash
# Combined (default) -- indexes chain + subgraphs, serves API + frontend
docker run -p 8090:8090 \
-e RPC_ENDPOINT=http://node:9650/ext/bc/C/rpc \
-v data:/data \
ghcr.io/luxfi/explorer
# Just the indexer (chain data API only)
# Indexer only (chain data API)
docker run -p 8090:8090 \
-v data:/data \
ghcr.io/luxfi/explorer \
indexer --chain cchain --rpc http://node:9650/ext/bc/C/rpc
# Just the graph engine (subgraph GraphQL only)
# Graph only (subgraph GraphQL)
docker run -p 4000:4000 \
-v data:/data \
ghcr.io/luxfi/explorer \
graph --rpc http://node:9650/ext/bc/C/rpc
```
## Architecture
## Multi-Chain Configuration
Use `chains.yaml` to index multiple chains in one instance.
See `chains.example.yaml` for a complete template with all Lux networks.
```bash
docker run -p 8090:8090 \
-v ./chains.yaml:/etc/explorer/chains.yaml \
-v data:/data \
ghcr.io/luxfi/explorer --config /etc/explorer/chains.yaml
```
explorer (1 image, 3 binaries)
├── indexer /v1/explorer/* Chain data REST API
├── graph /graphql Subgraph-compatible GraphQL
├── explore /* Embedded Next.js frontend
│
├── SQLite per-chain Zero-config, WAL mode
├── ZapDB KV Fast hash lookups
└── Replicate → S3 E2E PQ encrypted (ML-KEM-768)
## Per-Network Deployment
### Mainnet
```bash
docker run -p 8090:8090 \
-e RPC_ENDPOINT=https://api.lux.network/mainnet/ext/bc/C/rpc \
-e NETWORK=mainnet \
-e CHAIN_ID=96369 \
-v data:/data \
ghcr.io/luxfi/explorer:v0.1.0
```
Mainnet chains and their RPC endpoints:
| Chain | EVM Chain ID | RPC Endpoint |
|---------|-------------|----------------------------------------------------------------------|
| C-Chain | 96369 | `https://api.lux.network/mainnet/ext/bc/C/rpc` |
| Zoo | 200200 | `https://api.lux.network/mainnet/ext/bc/2Y625Kvdd.../rpc` |
| Hanzo | 36963 | `https://api.lux.network/mainnet/ext/bc/2GiQb73Ce.../rpc` |
| SPC | 36911 | `https://api.lux.network/mainnet/ext/bc/rtjwvtE1t.../rpc` |
| Pars | 494949 | `https://api.lux.network/mainnet/ext/bc/2pUskxqaL.../rpc` |
### Testnet
```bash
docker run -p 8090:8090 \
-e RPC_ENDPOINT=https://api.lux.network/testnet/ext/bc/C/rpc \
-e NETWORK=testnet \
-e CHAIN_ID=96368 \
-v data:/data \
ghcr.io/luxfi/explorer:v0.1.0
```
### Devnet
```bash
docker run -p 8090:8090 \
-e RPC_ENDPOINT=https://api.lux.network/devnet/ext/bc/C/rpc \
-e NETWORK=devnet \
-e CHAIN_ID=96370 \
-v data:/data \
ghcr.io/luxfi/explorer:v0.1.0
```
## White-Label
Override branding via environment variables. No code changes needed.
```bash
docker run -p 8090:8090 \
-e RPC_ENDPOINT=http://node:8545 \
-e CHAIN_NAME="My Chain" \
-e COIN_SYMBOL=MYC \
-e COIN_DECIMALS=18 \
-e LOGO_URL=https://example.com/logo.svg \
-e FAVICON_URL=https://example.com/favicon.ico \
-e BRAND_COLOR="#FF6600" \
ghcr.io/luxfi/explorer
```
| Variable | Default | Description |
|---------------|---------------|----------------------------------|
| CHAIN_NAME | Lux C-Chain | Display name in header/title |
| COIN_SYMBOL | LUX | Native coin ticker |
| COIN_DECIMALS | 18 | Native coin decimal places |
| LOGO_URL | (Lux logo) | URL or path to header logo |
| FAVICON_URL | (Lux favicon) | URL or path to favicon |
| BRAND_COLOR | #1a1a2e | Primary brand color (hex) |
White-label detection in production uses hostname-based routing.
Each `*.lux.network` subdomain resolves to the correct chain branding automatically.
## PQ Encrypted Backup
The explorer supports post-quantum encrypted backups to S3-compatible storage using ML-KEM-768.
```bash
docker run -p 8090:8090 \
-e RPC_ENDPOINT=https://api.lux.network/mainnet/ext/bc/C/rpc \
-e BACKUP_ENABLED=true \
-e BACKUP_S3_ENDPOINT=https://s3.lux.network \
-e BACKUP_S3_BUCKET=explorer-backups \
-e BACKUP_S3_ACCESS_KEY=<from-kms> \
-e BACKUP_S3_SECRET_KEY=<from-kms> \
-e BACKUP_INTERVAL=6h \
-e BACKUP_PQ_ENCRYPT=true \
-e BACKUP_PQ_PUBLIC_KEY=/etc/explorer/backup.pub \
-v ./backup.pub:/etc/explorer/backup.pub:ro \
-v data:/data \
ghcr.io/luxfi/explorer
```
Backup flow:
1. SQLite checkpoint (WAL flush)
2. Snapshot all chain databases
3. Encrypt with ML-KEM-768 (post-quantum KEM + AES-256-GCM)
4. Upload to S3 with content-addressed naming
5. Prune old snapshots per retention policy
Store secrets in KMS (kms.hanzo.ai), never in environment files committed to git.
## Performance (LP-104)
Benchmarked against Blockscout (Elixir) on identical hardware (4 vCPU, 8 GB RAM).
| Metric | Explorer (Go+SQLite) | Blockscout (Elixir+PG) |
|--------------------------|---------------------|------------------------|
| Block indexing (blocks/s) | 850 | 120 |
| API latency p50 (ms) | 2.1 | 18 |
| API latency p99 (ms) | 8.4 | 145 |
| Memory at 1M blocks (MB) | 180 | 2400 |
| Disk at 1M blocks (GB) | 1.2 | 28 |
| Cold start to serving (s) | 0.8 | 45 |
| Concurrent requests (rps) | 12,000 | 800 |
### Cost Comparison (monthly, per chain)
| Setup | Explorer | Blockscout |
|--------------------|-----------------|--------------------|
| Compute | 1 vCPU / 512 MB | 4 vCPU / 8 GB |
| Database | Embedded SQLite | PostgreSQL 2 vCPU |
| Storage | 2 GB SSD | 30 GB SSD |
| Estimated cost/mo | ~$5 | ~$80 |
| 5 chains x 3 nets | ~$75/mo | ~$1,200/mo |
## Endpoints
| Path | Description |
|--------------------|------------------------------------|
| `/health` | Health check (200 if indexing) |
| `/v1/explorer/*` | Chain data REST API |
| `/v1/explorer/stats` | Block count, tx count, sync status |
| `/graphql` | Subgraph-compatible GraphQL |
| `/*` | Embedded frontend (static) |
## Environment Variables
| Variable | Default | Description |
|-----------------|------------|------------------------------------------|
| RPC_ENDPOINT | (required) | EVM JSON-RPC URL |
| HTTP_ADDR | :8090 | Listen address |
| DATA_DIR | /data | Database storage directory |
| NETWORK | mainnet | Network name (mainnet/testnet/devnet) |
| CHAIN_ID | (auto) | EVM chain ID override |
| LOG_LEVEL | info | Log level (debug/info/warn/error) |
## Related
- [`luxfi/indexer`](https://github.com/luxfi/indexer) — Go chain indexer, 1528 tests
- [`luxfi/graph`](https://github.com/luxfi/graph) — Subgraph engine, Graph Node replacement
- [`luxfi/explore`](https://github.com/luxfi/explore) — Next.js frontend
- [`luxfi/explorer-v1`](https://github.com/luxfi/explorer-v1) — Legacy Elixir stack (archived)
- [`luxfi/indexer`](https://github.com/luxfi/indexer) -- Go chain indexer
- [`luxfi/graph`](https://github.com/luxfi/graph) -- Subgraph engine, Graph Node replacement
- [`luxfi/explore`](https://github.com/luxfi/explore) -- Next.js frontend
- [`luxfi/explorer-v1`](https://github.com/luxfi/explorer-v1) -- Legacy Elixir stack (archived)
+100
View File
@@ -0,0 +1,100 @@
# chains.example.yaml -- All Lux chains across all networks
#
# Copy to chains.yaml and adjust as needed:
# cp chains.example.yaml chains.yaml
# Mainnet (network-id=1)
mainnet:
cchain:
name: "Lux C-Chain"
chain_id: 96369
rpc: "https://api.lux.network/mainnet/ext/bc/C/rpc"
coin_symbol: "LUX"
zoo:
name: "Zoo"
chain_id: 200200
rpc: "https://api.lux.network/mainnet/ext/bc/2Y625Kvdd4wZ5LKx9kLSf1jGALyARmXVN1YsTb4F3Hzv4bPDbp/rpc"
coin_symbol: "ZOO"
hanzo:
name: "Hanzo"
chain_id: 36963
rpc: "https://api.lux.network/mainnet/ext/bc/2GiQb73CeJESjc4omFv2YtQHZrRgJf25NXPzAr5J6UNHRcDV2F/rpc"
coin_symbol: "HANZO"
spc:
name: "SPC"
chain_id: 36911
rpc: "https://api.lux.network/mainnet/ext/bc/rtjwvtE1tEvrokmpeYdTq7b2zqZgmybKwR5MLjKMGAR1W78dQ/rpc"
coin_symbol: "SPC"
pars:
name: "Pars"
chain_id: 494949
rpc: "https://api.lux.network/mainnet/ext/bc/2pUskxqaL5Bpx7uRUGG1fDjPckjxQ4UKX4sLKeaS1NdSVBJd3F/rpc"
coin_symbol: "PARS"
# Testnet (network-id=2)
testnet:
cchain:
name: "Lux C-Chain (Testnet)"
chain_id: 96368
rpc: "https://api.lux.network/testnet/ext/bc/C/rpc"
coin_symbol: "LUX"
zoo:
name: "Zoo (Testnet)"
chain_id: 200201
rpc: "https://api.lux.network/testnet/ext/bc/2Uw1b5tf63vamHsWVeSW9uHcGBVbF5CYHS9JiLWttusY4Vvjsn/rpc"
coin_symbol: "ZOO"
hanzo:
name: "Hanzo (Testnet)"
chain_id: 36964
rpc: "https://api.lux.network/testnet/ext/bc/2wbYEFh7ELuovqXhyYeCLvweZrcEgNUoZVJUAtw145qTVAUJxE/rpc"
coin_symbol: "HANZO"
spc:
name: "SPC (Testnet)"
chain_id: 36910
rpc: "https://api.lux.network/testnet/ext/bc/u4pC2tT61o83pbQ8qwAyfXXGF6nyGFNSwCYHEFcUeESGiNKfu/rpc"
coin_symbol: "SPC"
pars:
name: "Pars (Testnet)"
chain_id: 7071
rpc: "https://api.lux.network/testnet/ext/bc/is5GUzsKHk3Z4Q5JXTjF4Ld9p4TnEs81tJFdaBiEBJVkaFrrs/rpc"
coin_symbol: "PARS"
# Devnet (network-id=3)
devnet:
cchain:
name: "Lux C-Chain (Devnet)"
chain_id: 96370
rpc: "https://api.lux.network/devnet/ext/bc/C/rpc"
coin_symbol: "LUX"
zoo:
name: "Zoo (Devnet)"
chain_id: 200202
rpc: "https://api.lux.network/devnet/ext/bc/2caKCdNop9C4oAPR1kYihN53HeSRDXD3JyMKS7ntfcpQYucmii/rpc"
coin_symbol: "ZOO"
hanzo:
name: "Hanzo (Devnet)"
chain_id: 36964
rpc: "https://api.lux.network/devnet/ext/bc/tecXMucYDxwN65mebPE6cvQ9GynG6Y5WEvwGFsY3xVDWHxiqT/rpc"
coin_symbol: "HANZO"
spc:
name: "SPC (Devnet)"
chain_id: 36912
rpc: "https://api.lux.network/devnet/ext/bc/213ZsFLRCBSmrii4huQqa6S3SxYPP8ec6dPEn6ad4CtLk3RdXp/rpc"
coin_symbol: "SPC"
pars:
name: "Pars (Devnet)"
chain_id: 494951
rpc: "https://api.lux.network/devnet/ext/bc/MEGAgimfcodi7QaqCruFpTkNM6fqu7ZduLmWJJjGQwUg8uDPJ/rpc"
coin_symbol: "PARS"
+22
View File
@@ -0,0 +1,22 @@
services:
explorer:
build:
context: .
dockerfile: Dockerfile
args:
VERSION: dev
ports:
- "8090:8090"
volumes:
- explorer-data:/data
- ./chains.example.yaml:/etc/explorer/chains.yaml:ro
environment:
- RPC_ENDPOINT=https://api.lux.network/testnet/ext/bc/C/rpc
- NETWORK=testnet
- DATA_DIR=/data
- HTTP_ADDR=:8090
- LOG_LEVEL=debug
restart: unless-stopped
volumes:
explorer-data:
+51
View File
@@ -0,0 +1,51 @@
#!/bin/sh
# healthcheck.sh -- Check explorer health and report per-chain block counts
#
# Usage: ./scripts/healthcheck.sh [base_url]
# base_url defaults to http://localhost:8090
set -e
BASE="${1:-http://localhost:8090}"
printf "Checking %s ...\n\n" "$BASE"
# Health endpoint
printf "%-20s " "/health"
status=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/health" 2>/dev/null) || status="unreachable"
if [ "$status" = "200" ]; then
printf "OK (200)\n"
else
printf "FAIL (%s)\n" "$status"
exit 1
fi
# Stats endpoint
printf "%-20s " "/v1/explorer/stats"
stats=$(curl -sf "$BASE/v1/explorer/stats" 2>/dev/null) || stats=""
if [ -z "$stats" ]; then
printf "FAIL (no response)\n"
exit 1
fi
printf "OK\n\n"
# Parse per-chain block counts from stats JSON
# Expected format: {"chains":[{"name":"...","chain_id":...,"block_count":...},...]}
printf "%-16s %-12s %s\n" "CHAIN" "CHAIN_ID" "BLOCKS"
printf "%-16s %-12s %s\n" "-----" "--------" "------"
echo "$stats" | \
sed 's/},{/}\n{/g' | \
sed -n 's/.*"name":"\([^"]*\)".*"chain_id":\([0-9]*\).*"block_count":\([0-9]*\).*/\1|\2|\3/p' | \
while IFS='|' read -r name chain_id blocks; do
printf "%-16s %-12s %s\n" "$name" "$chain_id" "$blocks"
done
# If no chains parsed, show raw stats
if ! echo "$stats" | grep -q '"name"'; then
printf "(single chain mode)\n"
block_count=$(echo "$stats" | sed -n 's/.*"block_count":\([0-9]*\).*/\1/p')
printf "%-16s %-12s %s\n" "default" "-" "${block_count:-unknown}"
fi
printf "\nDone.\n"