1.5.12 stopped a block with no time from anchoring the span at 1970, which fixed Hanzo — 58,370 seconds per block became 10.3, its real cadence. It did nothing for Zoo, which still read 2,387 for a chain building one every twelve: Zoo's stray row is not timeless, it carries a plausible timestamp dated 1.76 years before Zoo's own first block, and no filter on the VALUE can tell that from a legitimately old chain. Nothing about the value distinguishes them. The ORDER does. Timestamps are monotonic in block order by consensus rule, so the chain's elapsed time is the last block's time minus the first block's — by number. On a sound index that is identical to MIN and MAX of the column; on a damaged one it is the answer and MIN/MAX is not. Zoo's first block is three days old, so three days is what its span should have been all along. This subsumes 1.5.12: a stray row only moves the answer now if it is an endpoint, and a timeless one is still skipped because a block that does not say when it happened cannot bound how far apart blocks are. The suite covers both shapes at once, numbered mid-run the way a damaged row actually sits: epoch 0, and two years early. The old query reports 1.26e7 seconds per block on the second. Co-authored-by: Hanzo Dev <[email protected]>
Explorer
Single-binary Lux explorer: chain indexer, GraphQL engine, and SPA frontend
in one Go process. Imports luxfi/indexer
and luxfi/graph as libraries; embeds the
luxfi/explore Next.js build via
go:embed.
ghcr.io/luxfi/explorer
-> indexes one or more EVM chains (luxfi/indexer)
-> exposes per-chain GraphQL (luxfi/graph)
-> serves embedded SPA + REST API (luxfi/explore)
Routes
| Path | Purpose |
|---|---|
/ |
SPA (embedded; SPA-routing fallback) |
/envs.js |
Runtime config window.ENV = {...} |
/icon.svg, /logo.svg |
Per-host brand assets (disk override) |
/health |
Service health |
/v1/indexer/* |
Default chain explorer API |
/v1/indexer/{slug}/* |
Per-chain explorer API |
/v1/explorer/{slug}/* |
Legacy alias for /v1/indexer/{slug} |
/v1/graph/{slug}/{subgraph}/graphql |
Per-chain, per-subgraph GraphQL |
/v1/explorer/admin/chains[/{slug}] |
Runtime registry CRUD |
/v1/explorer/realtime |
WebSocket realtime hub |
/v1/explorer/realtime/stats |
Realtime stats |
Quick Start
docker run -p 8090:8090 \
-v $(pwd)/chains.example.yaml:/etc/explorer/chains.yaml:ro \
-v explorer-data:/data \
ghcr.io/luxfi/explorer:latest \
--config /etc/explorer/chains.yaml
Or with compose:
docker compose up
Configuration
chains.yaml is the single source of truth. Every aspect of the deploy --
chains, branding, GraphQL subgraphs, network switcher links -- is per-chain
customizable. See chains.example.yaml for the full
schema.
data_dir: /data
http_addr: :8090
brand_default:
name: Lux Explorer
coin: LUX
accent_color: "#5cf"
networks:
- { label: Mainnet, domain: explorer.lux.network, chain_id: 96369 }
chains:
- slug: cchain
name: Lux C-Chain
chain_id: 96369
type: evm
rpc: https://api.lux.network/v1/bc/C/rpc
coin: LUX
enabled: true
default: true
indexer:
poll_interval: 30s
graph:
enabled: true
subgraphs:
- { name: amm, schema: amm, enabled: true }
brand:
name: Lux C-Chain
accent_color: "#5cf"
$ENV_VAR substitution in rpc: / ws: lets one file template every
network -- mount it read-only and inject secrets via env vars.
Per-Network Customization
The same image powers every deploy. To rebrand for a different network:
- Mount a different
chains.yaml. - Optionally mount
icon.svg/logo.svgat the path referenced bychains.yaml-- they are read from disk per-request, not embedded. - Set environment variables to fill the
${VAR}placeholders.
No image rebuild needed. The SPA reads chain list and brand at runtime from
/envs.js.
Runtime Registry
Chains can be added, updated, and removed at runtime without restart:
# Add
curl -X POST http://localhost:8090/v1/explorer/admin/chains \
-H 'Content-Type: application/json' \
-d '{"slug":"new","name":"New","chain_id":123,"rpc":"http://node:8545","type":"evm"}'
# List
curl http://localhost:8090/v1/explorer/admin/chains
# Remove
curl -X DELETE http://localhost:8090/v1/explorer/admin/chains/new
Adding a chain spawns its indexer + (optional) graph workers; removing cancels them and frees the routes.
mDNS Discovery
Set EXPLORER_MDNS=true to auto-register chains advertised by local Lux
nodes (_luxd._tcp, _zood._tcp, _hanzod._tcp, _parsd._tcp). The
explorer queries info.getChains on the discovered node and registers each
tracked chain. mDNS-sourced chains never override config-sourced entries.
Storage Layout
/data/
cchain/
query/indexer.db (SQLite WAL, indexer)
{zapdb}/ (KV store)
graph/
amm/graph.db (SQLite WAL, per-subgraph)
zoo/
...
Each chain (and each subgraph) owns an isolated SQLite + KV store. WAL is
optionally streamed to S3 with PQ encryption when REPLICATE_S3_ENDPOINT
is set -- see luxfi/indexer/daemon.
Local Build
# Sibling layout: ~/work/lux/{indexer,graph,explorer}
cd ~/work/lux/explorer
go build -o explorer .
go.mod uses local replace directives pointing at ../indexer and
../graph. Production CI clones all three repos into the same layout
inside the Docker build.
Endpoints (curl examples)
curl http://localhost:8090/health
curl http://localhost:8090/v1/indexer/cchain/blocks/latest
curl -X POST http://localhost:8090/v1/graph/cchain/amm/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"{ swaps(first:5) { id amount0 amount1 } }"}'
Related
luxfi/indexer-- chain indexer libraryluxfi/graph-- per-chain GraphQL libraryluxfi/explore-- Next.js SPA