The page builds its socket URL from its API base, so a brand page whose base is
/v1/indexer/<slug> opened /v1/indexer/<slug>/v1/base/realtime and got a 404.
The stream now answers there, scoped to <slug>.
indexer v1.5.10 reports average_block_time in milliseconds, which the page
divides by 1000. The image builds on Go 1.27, the version go.mod requires.
Co-authored-by: Hanzo Dev <[email protected]>
alpine carried a shell, a package manager, libc and sqlite-libs so that a
dynamically linked binary could find them. Linking statically removes the
reason for all four: the image is the binary and the TLS roots, on
distroless static, as nonroot.
The explorer serves its frontend from //go:embed, so nothing else was ever
needed at runtime.
Co-authored-by: Hanzo Dev <[email protected]>
luxfi/* is public, so it resolves through the module proxy — which serves each
version immutably — and verifies against the checksum database under the h1:
hashes in go.sum. A tag that moves upstream therefore cannot change the bytes
this image compiles. GOPRIVATE names github.com/hanzoai/* and nothing else.
`ChainConfig.Enabled` existed on the struct, was written in every chains.yaml
we ship, and was consulted by nothing. Only `Graph.Enabled` and the subgraph
flag were ever read (supervisor.go). So `enabled: false` registered the chain,
spawned its indexer and polled its RPC exactly like `enabled: true` — a switch
that looked like a switch and was wired to nothing.
Found by using it. lux-mainnet's config lists spc, which is a sovereign L1 and
has never been a chain on that network. Setting `enabled: false` and rolling
the config changed nothing: the pod still logged
[registry] added chain spc (…/v1/bc/spc/rpc) source=config
[spc/amm] [indexer] starting
[evm] Failed to get latest block: http status 404
at 1,800 errors an hour, against a chain that was already switched off.
Honoured in ChainRegistry.Add, which is the single door — config, env, mDNS
and the admin API all arrive there — so this is the whole of what the field
means, in one place. A disabled chain is not registered, so it does not index,
mounts no routes, and does not reach the SPA's chain list (which reads
registry.List()).
Enabled is now *bool because absent must not mean off. Every config written
while the field was inert says `enabled: true` out of convention; a plain bool
would read a future config that simply omits the key as false and make that
chain vanish with no error and no log. Nil is no opinion; only an explicit
`enabled: false` disables.
Tests: registration gated in all three states (true / false / absent), a
disabled chain proven not to reach the supervisor or the chain list, and
LoadConfig proven to decode into the pointer — a wrong yaml tag would leave it
nil and silently re-enable everything it disables. Verified by defeating the
check: the false and supervisor cases fail, the absent case still passes.
Also: the Dockerfile now runs vet + tests in the builder stage. This repo has
exactly one workflow, release.yml, which builds on a v* tag; nothing ran the
test files at all. Putting them where cgo and sqlite-dev already exist costs
no new job, no new runner dependency, and means a release that fails them
never becomes an image.
Co-authored-by: Hanzo Dev <[email protected]>
The list named github.com/luxfi/graph and github.com/luxfi/indexer because they
were private and 404 on proxy.golang.org. Both are open source now and both
serve from the proxy, so the exceptions have nothing left to do — and every name
on a GOPRIVATE list is a module Go fetches from git and trusts unverified.
Co-authored-by: Hanzo Dev <[email protected]>
GOPRIVATE switches off the module proxy AND checksum verification together.
Covering github.com/luxfi/* meant every luxfi module was fetched straight from
git and trusted without verification — which is how moved tags got captured into
module caches and served from them indefinitely, making the same commit build on
one machine and fail on another.
Nearly all of luxfi is public and on proxy.golang.org, so it belongs under
normal verification. The private org is lux-private.
Twelve luxfi repos are still private AND still 404 on the proxy, so they are
listed individually rather than by wildcard — coverage now matches what actually
needs a direct fetch, and every other luxfi module is verified against
sum.golang.org again:
bridge broker compliance erc20-go fasthttp graph hsm indexer
mpc ordering password pool
The right end state is those repos moving to lux-private; until then the
explicit list keeps the exemption honest instead of blanket.
Co-authored-by: Hanzo Dev <[email protected]>
The v1.3.1 image build reached the runner (lux-build works) but failed two
ways that the local build now proves fixed:
1. Dockerfile cloned private luxfi/{indexer,graph} over unauthenticated
https -> 'could not read Username'. Those clones were vestigial: explorer
has no replace directive, so under -mod=mod graph v1.2.3 + indexer v1.4.5
resolve from the module cache, not siblings. Drop the clones; add the
node-style BuildKit-secret git auth (ghtok) + GOPRIVATE/GONOSUMDB/GONOPROXY
before go mod download so private modules resolve.
2. GHCR push would be denied (repo-scoped GITHUB_TOKEN). Log in with the
cross-org PAT (GH_TOKEN/UNIVERSE_PAT) and stamp
org.opencontainers.image.source so the package stays linked.
Verified locally: docker buildx build --secret id=ghtok,... compiles the
unified FE+indexer+graph binary green (go mod download 74s, go build 98s,
image exported).
The Go binary is now a data-layer service only. The `/` catch-all that
served an embedded SPA shell is gone; v1.3.0 keeps just the endpoints
the FE depends on:
- /v1/* — indexer + blockscout-compat REST APIs
- /health — health probe
- /envs.js — runtime brand/chain config (window.ENV)
- /icon.svg, /logo.svg — per-host brand SVGs (overlay-friendly)
- /icon-{slug}.svg, /logo-{slug}.svg — extra named brand assets
Every other path returns 404 so the IngressRoute hands it off to the
per-brand Blockscout FE Deployment (ghcr.io/{brand-org}/explore-{env}).
Concrete deltas:
- frontend.go: drop handleSPA, writeIndex, f.index; replace with
handleNamedBrandAsset + brandAssetNames discovery
- frontend_test.go: TestFrontendNoSPA + TestFrontendBrandAssetsServed
replace the index-html overlay tests
- Dockerfile: drop Next.js frontend build stage; runtime image is just
the Go binary + brand SVGs (no embedded HTML)
- static/index.html: removed (60-byte placeholder)
The per-brand FE Deployments + IngressRoute patches that complete this
split live in the cluster (lux-mainnet + lux-testnet namespaces). Source
of the FE images: luxfi/explore via Dockerfile.branded.
Replace the default currency symbol + network ID
(8675312) defaults with Lux mainnet defaults (LUX, 96369). The
explorer binary white-labels per-tenant via runtime env, not via
-specific build defaults.
Two related changes that get the explorer SPA rendering real chain
data without waiting for upstream luxfi/explore to ship a Next.js
static export.
1. Runtime SPA overlay (config.go, frontend.go)
New cfg.StaticDir (also EXPLORER_STATIC_DIR env var). When set,
the frontend prefers the on-disk copy of any static asset over the
embedded FS — so a ConfigMap-mounted SPA shell replaces the
embedded 60-byte placeholder without rebuilding the binary. The
embedded FS remains the source of truth at build time and the
fallback when no overlay is configured.
handleSPA + serveBrandFile both check overlay > embedded. Tests
cover overlay-present, overlay-missing-file fallback, no-overlay,
and env-var-only configuration.
2. Substantive SPA shell (static/index.html)
Replaced the 60-byte stub (<div id="root"></div>) with a vanilla
JS SPA — ~7KB, zero dependencies, no React/Next.js. Reads
window.ENV from /envs.js, fetches /v1/indexer/{slug}/{stats,blocks,
transactions}, renders KPIs + Latest Blocks + Latest Transactions
with per-chain switching and brand color/title overrides. Polls
every 15s. Stores selected chain in URL hash + localStorage.
This is the explorer's default UI until upstream luxfi/explore
ships output: 'export' and the Dockerfile's optional pnpm-build
overlay activates.
Dockerfile: drop the leftover merge-conflict markers around the SPA
build stage, keep the explanatory comment.
Tests: 4 new in frontend_test.go covering overlay precedence + env-var
fallback. All existing tests (registry, realtime) still pass.
Two related fixes for running the explorer against a mainnet-fork
devnet:
1. Honor the per-chain `indexer.start_block` field from chains.yaml
when the indexer DB is fresh. Without this the indexer always
backfills from block 0, which on a mainnet fork is 25M+ blocks of
irrelevant pre-fork history. Now an operator can set:
chains:
- slug: evm
rpc: http://anvil:8545
indexer:
start_block: 25162000
and the indexer skips to that block on first run. Once any rows
exist in evm_blocks, start_block is ignored and indexing resumes
from MAX(number)+1 as before — backward-compatible for
fresh-genesis chains.
2. Default the bundled SPA's NETWORK_ID to 8675312 (the Liquid
devnet chain ID). The Dockerfile previously inherited
luxfi/explore's default of 8675311 — a stale leftover from before
the devnet chainId migration. Adds NETWORK_ID / NETWORK_NAME /
CURRENCY_SYMBOL as build-args so the same Dockerfile produces
correct images for devnet/testnet/mainnet.
Requires luxfi/indexer #8 (StartBlock field on evm.Config) — bumped
via `go mod edit` to the post-merge commit d5e65d4b50c1.
Module renamed to github.com/luxfi/explorer (was -unified). Imports
luxfi/indexer/{daemon,evm,storage,explorer} and luxfi/graph/{engine,
indexer,storage} as libraries; embeds the luxfi/explore SPA via go:embed
all:static.
A new ChainSupervisor wraps the existing ChainRegistry: adding a chain
(via config / mDNS / admin API) spawns a per-chain indexer goroutine plus
one graph engine + indexer per enabled subgraph; removing cancels them
and frees the routes from a sync.Map dispatcher mounted on /v1/indexer/,
/v1/explorer/ (legacy alias), and /v1/graph/.
Config schema is fully per-chain: indexer poll interval, list of graph
subgraphs (each can pick a built-in schema like amm, securities, fhe…),
brand (name/coin/accent + on-disk icon/logo paths), token features, and
top-level networks list. /envs.js exposes the chain list and active
brand to the SPA at runtime, so a deploy can rebrand by remounting
chains.yaml + assets — no image rebuild.
Dockerfile clones luxfi/explore + luxfi/indexer + luxfi/graph as
siblings inside the build, populates ./static from the SPA build, and
produces one binary. CI checks out the three repos as siblings so the
local replace in go.mod works without tagged releases.
4 repos, 4 images:
indexer → chain data (blocks, txs, tokens, contracts)
graph → subgraph events (swaps, pools, DeFi, GraphQL)
explore → Next.js frontend
explorer → all three combined (this)
Single image contains 3 binaries: indexer, graph, explorer.
Each can run standalone or together.
Three repos, three images:
luxfi/indexer → ghcr.io/luxfi/indexer (Go backend, API + indexing)
luxfi/explore → ghcr.io/luxfi/explore (Next.js frontend)
luxfi/explorer → ghcr.io/luxfi/explorer (this: combined single image)
Dockerfile pulls both repos at build time, embeds the Next.js static
export into the Go binary via go:embed, produces a single container.