Commit Graph
77 Commits
Author SHA1 Message Date
zooqueen 72ef1420e9 Routing: use /v1/<svc> prefix, never /api/ (house convention)
The zip front router mounted service reverse-proxies under /api/<prefix>; renamed
to /v1/<prefix> per convention (we never use a /api/ path prefix). Upstream
service-internal /api/v1/... paths (behind the proxy) are unchanged.
2026-06-23 21:26:39 -07:00
zooqueen 7156dfdd7a feat(single-binary): zip front router + reverse-proxy to in-process Blockscout-rs services
Make the explorer ONE Go binary fronted by github.com/hanzoai/zip (Fiber v3 /
fasthttp) that runs the Blockscout-rs Rust services IN-PROCESS via the existing
cgo FFI staticlib and reverse-proxies to each. Least-work: the Rust services are
not rewritten/ported — their real entrypoints run on Tokio threads inside the
process, keeping their own HTTP servers on loopback.

Front router (front.go): zip owns the public listener; routes in order
  /api/<prefix>/*  -> httputil.ReverseProxy -> 127.0.0.1:<svc_port> (prefix stripped)
  /*               -> the existing net/http explorer mux via zip.AdaptNetHTTP
Security headers + CORS become zip middleware (replaces withSecurity).

Config (services: block, ServicesConfig/resolveServices in config.go+services.go):
single source of truth for enabled services, loopback ports (base 8050,
HTTP=base+2n/gRPC=+1), URL prefixes, and per-service settings JSON; both the
proxy and the FFI launcher read it. startFFIServices now takes resolvedService
and feeds each service its server addr (+ DB url when set).

Status: sig-provider WIRED + PROVEN end-to-end — one process owns :8090 and
:8050, curl /api/sig/health -> {"status":"SERVING"} and the real REST API
/api/sig/api/v1/abi/function -> 200. visualizer + smart-contract-verifier are
stateless and ready to wire (recipe in ffi/README.md). stats +
multichain-aggregator stay DISABLED: blockscout-service-launcher's DB layer is
Postgres-only (initialize_postgres pins DatabaseBackend::Postgres) — no SQLite
without a Rust rewrite — and stats also needs a populated Blockscout indexer
Postgres. The Go side already passes database.url+migrations through, so they
enable with config alone once a Postgres exists.

Build: make single (cargo build ffi/ + go build -tags ffi) unchanged; default
go build ./... is the Go-native binary (ffi_off.go stub, no cgo). go build ./...
and go test ./... pass; make single links the sig-provider symbol and boots.
2026-06-23 21:20:29 -07:00
zooqueen f3bf55b0b9 feat(ffi): run Blockscout-rs explorer services in-process via cgo (single binary)
Make the explorer ONE binary that runs the Rust Blockscout-rs services
in-process via FFI — not by rewriting them in Go and not by exec'ing
subprocesses. The real Rust service entrypoints run on their own Tokio
runtime threads inside the Go binary.

- ffi/ — one C-ABI staticlib (crate-type=["staticlib"]) wrapping the
  service crates by path (../../explorer-rs/<svc>). One staticlib = one
  Rust std/panic runtime, no duplicate-symbol conflicts. A `service!`
  macro is the single place that bridges a service: parse Settings JSON,
  spawn a dedicated OS thread with a multi-thread Tokio runtime, run the
  service's real entrypoint. Exports lux_explorer_start_<svc>() +
  lux_explorer_start_all(). Disabled services still export their symbol
  and return LUX_FFI_ERR_DISABLED so the Go side links identically.
  sig-provider is wired and proven first (stateless, fastest to build);
  the other four services' deps/features/service! blocks are written out
  commented with confirmed entrypoint signatures for mechanical scale-out.

- ffi_on.go (//go:build ffi) — cgo preamble (-llux_explorer_ffi + the
  darwin/linux system libs the Rust graph needs) + extern decls;
  startFFIServices() launches the services at startup from main().
- ffi_off.go (//go:build !ffi) — no-op fallback so plain `go build ./...`
  compiles WITHOUT the staticlib present.

- Makefile `make single` = cargo build --release in ffi/ then
  go build -tags ffi. ffi/README.md documents the C ABI, the protoc /
  protoc-gen-openapiv2 build-tool prereqs, the seeded Cargo.lock, and the
  one-service-at-a-time scale-out recipe.

Verified: default `go build ./...` exits 0; ffi/ staticlib compiles with
sig-provider linked (.a 7.9MB shim -> 58MB with service); cgo -tags ffi
links it into the binary (symbols present via nm); the single binary boots
and logs `[ffi] start sig-provider -> ok (0)` with the service running
in-process. go vet clean under both build tags.
2026-06-23 21:06:50 -07:00
zooqueen 345290e6df build: bump luxfi/indexer v1.4.5 → v1.4.6 (real tx fee + block burnt_fees/rewards, decimal value) 2026-06-23 14:59:00 -07:00
zooqueen 3611483f2b build: replace luxfi/indexer => sibling clone so the Docker build uses indexer source (real tx fee + block burnt_fees/rewards, decimal value) 2026-06-23 14:45:44 -07:00
zeekay feaa1442b5 feat: enrich AMM token metadata — bump graph v1.2.3→v1.2.6 + opt-in backfill
graph v1.2.6 reads ERC20 symbol/name/decimals for AMM Token entities
(was address-placeholder). supervisor.go runs graph's BackfillTokens at
startup when BACKFILL_TOKENS=1 to enrich existing placeholder rows in
place (≤3 eth_calls/token, no re-sync), before the live indexer loop.
v1.3.3
2026-06-21 23:55:00 -07:00
zeekay 066a8060c6 fix(docker): resolve private luxfi modules + GHCR push via cross-org PAT
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).
v1.3.2
2026-06-21 15:07:07 -07:00
zeekay b0ad7f49e7 ci: route docker.yml off classic [self-hosted,linux,amd64] to lux-build ARC
The classic-triple pool is serviced by fleet arcd hosts (dbc/evo/spark)
that are not reliably available — the v1.3.0 tag never produced an image and
recent builds sit queued. lux-build is the org-scoped autoscalingrunnerset
in lux-k8s (listener healthy 18d, scales 0->N on demand) that already
publishes the node image. Same one-line pool swap node/docker.yml uses.
v1.3.1
2026-06-21 14:58:40 -07:00
zeekay c93423f735 deps: bump graph v1.2.2->v1.2.3 (native 0x9999 V4/DEXFill indexing, dex+amm subgraphs) + pin indexer to real v1.4.5 tag
graph v1.2.3 lands the native 0x9999 DEX indexing fix: dex/amm builtin
subgraphs, 0x9999-scoped Fill source (DEXFill log), keccak topic-hash
fixes, and re-genesis self-heal. The supervisor already wires
PoolManager(0x9999)+StartBlock into graphidx.NewWithConfig and mounts the
dex+amm subgraphs at /v1/graph/{slug}/{subgraph}/graphql; this bump makes
the engine those routes resolve against carry the fix.

Also replaces the unpushable phantom pin
indexer v1.4.5-0.20260526184203-3b72d71cc8fd (revision 3b72d71cc8fd never
existed on origin) with the real published indexer v1.4.5 tag (main HEAD
ac537780: server-side per-(token,holder) balances + log/transfer column
aliases). This unblocks the unified explorer Docker build, which resolves
graph+indexer from go.mod under -mod=mod.

Verified: go mod tidy clean; CGO build of the unified binary green;
go test ./... ok; graph dex_route_test confirms markets/fills resolve on
the dex schema from 0x9999 entities.
2026-06-21 14:52:43 -07:00
zeekay 5a664afa21 feat(explorer): pass PoolManager(0x9999)+StartBlock to graph indexer; mount dex subgraph
- config: add ChainConfig.PoolManager (DEX settlement precompile, default 0x9999)
- supervisor: runSubgraph uses graphidx.NewWithConfig (PoolManager + StartBlock)
- chains.example: cchain mounts dex (CLOB) subgraph + pool_manager
2026-06-21 12:26:11 -07:00
zooqueenandHanzo Dev d702e5b05e docs: tidy LLM.md indexes; CLAUDE.md -> LLM.md symlink convention
Co-authored-by: Hanzo Dev <[email protected]>
2026-06-17 09:55:44 -07:00
zeekay 2f0af7d7ca ci: route to canonical native arcd labels [self-hosted, linux, <arch>]
Replaces non-canonical scale-set / org-prefixed labels with the
existing labels every arcd host registers with. Matches evo for
amd64 and spark for arm64. No new labels added.
2026-06-10 20:20:27 -07:00
zeekay 3959357be3 feat(v1.3.0): drop embedded SPA — per-brand FE Deployment owns the UI
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.
2026-06-10 13:21:01 -07:00
zeekay 73749c70f4 chore: scrub default from Dockerfile NETWORK_CURRENCY_SYMBOL
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.
2026-06-10 12:17:24 -07:00
zeekay 366e4c57d2 fix(spa): brand merging + env-tag canonicalisation
- BRAND is now a merge of SERVER_BRAND (chains.yaml-supplied) and
  INFERRED_BRAND (host parser table). Server wins on name/coin/accent
  when it's non-default; inferred fills in iconUrl/logoUrl when the
  chain config doesn't carry per-brand SVG paths. Stops Zoo rendering
  with the default Lux favicon when chains.yaml omits icon_url.
- networkFromHost now canonicalises "test"→"testnet", "dev"→"devnet"
  so the dropdown label matches the server-side VITE_CURRENT_NETWORK.
2026-06-10 11:21:02 -07:00
zeekay c75790d446 fix(spa): client-side network derivation fallback for older images
The Go-side networksForHost was added in v1.2.20 but the deployed
image is still v1.2.18 (CI pool gap — lux-build AutoscalingRunnerSet
is missing from arc-system). The SPA now also parses location.host
client-side so the network switcher works when /envs.js doesn't ship
VITE_CURRENT_NETWORK / VITE_NETWORK_HOSTS.

Same shape rules as the Go side: explore.{brand}.{tld} (mainnet,
preserve tld), explorer.{brand}-test.network (testnet, .network is
canonical). Numeric and *.local hosts skip the dropdown.

This lets us ship the cross-network switcher via an EXPLORER_STATIC_DIR
overlay (ConfigMap-mounted SPA) without rebuilding the image.
2026-06-10 11:07:39 -07:00
zeekay 248e16dcb4 feat(frontend): cross-network switcher (mainnet ↔ testnet) in SPA
handleEnvs now returns VITE_CURRENT_NETWORK + VITE_NETWORK_HOSTS so
the SPA can render a Network dropdown next to the Chain picker. When
the user picks "testnet" from explore.zoo.network the SPA redirects
to explorer.zoo-test.network (and vice-versa).

Sibling host derivation lives entirely in Go:
- parseBrandEnv extracts (brand, env, dotted, ok) from any of our
  deployed host shapes — explore.{brand}.tld, explorer.{brand}-test.network,
  explore-{brand}.lux.network, explore-{brand}-test.lux.network.
- networksForHost composes the sibling map. Mainnet TLD is preserved
  when the user is on a brand-owned host (explore.hanzo.ai stays .ai);
  testnet is always {brand}-test.network because that's the canonical
  shape.
- isValidBrand rejects numeric stems so an IP host (10.0.0.1) doesn't
  fall through to "explore.10.0.0.1" / "explorer.10-test.network".

SPA renders the dropdown above the chain picker; hidden when the host
yields fewer than two siblings (local dev, admin tooling).

Tests cover all four canonical surfaces (lux+zoo × mainnet+testnet),
hanzo's TLD asymmetry (.ai mainnet → .network testnet), the
explore-{slug}.lux.network prefix shape, and the unparseable-host
guards (IPs, in-cluster service names, localhost).
2026-06-10 11:05:15 -07:00
zeekay 2cbbb4f333 fix(brand): per-host whitelabel + default-chain — covers *-test.network
- host matcher now accepts explorer.* (alias of explore.*) and
  {prefix}.{slug}-{env}.tld shapes — so explorer.zoo-test.network
  resolves to the Zoo brand (was falling through to Lux default).
- new chainSlugForHost helper drives a default-chain promotion in
  handleEnvs: when the host pins a chain, the SPA opens on that
  chain instead of the config-default cchain. explore.zoo.network
  now defaults to Zoo, not C-Chain.
- factored host matching into pure hostMatchesSlug predicate so
  brand and slug paths share one ground truth.
- tests cover both regressions (explorer.zoo-test.network → Zoo,
  explore.zoo.network → slug=zoo) plus the existing
  cross-contamination guards (zoofoo must not match zoo).
2026-06-10 10:46:27 -07:00
zeekay c3afae17ba go: 1.26.3 → 1.26.4 (security: crypto/x509, mime, net/textproto) 2026-06-06 22:05:21 -07:00
zeekay 1fcef19f3f docs: document the unified explorer bundle deployment
luxfi/explorer + luxfi/graph + luxfi/explore now ship as ONE Deployment
per env on lux-k8s. CLAUDE.md describes the 2-container sidecar layout
(explorer Go + explore Next.js), image versions, manifests, and verified
public URLs across mainnet / testnet / devnet.
2026-06-05 15:59:45 -07:00
zeekay c37bb4e893 fix(frontend): match per-chain Brand for explore-{slug} / api-explore-{slug} hosts
brandStruct only matched bare "{slug}" and "{slug}.tld" host shapes —
but the unified explorer ships behind IngressRoutes whose Host()
patterns are exclusively `explore-{slug}.lux.network` and
`api-explore-{slug}.lux.network` plus brand-owned roots like
`explore.hanzo.ai`, `explore.zoo.ngo`, `explore.pars.network`. With
the old matcher every per-chain host fell through to BrandDefault
("Lux Explorer"), so per-chain theming silently no-op'd in prod
(verified 2026-06-05: explore-hanzo.lux.network /envs.js returned
"VITE_BRAND":{"name":"Lux Explorer"} instead of Hanzo).

New matcher recognises the four prefix shapes the IngressRoutes
produce, both with and without an env suffix (-test / -dev):
  - explore-{slug}.tld...        UI host on lux.network
  - api-explore-{slug}.tld...    API host on lux.network
  - explore-{slug}-{env}.tld...  UI host for testnet / devnet variants
  - api-explore-{slug}-{env}...  API host for testnet / devnet variants
  - explore.{slug}.tld...        UI on brand-owned domain
  - api-explore.{slug}.tld...    API on brand-owned domain
Plus the prior bare-{slug} and {slug}-prefix shapes for backward
compat.

Test coverage: TestBrandStructHostMatching, 18 cases incl. the
explore-spcfoo cross-contamination guard ensuring partial-slug-
suffix matches don't bleed.
2026-06-05 15:05:22 -07:00
zeekay 1e602d24e9 feat(frontend): runtime static-dir overlay + substantive SPA shell
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.
2026-06-05 11:05:53 -07:00
zeekay ebfcc0dc16 chore(deps): go get -u ./...
Updated dependencies to latest minor versions via go get -u + go mod tidy.
2026-06-02 11:11:38 -07:00
zooqueenandHanzo Dev 4083aa96e1 Add Claude Code project docs (CLAUDE.md, LLM.md)
Co-authored-by: Hanzo Dev <[email protected]>
2026-05-31 21:58:01 -07:00
zeekay 4e7bb24121 chore: brand-neutral cleanup — remove cross-tenant references 2026-05-25 15:13:23 -07:00
Darkhorse7stars 422c63af88 deps: bump indexer for is_contract column-read fix (#11) 2026-05-24 06:41:41 -07:00
Darkhorse7stars c6c9fb2308 deps: bump indexer for address counter increment + getCode resolve
Pulls luxfi/indexer#10 on top of #9. Address pages on the explorer SPA
now show real tx counts and correctly flag contracts.
2026-05-24 06:06:33 -07:00
Darkhorse7stars c549d96f91 deps: bump indexer to 0.20260524105612-65220f1104cd
Pulls luxfi/indexer#9 — formatAddress + addrCounters now read
both the legacy `transactions_count` / `contract_code` and the
SQLite-backed `tx_count` / `code` column names, and coerce nil
counters to int64(0) so the Blockscout-derived SPA can call
.toLocaleString() without crashing.

Repros against rpc.dev..com:
  GET /v1/indexer/evm/addresses/0x58a86aafb6cdd0989c799353c891e420fb530e0a
returned transactions_count: null + is_contract: false for a real
deployed AMM pool → now returns the actual tx count + is_contract: true.
2026-05-24 03:57:23 -07:00
Darkhorse7starsandGitHub c8a3e72d67 ci: drop arm64 QEMU build (2h -> ~5 min) (#11)
The previous matrix did linux/amd64,linux/arm64 with arm64 via
docker/setup-qemu-action. QEMU-emulated builds of this image
(Next.js SPA + Go cgo-sqlite) ran for ~2 hours per tag — fast
enough to bottleneck every hotfix tag.

Drop arm64. Cluster runners are amd64; Mac devs already run native
arm64 under OrbStack with its own arm64 base story. If we ever
need an arm64 multi-arch index, the right path is a second matrix
entry on a native arm64 runner — not QEMU.
2026-05-24 03:06:58 -07:00
Darkhorse7starsandGitHub 296eee1f72 indexer: wire chains[].indexer.start_block + default devnet chainId (#10)
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.
2026-05-24 00:44:12 -07:00
Darkhorse7starsandGitHub ad72ca6497 fix(sse): minimal HEAD response to avoid GCP/Traefik 502 on streaming content-type (#9)
GCP HTTP/2 load balancers (and Traefik in some configs) return 502 to
clients when a HEAD response carries `Connection: keep-alive` plus a
streaming content-type — because they expect HEAD to be a closed-body
response, not an opened SSE stream.

Symptom: external `curl -I https://.../v1/base/realtime` → 502 even
though in-cluster `curl -I http://localhost:8090/v1/base/realtime` →
200 OK with full SSE headers. SPA's `fetch(..., {method:'HEAD'}).then(r => r.ok)`
gets false → live channel `disabled=true` → no live updates ever fire.

Fix: HEAD pre-flight returns ONLY `Content-Type: text/event-stream` +
200, without the streaming-only headers. The full SSE header set still
ships on the actual GET stream.

Applies to both the per-channel HandleSSE() and the multiplexed
HandleMultiplexedSSE() handlers.
2026-05-21 20:31:32 -07:00
Darkhorse7starsandGitHub 08f343c4a1 feat(realtime): add /v1/base/realtime multiplexed SSE channel (#8)
The bundled SPA opens a single EventSource at /v1/base/realtime and
routes incoming frames via msg.event off a handler map — see
realtime_sse.go for the wire shape. Per-channel /blocks /transactions
etc. stay for external consumers; the SPA only uses this one path.

  client.es.onmessage = o => {
    const c = JSON.parse(o.data);
    const d = this.handlers.get(c.event);
    if (d) for (const m of d) m(c.data);
  };

Wire:
  - HandleMultiplexedSSE() attaches a wildcard sseClient (channel=`*`)
  - Broadcast() now builds two SSE payloads per event:
      perChannel: existing `event:<type>\ndata:<msg>\n\n` framing for
                  /blocks /transactions /etc.
      wildcard:   `data:{"event":<type>,"chain":<slug>,"data":<...>}\n\n`
                  for /v1/base/realtime — no `event:` line so SPA's
                  onmessage gets the JSON envelope and dispatches.
  - sseRegistry.fanout dispatches both payloads to matching clients
    based on channel ("*" → wildcard, "blocks" → perChannel)
  - main.go mounts GET + HEAD /v1/base/realtime (HEAD is the SPA's
    pre-flight to decide whether to disable the live channel).

Without this path the SPA's HEAD pre-flight got a 404 (the explorer
SSE-HEAD-silencer from #4) and the client set `disabled=true`, so
no live updates ever reached the home page.
2026-05-21 20:27:59 -07:00
Darkhorse7starsandGitHub 25c98516bb fix(supervisor): preserve unrecognized first-segment when default-routing /v1/indexer (#7)
dispatchIndexer's splitSlug peels off the first segment as a "slug" if
it matches the slug regex ([a-z0-9][a-z0-9-]{0,63}). When that segment
turns out NOT to be a registered chain, the previous code forwarded only
the REMAINDER to the default chain — so `/v1/indexer/main-page/blocks`
silently became `/v1/indexer/evm/blocks` server-side, a completely
different endpoint that returns a paginated envelope instead of the raw
array the SPA expects.

The SPA's home page widget does `data.slice(0, 8).map(...)` on the
result and threw "o.slice is not a function" against the envelope shape.

Fix: when the parsed slug isn't a registered chain, re-join it with
the rest before delegating so the original path is preserved under
the default chain (`/v1/indexer/main-page/blocks` → `/v1/indexer/evm/main-page/blocks`).
2026-05-21 20:15:18 -07:00
Darkhorse7starsandGitHub 41e84df01a feat(realtime): bridge per-chain block events into the realtime hub (#6)
When ChainSupervisor.AttachRealtime is called (from main), each chain's
indexer.Subscriber.OnBroadcast is wired to push block events into the
RealtimeHub on the chain's slug. The hub then fans out to WebSocket
clients (existing path) and SSE subscribers (new in #5).

Requires luxfi/indexer v1.4.4 — pulls in the exported Subscriber()
accessor.

End-to-end chain:
  evm.Indexer.indexBlock → Subscriber.BroadcastBlock → OnBroadcast →
  RealtimeHub.Broadcast → ws.send + SSE fanout → SPA EventSource

Now /blocks, /transactions, /token-transfers, etc. on the explorer
host actually stream live events as blocks land instead of just
returning HEAD 200 with no payload.
2026-05-21 19:09:38 -07:00
Darkhorse7starsandGitHub e87e5b35c3 feat(realtime): add per-channel SSE endpoints for the bundled SPA (#5)
* fix(frontend): 404 on HEAD for SPA-fallback paths

The bundled SPA opens EventSource (SSE) channels for live block / tx /
token streams and pre-flights each with HEAD:

  fetch(url, {method: 'HEAD'}).then(r => r.ok ? openEventSource() : disable());

When the path doesn't match a real asset and falls through to index.html,
the HEAD returns 200 — so the SPA proceeds to open the EventSource. That
immediately aborts because the body is `text/html`, not
`text/event-stream`, and the browser logs:

  EventSource's response has a MIME type ('text/html') that is not
  'text/event-stream'. Aborting the connection.

…for every channel (blocks, internal-txs, tokens, token-transfers,
gas-tracker, stats, validators). Noisy; channel disabled anyway after
the abort.

Return 404 specifically for HEAD requests on the SPA-fallback path. Real
embedded assets still serve fine on HEAD; only the index.html fallback
gets the 404, which lets the SPA's HEAD pre-flight cleanly fail and the
channel disable itself silently. GET/POST/etc. on the same paths still
serve index.html for SPA client-side routing — no visible UX change.

* feat(realtime): add per-channel SSE endpoints for the bundled SPA

The bundled Vite SPA opens EventSource (Server-Sent Events) against
relative paths matching its channel list — `/blocks`,
`/transactions`, `/token-transfers`, `/internal-txs`, `/tokens`,
`/gas-tracker`, `/validators`, `/stats`. None of these existed on
the backend, so every channel pre-flight (`HEAD <path>`) fell
through to the SPA's static fallback, the SPA then opened the
EventSource, and the browser logged:

    EventSource's response has a MIME type ("text/html") that is not
    "text/event-stream". Aborting the connection.

…for each channel on every page load. PR #4 silenced this by 404-ing
the HEAD pre-flight for SPA-fallback paths. This PR goes further:
make the endpoints REAL so the SPA stays connected and receives
broadcasts from the existing RealtimeHub.

Adds:
* `realtime_sse.go` — `sseRegistry` (separate from the WebSocket
  client map), `HandleSSE(channel)` factory returning a per-path
  http.HandlerFunc. Each client gets a 64-deep bounded channel;
  slow consumers drop messages rather than back-pressure the hub.
* `RealtimeHub.Broadcast` — now fans out to both WebSocket clients
  (existing path, unchanged shape) AND SSE clients (new path).
  Message payload is encoded once, wrapped in SSE framing only
  when sending to the SSE registry.
* `main.go` — 8 path → channel mappings, registered on the mux with
  explicit HEAD + GET so the SPA's HEAD pre-flight + EventSource
  open both hit the same handler.

Special handling:
* HEAD requests return 200 + the SSE Content-Type, no body. The
  SPA's `fetch(url, {method: HEAD}).then(r => r.ok)` check passes
  and EventSource opens cleanly.
* `/stats` synthesizes an event every 5s from the hub's connection
  count — useful liveness signal even before broadcasters are
  wired up.
* Other channels stay open with 30s keep-alive comment lines
  (`: ping\n\n`) and wait for hub.Broadcast() to fire. They emit
  no events today (no production broadcasters), but the
  infrastructure is in place — once the indexer is wired up to
  push to the hub, live streaming "just works".

Verified locally:
* `go build .` → clean (34MB binary)
* `go test ./... -run 'Realtime|Hub|SSE'` → ok
* The existing WebSocket subscribe / broadcast flow is unchanged
  (no breaking changes to RealtimeHub's public API or wire format).

---------
2026-05-21 18:57:04 -07:00
zeekay 1d69a1aa9f fix(frontend): 404 on HEAD for SPA-fallback paths (#4)
The bundled SPA opens EventSource (SSE) channels for live block / tx /
token streams and pre-flights each with HEAD:

  fetch(url, {method: 'HEAD'}).then(r => r.ok ? openEventSource() : disable());

When the path doesn't match a real asset and falls through to index.html,
the HEAD returns 200 — so the SPA proceeds to open the EventSource. That
immediately aborts because the body is `text/html`, not
`text/event-stream`, and the browser logs:

  EventSource's response has a MIME type ('text/html') that is not
  'text/event-stream'. Aborting the connection.

…for every channel (blocks, internal-txs, tokens, token-transfers,
gas-tracker, stats, validators). Noisy; channel disabled anyway after
the abort.

Return 404 specifically for HEAD requests on the SPA-fallback path. Real
embedded assets still serve fine on HEAD; only the index.html fallback
gets the 404, which lets the SPA's HEAD pre-flight cleanly fail and the
channel disable itself silently. GET/POST/etc. on the same paths still
serve index.html for SPA client-side routing — no visible UX change.
2026-05-21 18:31:54 -07:00
zeekay 19dd846f6c ci: switch workflows to lux-build (luxfi-scoped ARC pool)
Cross-org runs-on dispatch failure: hanzo-build-linux-amd64 scale set is
registered to github.com/hanzoai and silently rejects luxfi/* jobs.
Switch to lux-build (githubConfigUrl=github.com/luxfi, max 20).
2026-05-19 13:21:03 -07:00
zeekay 7e968642af ci: rip orphan replace block (graph + indexer sibling-dir shims) 2026-05-19 08:40:20 -07:00
zeekay bb4ce66752 ci: rip ../graph + ../indexer replace shims; bump to graph v1.2.0 + indexer v1.4.1 2026-05-19 08:33:35 -07:00
zeekay 7e82c67661 ci: fix runner label (hanzo-build pool)
Imaginary runner label `lux-build-linux-amd64` does not exist. The live
amd64 pool is `hanzo-build-linux-amd64`.

- docker.yml: docker job
2026-05-19 08:03:36 -07:00
zeekayandGitHub 8f3d58ef5e chore: add LICENSE (BSD 3-Clause License) + LICENSING.md pointer (#3)
* chore: add BSD 3-Clause License

* docs: add LICENSING.md pointer to canonical IP/licensing strategy
2026-05-16 10:49:07 -07:00
zeekay a18fcba81b deps: luxfi/crypto v1.19.0 2026-05-13 11:59:40 -07:00
zeekay fb37b21f0c go.mod: bump go directive to 1.26.3 (security advisory) 2026-05-12 21:29:18 -07:00
zeekay f2410a95ff ci: build multi-arch (amd64+arm64) — Mac devs run native arm64, no Rosetta
Matches lux/graph + lux/indexer docker.yml. The unified explorer binary
bundles SPA + indexer + graph in one image, so this is the canonical
multi-arch artifact for the  local stack and devnet.
2026-05-06 10:45:15 -07:00
zeekay 875af11d04 ci(docker): amd64-only — multi-arch QEMU stalls on heavy SPA + Go 2026-05-05 22:32:59 -07:00
zeekay 50fc1cf497 ci(docker): go build -mod=mod populates go.sum on the fly 2026-05-05 21:54:05 -07:00
zeekay ceaf8cab76 ci: drop ci.yml — Docker workflow handles cgo build via alpine 2026-05-05 21:50:48 -07:00
zeekay 7844e67ea6 ci: ARC has no_new_privs (no sudo); GOSUMDB=off everywhere 2026-05-05 21:44:22 -07:00
zeekay 6b5450579e chore: gitignore .claude/ runtime state 2026-05-05 21:34:42 -07:00
zeekay b50a7e1be6 ci: regenerate go.sum at build time (proxy cache inconsistency) 2026-05-05 21:34:25 -07:00