# v2 git bundle
72ef1420e9ea14835791e8bf3d58634af4d228ca refs/heads/bundle
72ef1420e9ea14835791e8bf3d58634af4d228ca HEAD

PACK     zžxÞ!þtree 6141382d83b07b2c0e284626ccd11dc1d257c5e6
parent 7156dfdd7a99f5ca21a1223f2221d4760b4e85e0
author zooqueen <a@zoo.ngo> 1782275199 -0700
committer zooqueen <a@zoo.ngo> 1782275199 -0700

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.
ƒÒ›Ç”‹x´K÷tree 5e612cc96744e78f6cdd96406b0115fa51bd65ec
parent f3bf55b0b965975a0ac3eb753741d621e3e8461c
author zooqueen <a@zoo.ngo> 1782274829 -0700
committer zooqueen <a@zoo.ngo> 1782274829 -0700

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.
ü)þdœŠx¬S÷tree 9e8ee3c75183b8be0cea59d7adb67cdcea7ce113
parent 345290e6df11a6dfb5477892914f9fd7e6d7c0d1
author zooqueen <a@zoo.ngo> 1782274010 -0700
committer zooqueen <a@zoo.ngo> 1782274010 -0700

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.
uþÊ�x ßþtree ca35b980476918978416e94a9ad803e4f24cacef
parent 3611483f2bbb74121a458560d25c43aa848d616e
author zooqueen <a@zoo.ngo> 1782251940 -0700
committer zooqueen <a@zoo.ngo> 1782251940 -0700

build: bump luxfi/indexer v1.4.5 â†’ v1.4.6 (real tx fee + block burnt_fees/rewards, decimal value)
ÀnX1šxœ…�IN1E÷}ŠZ‚Òžâ6‚ˆ÷@e»:±pÛÁDœ)kv_Uï}é·Bd•õ´÷Î®n!ÅŒ™�Ô¹än×CC\Lg,”¬„È¤äV	aŒdFà¢­Ø¯Äf5å•7J3?ao§\à'çÏN”à_G~LÇ| ¦ÎcBÂÃ¬çyryÛBkô/¯üd{ˆþ	
�#:‚Ø/kØ…äé2:^Pƒ�!ÁÅœj†v"xËîc¼ÿ\è•*\�š{57…0B»Œ•w`ã^R{—º+ô�Å×{ðäÂ6Ð/Œ�n§_ñie•#x5Êýtree 3c3dec99c575de736fb145dd30c3c94f48ce3d12
parent 066a8060c6b97b6e0229c089d2cad374bbe9871b
author Zach Kelling <z@zeekay.io> 1782111300 -0700
committer Zach Kelling <z@zeekay.io> 1782111300 -0700

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.
 nºÌœIxœcûtree ba716416600507c457856837e97bedd31333863f
parent b0ad7f49e7c86b7ab0e9bdd6cb398cd2c18786b8
author Zach Kelling <z@zeekay.io> 1782079627 -0700
committer Zach Kelling <z@zeekay.io> 1782079627 -0700

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).
Òoˆ†•(x…zýtree 61f61a06c21ead11e8dbede2e71c095d7d9df936
parent c93423f735b1f30ed3428bb37c7de0f2e5949fe8
author Zach Kelling <z@zeekay.io> 1782079120 -0700
committer Zach Kelling <z@zeekay.io> 1782079120 -0700

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.
dÙy“Pxüútree 5ddb06aa57620e76513b326d53f1bb9d0be21b91
parent 5a664afa2140a96622500c9faa59b3a33804dd0b
author Zach Kelling <z@zeekay.io> 1782078763 -0700
committer Zach Kelling <z@zeekay.io> 1782078763 -0700

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.
S ®=˜ x÷ýtree f4b03ec624fdd7dea2f4332c3caa20b447df8f63
parent d702e5b05e858ac3723f5ddbe352d3a9fd572f57
author Zach Kelling <z@zeekay.io> 1782069971 -0700
committer Zach Kelling <z@zeekay.io> 1782069971 -0700

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
Šž«²˜x(×þtree b63098112a92ad45d48589d236f4b40f7a00c7ce
parent 2f0af7d7ca190dd3a4a317c6cd85c6d6484fdf4c
author zooqueen <a@zoo.ngo> 1781713604 -0700
committer zooqueen <a@zoo.ngo> 1781715344 -0700

docs: tidy LLM.md indexes; CLAUDE.md -> LLM.md symlink convention

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
UÒ[�xÀ?þtree 2e60baefb8dd27831edfeaa810087340cab9b5bd
parent 3959357be3f0d9e0dfc31ed6ad3941d73d727ba9
author Zach Kelling <z@zeekay.io> 1781148027 -0700
committer Zach Kelling <z@zeekay.io> 1781148027 -0700

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.
ªØ“#”]xÔ+útree b602356e6af99611f26e0750737df4bfcdfd1524
parent 73749c70f4def7abc8a4b1ed2d948a2d68bc6c20
author Zach Kelling <z@zeekay.io> 1781122861 -0700
committer Zach Kelling <z@zeekay.io> 1781122861 -0700

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.
%Êø–”xÔ+þtree e6019e0f297add27651850bbf8f58cd6b264ec99
parent 366e4c57d2a864b34e029e3b677f386bf677dd32
author Zach Kelling <z@zeekay.io> 1781119044 -0700
committer Zach Kelling <z@zeekay.io> 1781119044 -0700

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.
lð•e�-xÝ"ýtree 770866e35ba51fd79adb60ad5b776a88fe3b2843
parent c75790d446cffe079b8d44e4be04095412967546
author Zach Kelling <z@zeekay.io> 1781115662 -0700
committer Zach Kelling <z@zeekay.io> 1781115662 -0700

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.
èùóP�8x�rütree 313b53ff42c8059407686565713613fb264a7933
parent 248e16dcb43b82f6522892a790b15ca4d8a3f180
author Zach Kelling <z@zeekay.io> 1781114859 -0700
committer Zach Kelling <z@zeekay.io> 1781114859 -0700

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.
‡2rš^xêútree d939c2cfff5d773b3fa266566cd0aad949955863
parent 2cbbb4f33345fb35f4156ba59990aa7a445c6d1f
author Zach Kelling <z@zeekay.io> 1781114715 -0700
committer Zach Kelling <z@zeekay.io> 1781114715 -0700

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).
~È’’>xâütree 3486a514c60f3090f74d24ec1373c50a292d2ec9
parent c3afae17ba2d15c53ff2a3eab0567e5bcdc4fcfa
author Zach Kelling <z@zeekay.io> 1781113587 -0700
committer Zach Kelling <z@zeekay.io> 1781113587 -0700

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).
¨Zv›xôþtree d9a03227883732d65256e94f2a219a14e17ab540
parent 1fcef19f3f59666d3de90dc206a71cdfc9b4a654
author Zach Kelling <z@zeekay.io> 1780808721 -0700
committer Zach Kelling <z@zeekay.io> 1780808721 -0700

go: 1.26.3 â†’ 1.26.4 (security: crypto/x509, mime, net/textproto)
·O–– xùýtree 12804903a99012bf13bd903ce7f8f0b805b37750
parent c37bb4e8932a06d7abe47a21851ed4e9a46a7b33
author Zach Kelling <z@zeekay.io> 1780700385 -0700
committer Zach Kelling <z@zeekay.io> 1780700385 -0700

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.
ž$ª¹”cx4Ëùtree 2e336f7b7106557d590bd75931709b232098af54
parent 1e602d24e9d778f956b8fe1e50264e2357707095
author Zach Kelling <z@zeekay.io> 1780697122 -0700
committer Zach Kelling <z@zeekay.io> 1780697122 -0700

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.
Û#BštxJµøtree 875012abc092d9024cb6f927a86eb2d713a35dc3
parent ebfcc0dc162ab13d1251f3c22d5f7a869c6427e1
author Zach Kelling <z@zeekay.io> 1780682753 -0700
committer Zach Kelling <z@zeekay.io> 1780682753 -0700

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.
j~n‘x1Îþtree 9aa3177294db194eca31bbb3eff2d631aaa4553b
parent 4083aa96e18473c8a7290658af15a891ec69d89d
author Zach Kelling <z@zeekay.io> 1780423898 -0700
committer Zach Kelling <z@zeekay.io> 1780423898 -0700

chore(deps): go get -u ./...

Updated dependencies to latest minor versions via go get -u + go mod tidy.
eW]j—xèþtree 0314fc2a348c06d928a7e095079b7dc1331cf7dd
parent 4e7bb24121304e9e4bdc21d233af4292ab3c3cf9
author zooqueen <a@zoo.ngo> 1780289380 -0700
committer zooqueen <a@zoo.ngo> 1780289881 -0700

Add Claude Code project docs (CLAUDE.md, LLM.md)

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
XT˜x÷þtree 316a1bf6e906e3527a284b3c1fe0e8fcc9b46b34
parent 422c63af88fe9d6f1f9c5fa64d328306aed697cd
author Zach Kelling <z@zeekay.io> 1779747200 -0700
committer Zach Kelling <z@zeekay.io> 1779747203 -0700

chore: brand-neutral cleanup â€” remove cross-tenant references
\ƒS¯—xœ��Mj!…÷ž¢ ›Éb‚?Ý­†�EîJ-Éh7j“9~<CV¾ï�Æ™Q-ä,E"òF¯¤�ZƒÓœWr]tâ Æu@Ø&¯�t
™uÄÉXiÅdözV)
:Ç÷ÞàƒÚÏÌÎ¶jn¿>W”›–ï_…òý%ìå”µ¸©¤‚«´RŠIKƒÿ-‘�þ
þ,äù1UiîÉý3ìu4
óË~?K½6¦)?àò¤Ô³øúœVµŸxœ��KNÃ0†÷>ÅH]"Š�ÄqŒ¢‚Tâ~Œ“¨ŽÙ·Ç…ž€Å¼}3S"4-eé0ÌqÚ²AÙÚ�òj(Œªm­ «J
ÞI+{'™cÚñŽËžÑ¦¸{¥-ç�î$'j+SLð¡Ò¥ÆŒ"•2¼\õ$íú6.jöG—W`BÈ¾R¶ðH¥¤v—¹ü7€X\ó3èmYa÷ŠrõemÂœÁÄ-Üðs0	—Ûg0by�¡
¢ÿBBÎ›÷ü¶»ùé90
1@‰+Dy„Ó�¸ªóïlBÀ}õ1UþçùDB¼BžªK¨<”ýoylMSBSü78¯ÆZ†’”)ùH~ [‹†B—2xœ�‘MŽ1…÷>EIlfé¸ÿÓB„±™‘Âž”íêÄŠÛŽlw’Ù±â ˆÎIpgfÀŽ…eë¹ÞSU}Ñ�l�e]
Y‹tT×ËFTB•e±èŠR5¼’ŠÐ“� XR[¨¦åJÖ}“£j¨—BeY¢è©]ÛÖÃ1îœ‡è÷éÔ†ˆ>Àë“Ð¶ãMÁßnÔ&“nxyÛvI*ªf¼åœ%uÐ1Ò0E‡°1ÐVÑ9EE<KEOãæ¼nòbÖÔEÁû<¿ÌÈîFc˜ñÜëù“ëEßAïü€q¥”§à%`z½w£M-°îžP1áââŽÀÐå=l¢GPFílø&§úÌa#�M?2&IÑÐªÉÅÖŸot¤™@¹'•Ìç?,S¥tf,X(¼ºø¤#/	¬6L>÷“Õ66Õ¿†à.½3NîCªˆ3E^Sþúnm:Æ°,º—^´Ž^ÛíÕ5œt8&âÃ.Ic_èà] Ü¢¶!‚?ÈLÑ1›,À§�_a~ÌŸ77§ã0ÇÇ�Q˜ós½ÀEƒØ‹F*Å»E'Û®+ëR.ºœª„AÔ%'ŽÌS½M-þ»¾%ØÄ(Ð“ð¸Å%ôhMŒ 'f¢oÜ}JXÝÞÂÁ9?~>qš²Ãe)É;¢�x†Köß©Ñ�”±ß±O–/xö	ýtree d7eb53559185f5e2ac8c7313d18df19c97239274
parent 296eee1f72188c6d1e45ba9c327838e45921f287
author Darkhorse7stars <92065535+Darkhorse7stars@users.noreply.github.com> 1779617218 -0700
committer GitHub <noreply@github.com> 1779617218 -0700

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.Õúe–]xÖ)útree bcaf362ae5ffc1c01cb4dac8b65ba4c29f314283
parent ad72ca64976b1d7d6b984c4964746e1a668019ab
author Darkhorse7stars <92065535+Darkhorse7stars@users.noreply.github.com> 1779608652 -0700
committer GitHub <noreply@github.com> 1779608652 -0700

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.Ç�ë½œGx|ƒûtree abfe5dc2b9cd0a7a48fa5dd67a21a5394276ab87
parent 08f343c4a1cd227c75962c06833068c0ef8acb6f
author Darkhorse7stars <92065535+Darkhorse7stars@users.noreply.github.com> 1779420692 -0700
committer GitHub <noreply@github.com> 1779420692 -0700

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.ã;„r‘fxažùtree de3bf981ef4194c6bd1abf733996872270b27eef
parent 25c98516bb42a7a9e11a83decff8983fc90bdb2b
author Darkhorse7stars <92065535+Darkhorse7stars@users.noreply.github.com> 1779420479 -0700
committer GitHub <noreply@github.com> 1779420479 -0700

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.&CêŸGx€ûtree fdc48f3ac2b455d3f4ae846fa534011dbaa212b7
parent 41e84df01aaed72522e4ad2c0e3d00d723b47024
author Darkhorse7stars <92065535+Darkhorse7stars@users.noreply.github.com> 1779419718 -0700
committer GitHub <noreply@github.com> 1779419718 -0700

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`).#ÿŒu‘>xáütree da4ba9c87520199170911fa37074680541f61043
parent e87e5b35c38229fa9063d35782e366f7d172e503
author Darkhorse7stars <92065535+Darkhorse7stars@users.noreply.github.com> 1779415778 -0700
committer GitHub <noreply@github.com> 1779415778 -0700

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.ØT¤˜ïxœ¥WÝrÛÆ¾ÇSœQ.HÆ$DI¤$Ê®¥VãÌÄ‰GR“^d¦\`Ä†àÝ]˜b“Ìä*�É3ôÁü$ýÎ. Ùíd¦¾‘	ìžŸï|ç;Þ*E§§™Ì/T6ž�žM³¬˜e—Åtv)§§ùLžË“âBùôâ,i„UÆÓ‰<Ÿ‰!f…šÊ³±Ì.OfB§“Ë||r>‘—B�'ÅI"Z_Ö–^	»Ä_§.œÖÑ‹Ùéø|:=›>{òæ³Ö)ëRS[ÕT›t¡}Ùfi^¯^ÒÉÅÅlr2ŸNh4¾�<]iï•¥/´Ýfôb{í³ÿy-)”ð}«DåõJ®HHI�²£¼Æ¨ŠîînHÙÔÚxGÂ÷¥¢¬5²R’îÞ^Sÿ“é I>¥B?ô[�ã04O¨6ôúæúU¸†££BTU&ò%5Â—.IîŸ˜ªeÝ¼ªwuksE}øÐ6˜è¿Òïp«ªaæ˜ü'¾^*CÎ#�•#a��U£¢Ò‹1+‘—´!–«$!*”ÏË~k«!ý¸R(Š¼¢¿íý<H‘Ÿé[úÓK²i½¤?‡¨:1õtER;‘Uøÿày’|‡N‹d­œéyZ	8!A.	ç”¡1§mÝ.Jò5i#ÕCZúU5LØH@Ì*ßZ`q:Óû_~'WRcë\)éø.‡^tâKé¾>Ñ«•’ZxUmHdµ™Ê(+XËiGs¯ü1{ŸÉÔ>‰ÛEHñ‚·l½'©ª. ÙñÛsˆÚ5µ�‡R ôæË77ä7ÊØÛ»é`IxöÍîˆz8ì¥tÍk³^óÕÏ½®Mš$ïù7³ çífGê>¸!ÀDQ�ü~bìþŽ¼Æè¨a²ŽæKe‡ Žð8ôNTZ
�ö¤ôu­ÝæùÞþ¶Ü@lÖ€pª EX·¡`�õ®Q¹.tŽBoc·ýg«ªP›])÷CJ·`J¢V™’’]1e¢ÓUE ¬/´Q»¦zŽÿÀ>›:0ˆv“ßå—ˆhHëRƒ‹Õî|£Z!¬C§P^)Á&¡«]Å“'�öNU9]¡^%úâæþøí7w÷Çè©t—œ«ØÛø“7Ð(	pªCáµT„–5gÂ›šÞi§Ùåßþê°Pi�™ÿW°ËÎ·Ú«ÿª=´Ý±È‡n@b!´q>±ªž•(fš�c>{ÖTÚù�Ìü8òs>LæÇ�†"Ððî	;Ã³.�‡¢�yÃ«qÃÏ@ç9S˜ÉRpúèHõ€`�omm™#@iÈÂò¸•:„èÏE^pŠ/ç@�Jv”ë@%ö©ó=û†{±b1ÐªH§¾U•…’AWþ¸²í•åè©²} -GD[xbìð £#@�X(„(dJooé“Iì�<$—Ù†[m»ï`ùñÈZ�»°·ÀÔ ¢µ¸k¯’•XF™>0ùöæú«î ä·?(“J¹']D…ÌËFò*šbpÚ·ÛæÁ–€œ¯¥„”Jó]Oýº“.êyd.~ÜªnÚÍœúNaçÁL9XýNew ¶b¼c/£šˆøZp‡¡û[(s$O7ÛñÆÁˆÐ¼”Þ7i¼gÿÚèÉM(E´$MÐùd$•j0ÁÐÂH|ký9î»ª^3$®]¡‘HÚº¡•r•…cË1�þ#TÇ¹ÖF yEbºà|¾Cq¾•¤5€H@¦˜þY�i¿Ï¥Cý=Ôœ×��IÐ/I®�Ðõ×¯‚Nm¯Pß¨u8;HqýMŒ6L7æ3xVËÐ½Ü7k+š¿´	V
+VìŒç®¯¹åˆ(¾%ŽÙmCš+¨Ø¾È—qqyÿëo{Ö£†îc,ÆkÊïÁØª}û|©‡¦Ò¹ö‘îÏxt8ú‘ó¬Û×0˜ d	+ûÑQF€žw<L±@…'ˆˆ©úxžF.…MéÙ>Ù¿„=Ô�î¡¼×„}‡»��ÆÐæÛÙô; ˆ›—*ô,Ú�+ÌÍÖqxlh x+Ãä6†XÿKñfJAŠ¶‚2í4'è×s1ân¿ãøÀÎV´UX|¸áY`4[Øé ,AB#U™÷ø,ah®YÛp8ßêï×hÖŽ}Ø‹ÏÆŽ–h©‘»5Jp”€;$ÛŸ_“á{ó½ÁàÜ×BGI+»m‚Í|+à7 ½!…O0uÌ›WV	Ï}<À+Û�ê£Ø!Yˆ€{Ú€Ú ,Îq›¢Àø¦ œaáf8ì?ÈvIÃ4­Û*D:ŒßqpwýÐbB¯k»tGàÚ·ÊboÃ}k^Þ‚,.À�VW’Òyì.2õÏ&o>§La7ƒí1:Rzœ¦)�lk¨·Ó’Ÿ &?�•½h¡^âÂ}W‘"âÚÌåVg
ß6{\¨`]Cv{!áª0©‘È’ÄÇá“ #``TÓfhPº~û%¡ZŒ—Û
”&íþ%ÿ�!%eí�cxœ}Ë1
Â0 P]r!à¢ƒ’˜ÿ“D\Å¸ý¤?Z´VJìâ!¼ˆàåíêâÛßgü�Ü2KCŠTÒ„®`(CÄ,–£>A(µ&ï‹äÅ•Z¾d©}Y`“�àLÂ ÀÄþ³÷Ë ÐAÂdÈ
ºåcÓÊ=Å£Üñù\]rÕm:æÝU³ÞJíœmPƒœ+TJÄ¦®«œù_û]®_â¹è‘œN`ö®Ëáƒ@%�xÐ/þtree 4a3520bf2f2184bb8c1c19162cfdba7471d8af38
parent 7e968642af547fbdb38a1b27274648b7316bd784
author Zach Kelling <z@zeekay.io> 1779222063 -0700
committer Zach Kelling <z@zeekay.io> 1779222063 -0700

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).
b˜k™x	öþtree 2f2a5c66dc9a24f0e4d8b934ddcf08160dfbdf2f
parent bb4ce667525c70dcd3847c2350e3f72bd32a72ce
author Zach Kelling <z@zeekay.io> 1779205220 -0700
committer Zach Kelling <z@zeekay.io> 1779205220 -0700

ci: rip orphan replace block (graph + indexer sibling-dir shims)
âÃR;›xäþtree fe946c3655d13d622306adf8142fa975c2226355
parent 7e82c67661a4337c6f17cb9ffe90e9296d85ac5d
author Zach Kelling <z@zeekay.io> 1779204815 -0700
committer Zach Kelling <z@zeekay.io> 1779204815 -0700

ci: rip ../graph + ../indexer replace shims; bump to graph v1.2.0 + indexer v1.4.1
Ð'SÀœx|ƒþtree bb7107d16c636d6f9d677cd8ef75176724440b5a
parent 8f3d58ef5e21746b661c4aa62fdbe7f2cb7fb901
author Zach Kelling <z@zeekay.io> 1779202927 -0700
committer Zach Kelling <z@zeekay.io> 1779203016 -0700

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
á]y–xv‰þtree 1b601c107e5b2b5127157e64b2b0a7da33b69766
parent a18fcba81bd9105100c3616a89b6e29f82cf1a33
author Zach Kelling <z@zeekay.io> 1778953747 -0700
committer GitHub <noreply@github.com> 1778953747 -0700

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 strategyN¯qG“xã ÿtree 840c342b50cfd524a73f5fcd9413e28594e3e367
parent fb37b21f0c8a010be1b23c6e58f4614a35903470
author Zach Kelling <z@zeekay.io> 1778698780 -0700
committer Zach Kelling <z@zeekay.io> 1778698780 -0700

deps: luxfi/crypto v1.19.0
^‹Bk�x ÿþtree 0397891d3eeabb15b99bfad03a75e845514d49b6
parent f2410a95ff387bd503d004fe04ed165da8302db5
author Zach Kelling <z@zeekay.io> 1778646558 -0700
committer Zach Kelling <z@zeekay.io> 1778646558 -0700

go.mod: bump go directive to 1.26.3 (security advisory)
ºM^˜xØ'þtree a84952a30b356e5c7a51e88fcc1b6daf11828d32
parent 875af11d0487f71a18e59cf4e762429882794d4a
author Zach Kelling <z@zeekay.io> 1778089515 -0700
committer Zach Kelling <z@zeekay.io> 1778089515 -0700

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.
©ô˜«œxóþtree b1c7ed692ab2271e3f30a430260c0dacb79a9888
parent 50fc1cf497fe4d0c7ef889687aeb0b96d8ca1e43
author Zach Kelling <z@zeekay.io> 1778045579 -0700
committer Zach Kelling <z@zeekay.io> 1778045579 -0700

ci(docker): amd64-only â€” multi-arch QEMU stalls on heavy SPA + Go
e•R’xýþtree 5fb3ec9b9f4ec2dcf69999cd72a5a5b490e7e538
parent ceaf8cab7645620700b3af2eef5ae970d978b2b7
author Zach Kelling <z@zeekay.io> 1778043245 -0700
committer Zach Kelling <z@zeekay.io> 1778043245 -0700

ci(docker): go build -mod=mod populates go.sum on the fly
ÎÑOÛ™x	öþtree 7b59cbbaa57ea015fe68d85dac4d25345445750b
parent 7844e67ea67becc296ebebb080049d3be66de03c
author Zach Kelling <z@zeekay.io> 1778043048 -0700
committer Zach Kelling <z@zeekay.io> 1778043048 -0700

ci: drop ci.yml â€” Docker workflow handles cgo build via alpine
¼ S“xüþtree a2df3925c59af70f8680d33b1bc8888e18b55537
parent 6b5450579e07bdf21e2f5c5ca73913fe34b83972
author Zach Kelling <z@zeekay.io> 1778042662 -0700
committer Zach Kelling <z@zeekay.io> 1778042662 -0700

ci: ARC has no_new_privs (no sudo); GOSUMDB=off everywhere
mqMÀ�xð ÿtree bad4bf082412b6cb2f5223dd0be204330b9850ee
parent b50a7e1be650ebb6d744d60c5ade23453117ab17
author Zach Kelling <z@zeekay.io> 1778042082 -0700
committer Zach Kelling <z@zeekay.io> 1778042082 -0700

chore: gitignore .claude/ runtime state
›
I˜x÷þtree 860487c5a72962cf6ab735df02dc6c8e3e119326
parent f86302117b8f6e25dd24d17c6677d4df62ed7471
author Zach Kelling <z@zeekay.io> 1778042065 -0700
committer Zach Kelling <z@zeekay.io> 1778042065 -0700

ci: regenerate go.sum at build time (proxy cache inconsistency)
PÖ—xøþtree abf969e54029a2f0e07558ff0fe2fe26203f078e
parent cae33f0789498a01d67a1898b949308edc115814
author Zach Kelling <z@zeekay.io> 1778041088 -0700
committer Zach Kelling <z@zeekay.io> 1778041088 -0700

config: full chain list â€” all Lux native + subnet + external
ýPv™x	öþtree ab972f511d87cebfd24c8fec81cf93ffc1e27d23
parent 598e6f6004afe10dc906e2f146304df023db1744
author Zach Kelling <z@zeekay.io> 1778040880 -0700
committer Zach Kelling <z@zeekay.io> 1778040880 -0700

ci: refresh go.sum (proxy hash drift), install gcc on ARC runner
ŠéQj“#x3Ìýtree 0fc515d6feb88821662d1b110955301bde2fcc95
parent 8698aa8248a73818884fe7d68ccb95cd2145860e
author Zach Kelling <z@zeekay.io> 1778040555 -0700
committer Zach Kelling <z@zeekay.io> 1778040555 -0700

fix(go.sum): align hanzoai/replicate hash with proxy.golang.org

Local go env had GONOSUMDB=...hanzoai/* which made `go mod tidy` use the
GitHub-direct hash (YxjIynlâ€¦). CI fetches from proxy.golang.org which
returns a different hash (5Rl3â€¦) because the proxy cached a slightly
different zip layout. Replace ours with the same value indexer/graph
already use.
A»Í”!xëýtree aa632e6aa6b40cbaced617b1d2f1945e97b3087a
parent 0a97dedfe92493674e17c0b58ed2b2c3b0439c69
author Zach Kelling <z@zeekay.io> 1778040340 -0700
committer Zach Kelling <z@zeekay.io> 1778040340 -0700

ci: align with luxfi org's actual ARC pool (lux-build-linux-amd64)

Mirrors the luxfi/indexer change: switch all workflows from the
nonexistent hanzo-* labels to lux-build-linux-amd64, inline the multi-
arch Docker build (arm64 via QEMU), drop the cross-org reusable, drop
the workflow-sanity job that referenced cross-org actions.
^¢°ÒŸ\xÏ0útree db2b2f2448cf0d23ad32c220273256d39a0a16d4
parent 9fa3cdebd3762216b1d0982ed5c02f7534026d3d
author Zach Kelling <z@zeekay.io> 1778040232 -0700
committer Zach Kelling <z@zeekay.io> 1778040232 -0700

unified explorer: single Go binary with embedded SPA + per-chain graph

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.
.u�›xôþtree e9ef4f120b79eaea5dcdbc67fc3b7a64d84a2f64
parent a648c0b31dae1dbc158ea33ce8e113bf105f2d17
author Zach Kelling <z@zeekay.io> 1778036879 -0700
committer Zach Kelling <z@zeekay.io> 1778036879 -0700

chore(.gitignore): proper Go ignores, drop rebuild trigger comment
-tTy–xf™þtree a1f2c5fd9e7fc38a9e762f7514b9a9a8e6b4a78d
parent dd138bd8aa56ca14f14bf0c9ec64f575da85633c
parent 071cb269f28b479185f3b88ac9b7c1227d8cc529
author Zach Kelling <z@zeekay.io> 1778036368 -0700
committer GitHub <noreply@github.com> 1778036368 -0700

Merge pull request #2 from luxfi/ci/multi-arch-native

ci: build multi-arch (amd64 + arm64) on native runnersðÇog˜xø ÿtree a1f2c5fd9e7fc38a9e762f7514b9a9a8e6b4a78d
parent 8987a18472820f49e45846601ca13727de414f3a
author Zach Kelling <z@zeekay.io> 1778036307 -0700
committer Zach Kelling <z@zeekay.io> 1778036307 -0700

ci: register hanzo runner labels for actionlint
 5KÈŸxï ÿtree 02aed7906e41349de231ca755ff5d96fc6f8daf9
parent 911aac20a13f99ced64e81f223c37f9e6fe95887
author Zach Kelling <z@zeekay.io> 1778035887 -0700
committer Zach Kelling <z@zeekay.io> 1778035887 -0700

ci: use hanzo-* runner labels (sanity)
„‰H\œx,Óþtree d5a98873ed8293956340c809fffad804ae7a3d36
parent d74c06d00beb8ca378a1c533c4aa1f83ebdeaf1e
parent dd138bd8aa56ca14f14bf0c9ec64f575da85633c
author Zach Kelling <z@zeekay.io> 1778034163 -0700
committer Zach Kelling <z@zeekay.io> 1778034163 -0700

merge(main): keep multi-arch + semver-only triggers
ß¯\žx®Qþtree d5a98873ed8293956340c809fffad804ae7a3d36
parent f7d11ba9758f6c02c68190c6d333b87651518536
author Zach Kelling <z@zeekay.io> 1778034119 -0700
committer Zach Kelling <z@zeekay.io> 1778034119 -0700

explorer: remove  branding cross-contamination

Drop the _lqd._tcp / lqd / liquid daemon entry from the mDNS service
discovery list. The lqd daemon is the -side white-label node
binary; the OSS Lux explorer must not advertise it.
s"‹G–x¦Yþtree 0b695143561ce964ded453a18b176ef8db3bddc2
parent dfdb3052c3e00d7fd5c9d874d8cc4ba006a0b130
author Zach Kelling <z@zeekay.io> 1778033309 -0700
committer Zach Kelling <z@zeekay.io> 1778033309 -0700

ci: build multi-arch (amd64 + arm64) on native runners

Drop branch triggers, semver-only tags. Pin lux-build-linux-{amd64,arm64}
ARC runners. Eliminates the amd64-only manifest that crashed Apple Silicon
local k3d pulls.
þDˆ%˜xøþtree 7fd06b2687f41531672d96953f410837b9ed557f
parent 5b7a59a72544f95af0569c362a1bf59f95825a60
author Zach Kelling <z@zeekay.io> 1776997394 -0700
committer GitHub <noreply@github.com> 1776997394 -0700

ci: migrate to canonical docker-build reusable (#1)

- Add docker.yml using hanzoai/.github/.github/workflows/docker-build.yml@main
- Add workflow-sanity.yml to enforce canonical CI contract
- Remove bespoke docker build steps from existing workflows

Refs: hanzoai/.github canonical Docker CI contract.]Q¦·“xóþtree 7fd06b2687f41531672d96953f410837b9ed557f
parent 5b7a59a72544f95af0569c362a1bf59f95825a60
author Zach Kelling <z@zeekay.io> 1776997280 -0700
committer Zach Kelling <z@zeekay.io> 1776997280 -0700

ci: migrate to canonical docker-build reusable

- Add docker.yml using hanzoai/.github/.github/workflows/docker-build.yml@main
- Add workflow-sanity.yml to enforce canonical CI contract
- Remove bespoke docker build steps from existing workflows

Refs: hanzoai/.github canonical Docker CI contract.
…¥)žxþ ÿtree 05ec30403163c08342bb6aadcf78f72d50d2ce55
parent f8f1175eefc2deb79d5d410883e6761a418b092e
author Zach Kelling <z@zeekay.io> 1776374133 -0700
committer Zach Kelling <z@zeekay.io> 1776374133 -0700

ci: docker multi-arch build + semver release workflow
€ MÒœxü ÿtree ae8c1cbf01a3c4ef3b64f9442bedf313a5697b83
parent 31826b92f3b17083149b3521027226ae0719d6a6
author Zach Kelling <z@zeekay.io> 1776077660 -0700
committer Zach Kelling <z@zeekay.io> 1776077660 -0700

fix: use lux-build runners, not generic self-hosted
‰®L}’xíþtree 67410a2f862abb06a0545f03e1f65d5d0e6869e3
parent 6fa377ca541927cee1d0ef682ef213474c1394f5
author Zach Kelling <z@zeekay.io> 1776073381 -0700
committer Zach Kelling <z@zeekay.io> 1776073381 -0700

feat: use info.getChains for single-call discovery (no per-chain probing)
t‹Tgšxåþtree 0ff63ccea78bbb19a3597efae560484f7e17f0da
parent 5a66ed782a87a3677577552d1cd27c55326ac3fe
author Zach Kelling <z@zeekay.io> 1776071785 -0700
committer Zach Kelling <z@zeekay.io> 1776071785 -0700

fix: probe RPC before registering â€” only index chains this node actually tracks
U„Y�šx
õþtree f80df6b97507f299c3c70d1c113711d00044b827
parent 45230c0b273977852c9c30a5213ce27bf43037d8
author Zach Kelling <z@zeekay.io> 1776070690 -0700
committer Zach Kelling <z@zeekay.io> 1776070690 -0700

fix: fully dynamic chain discovery â€” zero hardcoded chain lists
	–Q±™x	öþtree 09f49cf4d3adc4d8da4b7a14c67d34add2510c17
parent cca50859f0645a03a0885679c96d588eda838b20
author Zach Kelling <z@zeekay.io> 1776069740 -0700
committer Zach Kelling <z@zeekay.io> 1776069740 -0700

fix: per-daemon chain sets â€” liquid has evm/dex/fhe, not C/P/X
9NQQŸxÿ  ÿtree 3a06e8dfee3ba02cf9b38c1052b2b0a3bca08186
parent bf584f6b48f9d265640f12d162b47250d18b1699
author Zach Kelling <z@zeekay.io> 1776069577 -0700
committer Zach Kelling <z@zeekay.io> 1776069577 -0700

feat: discover luxd, zood, lqd, hanzod, parsd via mDNS
ÓZMŸ'x€ýtree 453428f8c721029ab777cb81e7d366459207548d
parent ed8fb4b646ba370fab3b53583760b376f53d15b1
author Zach Kelling <z@zeekay.io> 1776069391 -0700
committer Zach Kelling <z@zeekay.io> 1776069391 -0700

feat: runtime chain registry, mDNS discovery, /v1/explorer/realtime

Runtime-mutable chain registry with admin CRUD API, mDNS auto-discovery
of local luxd nodes, and multi-chain WebSocket realtime subscriptions.

Config sources (priority order):
  1. Admin API (POST/PUT/DELETE /v1/explorer/admin/chains)
  2. --config chains.yaml
  3. CHAINS env var
  4. mDNS _luxd._tcp discovery (lowest, never overrides manual)

21 tests, all passing.
2Î˜›xë ÿtree 200ce12a53e92c05fa83f229e3fa078c9b474bce
parent 1a6d559ccfc01cdd2521511839f04334772dc735
author Zach Kelling <z@zeekay.io> 1776010884 -0700
committer Zach Kelling <z@zeekay.io> 1776010884 -0700

ci: trigger combined image rebuild
ÆUFŽ�xòþtree 80e2a7ada6b68d378a94574f2119a05b13b746d3
parent 7ee5150d270ca574c8e8f555a7048da9564df2bf
author Zach Kelling <z@zeekay.io> 1775943083 -0700
committer Zach Kelling <z@zeekay.io> 1775943083 -0700

docs: full README, compose.yml, chains.example.yaml, healthcheck, CI
ÑâQ¬“$xC¼ýtree 7dff837e60515e0be8a2360b832f716818c1a67a
parent cd4378e3f92324cd031ab01fcd584be7242e23d2
author Zach Kelling <z@zeekay.io> 1775940607 -0700
committer Zach Kelling <z@zeekay.io> 1775940607 -0700

feat: include graph engine (subgraph replacement) in combined image

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.
†ß¿_˜x÷þtree 30ec1903c50c309e778ff050ac500e7a227274c6
parent a2b779c20cbdce6ac8edd2ebf2a5e89e307f588f
author Zach Kelling <z@zeekay.io> 1775940007 -0700
committer Zach Kelling <z@zeekay.io> 1775940007 -0700

fix: use pre-built indexer image as base, layer frontend on top
øõQÀ”%xT«ýtree 65df319ce49c0c030046fb802b76864890d0b6bb
author Zach Kelling <z@zeekay.io> 1775939328 -0700
committer Zach Kelling <z@zeekay.io> 1775939328 -0700

feat: combined explorer image (indexer backend + explore frontend)

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.
uùËV¢Ixœ31 ½ôÌ’ŒÒ$†Ãiµ†áKî)è{?•ê?°<ãS¢Ö/C3°’Ìô¼ü¢T¯õbóNo~'³¹K-nÁÙ×NÏp�röquqÕËMaˆàJ> ×\Ýhá/ï§:ÃëÏÇ¦³P£\ò“³S‹Ò2sRv¯o-(³”8›­Ê7UãlìªÏPU>žÎ®~Á®ÿdlÖH[±ìðÝ*ºmiJ_@ÐÉ³Q”xú¹ƒ,”Ž�WŠ= }i»}ïãÿë×Í½y¦ÎÇ¤bÝþŒ'•…ùªÿ¤�|ïŸ‘¾òóà¥ÅP¾‰Ù©`Y•Ä8ÿ}Áò6´â‹iOo÷²Î´ú—P5A®Ž.¾`¿å7«Y'	e·¾8yg¶`óí°ÙeÿM Š’33óŠõR+srRõ*ss¬;û‰ŸØ™›>û¶àÆeÞönƒÙ›œŸ[�_TTöà²ÜW»)šÁ‹Ìí>í<4Ká9ïE¸²¼´Ìt½ô|‡À…×§ð»5üýòÅš{{ÈÙ3…»LÀAŸ––É°ìy—g¦½î¤ÔG›Ò.3m�3åç*w¨@éøü´4�.Â·ÛîÏ´)UþzÈqbkôü¤ÍO�Uå�YžL�˜=¹FÅ¿ç_4kç3“¿…
f0EEùy% 5Gìô~=è-}ä|þóXöUvoKGV“š—RÖúwò�k5’Zxt/å8º…O²}Ž¦,¾$µläÅWúoŽÎHã\äþŽ'C1åÙÕkße jÓóõróSjüº®>rÛà¶Õ¿£ìºe3û�D„ŠâÒ\†¸ó	÷çì>\c´ÞÆx†˜øŒ©3OAUäcdMÈýw:ÑêrôÎD<]æÊæÔ¹úÎ$˜’`ü•Ä_ëÈ5Ê=^nh£VÆwgQ„Ÿø¨’¢ÔÄœ’ÌÜT�²ªÈ¥ç¯_µ;\¿(‰Å„/ýrÏÝ×hÊâ‹‹ÁJ£ä´šX~L¾µMõ%«ÈÃÔ	j­§Ð•ÂÂ¡ãñC£æº‹W_úà\ßóÆý±ïi%¸ÚôÌâ’¢J�²ïùVGwmŒZ™õcÍ¬ûÚnš,�>hÊàFþbyY¼i‡é�(©oÂ{�Å7²A’NqrQfAI1Ã='«%_þíò«³-Wé·méˆÓvÝ5­8µ¨,392~ïÎ²ÿÛàðX-�¥á(ßûcæ¡G¡•$–d&3,Œ<ùþ%ÿÍÏY+6¼�§ª-ùÛfNiÈ¤âü"�I»¼Z³“§™|°CvýÄ‰4‹ºÐ "ÂÕ¤¯xO °ÿ100644 actionlint.yaml !U¡išQà›â‡Fµ°˜!>F40000 workflows 9�Xi2Z±6p›õš9DÉÊúÒPqdé\x æÿOO�;JÓØ|Oøí‘îw
mD´Ñ¡S.ˆ”8	ºx: Åÿself-hosted-runner:
  labels:
    - lux-build-linux-amd64
D8ô¦x& Ùÿ100644 docker.yml 7TÄö,Ô½¤I£(y)©BneO¬øcr·txG¸øname: Docker
on:
  workflow_dispatch:
  push:
    tags: ['v*']

permissions:
  contents: read
  packages: write
  id-token: write

# amd64-only on luxfi's native ARC pool â€” the SPA build (Next.js + 8GB
# heap) + Go cgo-sqlite Docker build under QEMU arm64 emulation pushes
# build time from ~5 min to ~2 hours. Cluster runners are amd64; Mac
# devs use OrbStack which has its own arm64 base image story. arm64
# rebuilds can come back via a separate matrix entry on a native
# arm64 runner â€” not via QEMU.
jobs:
  docker:
    # In-cluster ARC pool (lux-build autoscalingrunnerset in lux-k8s, org-scoped
    # to github.com/luxfi, amd64). The classic [self-hosted, linux, amd64] triple
    # is serviced by fleet arcd hosts that are not reliably available; lux-build
    # scales 0->N on demand and is the canonical luxfi image-build pool.
    runs-on: lux-build
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
        with:
          driver: docker-container
          driver-opts: network=host
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/luxfi/explorer
          flavor: latest=auto
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha,prefix=sha-
      - uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64
          labels: ${{ steps.meta.outputs.labels }}
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha,scope=explorer
          cache-to: type=gha,scope=explorer,mode=max
          provenance: false
          push: true
KmLâàW�Txp�úÇò°# The Dockerfile resolves private luxfi/* modules (graph, indexer, crypto,
      # ...) via go mod, and the GHCR push needs org-wide package write. The
      # default GITHUB_TOKEN is repo-scoped and cannot do either reliably, so
      # resolve a cross-org PAT (org GH_TOKEN, else repo UNIVERSE_PAT â€” both carry
      # write:packages) and use it for BOTH the ghcr login and the BuildKit
      # secret `ghtok`. Same mechanism as luxfi/node's docker.yml.
      - id: pat
        env:
          GH_TOKEN: ${{ secrets.GH_TOKEN }}
          UNIVERSE_PAT: ${{ secrets.UNIVERSE_PAT }}
        run: |
          tok="$GH_TOKEN"; src="GH_TOKEN"
          if [ -z "$tok" ]; then tok="$UNIVERSE_PAT"; src="UNIVERSE_PAT"; fi
          if [ -z "$tok" ]; then
            echo "::error::No GH_TOKEN or UNIVERSE_PAT secret set â€” go mod download / package push will fail on private luxfi/*"
            exit 1
          fi
          echo "Using secret: $src (length=${#tok})"
          echo "::add-mask::$tok"
          { echo= "token<<EOF"; echo "$tok"; echo "EOF"; } >> "$GITHUB_OUTPUT"“œteps.pat.outputs.token“¾œWlabels: |
            org.opencontainers.image.source=https://github.com/luxfi/explorer³On8secrets: |
            ghtok=${{ steps.pat.outputs.token“¯4registry,ref=ghcr.io/luxfi/explorer:buildcache-amd64“à4registry,ref=ghcr.io/luxfi/explorer:buildcache-amd64“;Ñ­7·xœEŽAoÂ0…ïþ–véo`PP%Æ´uÓŽSš˜b‘&Uâû÷K*¦]Þ{Ÿüdû	ŸÙëÈ” c_AE÷Ñ…HVŠî”U(	ÀnÂ•¢î	Ì#¨0É?œep¥¶xñ’Fmú 
ü¹JÓP:µe	[Ò¨k2ÁR•o¥Û8k(­×Ô¶ýns•
‚Ñùë,ÊÍ¼zx.¬Ô)qíMðBwÁ…†�EÈ.a½¯�­,lëÏm]Ò¾~iŽMIo_õ±xÞö>yá�À…>å'�ÓSyrž$ÁÝ®Á|IØ8î°›ØYÔQø¤�$\È™Piä„cv2d±ûÁA_ûÞÑN'®DÇž¤‚_•|¸6xœóññÕËM Fåµ�xõ
÷# Lux Explorer data-layer image (v1.3.0+).
#
# As of v1.3.0 the Go binary is API-only â€” it serves /v1/* (indexer +
# blockscout-compat REST), /health, /envs.js, and per-host brand SVGs.
# Every other path returns 404 and the IngressRoute routes it to the
# per-brand Blockscout FE Deployment (ghcr.io/{brand-org}/explore-{env}).
# Built from luxfi/explore via Dockerfile.branded â€” see ~/work/lux/explore.

ARG GO_VERSION=1.26

# ---- Stage 1: build the unified FE + indexer + graph binary ----
# graph + indexer are consumed as Go modules pinned in go.mod (luxfi/graph
# v1.2.3, luxfi/indexer v1.4.5), NOT cloned as siblings â€” explorer has no
# replace directive, so under -mod=mod the module cache is authoritative.
FROM golang:${GO_VERSION}-alpine AS builder
ARG VERSION=dev
RUN apk add --no-cache gcc musl-dev sqlite-dev git

# luxfi/* modules are private and may be re-tagged; resolve them direct
# (not via proxy/sumdb) and skip checksum-db cross-checks, exactly as the
# node image does. The default GITHUB_TOKEN is repo-scoped; a cross-org PAT
# is injected as the BuildKit secret `ghtok` by docker.yml.
ENV GOPRIVATE=github.com/luxfi/*,github.com/hanzoai/*
ENV GONOSUMCHECK=github.com/luxfi/*
ENV GONOSUMDB=github.com/luxfi/*
ENV GONOPROXY=github.com/luxfi/*
ENV GOFLAGS=-mod=mod

WORKDIR /src
COPY go.mod go.sum ./
# Configure git auth once so both `go mod download` here and the implicit
# fetch during `go build` can reach private luxfi/* modules.
RUN --mount=type=secret,id=ghtok,required=false \
    if [ -s /run/secrets/ghtok ]; then \
        git config --global url."https://x-access-token:$(cat /run/secrets/ghtok)@github.com/".insteadOf "https://github.com/"; \
    fi && \
    go mod download

COPY . .
RUN CGO_ENABLED=1 CGO_CFLAGS="-D_LARGEFILE64_SOURCE" \
    go build -trimpath \
      -ldflags="-s -w -X main.version=${VERSION}" \
      -o /out/explorer .

# ---- Stage 2: runtime ----
FROM alpine:3.21
RUN apk add --no-cache ca-certificates sqlite-libs wget
COPY --from=builder /out/explorer /usr/local/bin/explorer
RUN adduser -D -u 65532 explorer
USER explorer
VOLUME /data
ENV DATA_DIR=/data HTTP_ADDR=:8090
EXPOSE 8090
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s \
  CMD wget -qO- http://localhost:8090/health || exit 1
ENTRYPOINT ["explorer"]
¬!í\º_xúúBSD 3-Clause License

Copyright (c) 2024-2026 Lux Industries Inc.
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
   contributors may be used to endorse or promote products derived from
   this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.“ÏÞ¿%x_ ý# Licensing

This repository is licensed under the **BSD 3-Clause License** (see [LICENSE](LICENSE)). It belongs to the **public** tier of the Lux three-tier IP strategy: anyone may use, fork, or redistribute it, including for commercial purposes, subject to the BSD 3-Clause License terms.

For the canonical Lux IP and licensing strategy, see:
<https://github.com/luxfi/.github/blob/main/profile/README.md>

For commercial inquiries that go beyond the public tier (e.g. private moat acceleration kernels, Eco-tier patent-protected primitives outside Authorized Networks), contact `licensing@lux.network`.
À…Õ�¶¼xœ�XÛrÛÈ}çWtÙU)’&JŠm‰koE–hGYYâ’’ý�J`HÌ
À`g”h—SyÊ¤ò/yÏ§äKrzÀ›äÝÄ^_J0ÓÓ—sN÷ø1É»*×FšV«Û½4óa·Ky}7SDÿþQ·;J´]Z'‹Õ—õû±p¿ŠÂk+�Ex«ÍMˆ¡ß®GÞ°š«’—gÎUv†så²:]<XàK«õø1�*›è…4ËVë*S–f*—ÔŽNÎ�¯OGA‘FÂ[—IJD©K•ˆœÄ\–®?‰*çd¤HùEççoy=/d—E®Êrš”èºJ…“$áŽ4TŠB’(SŠµËÈ:±$UbK™Þ§÷X$yk®õ<2¾ö)šŒŽOßz§è?û'eu!ÊµÃBÉ[j«UFZ¸ØáM•Hnàpð“ÕeD!E'ÂÌuàt‘ûÇjYý“LÜö>zuH.Êy�ýô;JeeÙbÐdÕWb–ë[6KOÎÈÖþH^•êdýAÞ9Y¦2%~wßAwªâ¼Éd¥m«5•Òç[›y?—™oS+Ü¯a½f¦�ß>«ói]àm–¤gdwÏñùW¥“¦ÏÏûX&JÚ¦
×¥š)8½†Åu™28öûÏúüy
ÿïÃŠž4°çFTÙæiõ™þ�çÒ­éòbD§ß–ÜlU0 ËE@Ç&É”CMj#‡\ún÷Á9mÇ`eÏ;€;'™c~£)V¥@¸QR¤Ã�ëQ@gE¥�³-By’B!ð;¬ò9ùò³�-å*6ÂøM8iI—Gƒhˆ—Dábom0üdózþ9ìÒæ{ú*×ÉXW»>©„C]$ýizyAÇã³�ìÚÄ'[ÇþÅç°1ò†~<'ä¬Ÿdüá­WmŒ¬3‚¤ª|èÉ¤.�FÎ•uHÞÉäúôK zîWïZx/ã)¢‘µžNGÈÐÐ“2möÓÿúÅûGE,S¦Æt|L6Sµg"Ïc�™Í—Ø9+À¿!
“%&Pú�’÷‚ý`ï0ú˜ì ã„ÀÇ	‰ª¢ö¶Ì˜›N°®'VãL£’ÌûÄ”`é²�ƒ¬Je"%ºtH ‰òI–Åm¬Ón÷;Ø±0ßèg)SÖ«Ÿ…*IºÒü§¬· ¶,œ¬¹&0jYÒ¡èp[ä™¶®Áœ§;´UT¥DÞ‘ÊvR>åK6QÛMPUH(•ê¯Ž^€pß+ß"bË�àÿ%ýkÿé³ašÆ{Oãdÿp6;8Lž?Û;Š“Áa:;:8x.ûñÁ¾œ>Kâ£££q%ñAòûçOEúüi*ï­‘Œ6*¤�ïþ9]ÓûâòŠÛŠ¬„áN²U�€ÎÑh<à¼¦ÜËç*™¾fy—�7”Y“H–è›HjÅ5«Äm‰<ÆË¦ uÅ­Å"Ñ·™Ûý^ûç¿ÞÃ@–ü…Ú8SËh˜i–KQät¢Ë™š¿UÀmVR.ç"�J!òþF}¶1QÛ£hˆV_!ìM,=O‹Vó:¥Ñ»·Ê_,áZ·›Ê\:	ðAñønâBê ïMPu#î­M¦vÚj�!Ò#¹Gû[˜Û^2UTâó¬-·V@°Ýc’6„Yu·ržËþªè«Ò×ó£ªhf`Â±#ˆÆ"�}täDZ´Êd®éõë³N“µ�³L‹X¢!©Üqq¹�l©ã2�Ö0W9ÒûÆ'5f	Û¸oéì¢?ž\žŒ¦SjKÆºÒ7ŠAÄcM�J�òÇ+w:¾?´dc%ûx§˜Zº…c
Þ¢ëyv¯d¢ü¨…
lDí×*†ï‹$h&¬cBwØ{ß­}*|¹LÐ:O¯úï/'?ýWöš6^s5™FÞå0Z„ÜÚPÛ•Ø)ÃCYŽ
BÌ+VØÒA%¼µh¶E<w9•€÷€Ãëì~Àç¨G	ó¬ï–•Ü®ìø1«…ªôèFÊŠKß¨oKúãÕÕ¸�™ñmÓ[ÅB4,…j¥~–-ÄŒz\ -©œ‰ul^sMÓÕÖ<PA­ÆPë�î—p�fM	”‰‘ÂñspôlÌ5‡XÇ¬ì‰®ä6,»¬¨;n0È>¼_]OFÓ—VÍ¹¼H¼é�ùµÈÕGž"|¯wJ…ÔùÊñiŽ�a�\4E]émŽ®*A4OÇk„60ÎCBŽÃ˜gêîû°‹‘ñïÿhä¾v*&ÞÆ€Ûrõmoÿy0Àï½á»H>põ¿�`ªÝAØFU•L;˜Ê×Y„'\3È}ÈlUàÊ|°?!8NEå.¤ã’FÜÑR”[RVpuŸÐÉådê‘È*Ådx‹Ç&E�Ü!9ëœ½.î(b�¢•ˆØfqFh”:_Èõ{ŸP8¤KTI×zƒ!"‹¦É]o¥»›ºö6€#?êQ;VÒáàé 3ÊK~ód¿ç“ñÉË'{�5)ãÝLn?C­ÄÍJÇÉ²~2ˆ(�¥\Ôe’5Åg°•¨>sÄ8|ßzïzpéš»«
_ˆ|–¦ Um5»€£÷g“Ñ)2ez7º@˜ißé>~P›S±ÖF�2/¸p‰�r¤¨8J•“Ì†Om™‡À§GÖŸøhøh:š¼;»xóèszlNO¸ýˆ¤Ï‚oDâú@·
Ó4@‡±v%…"]ò=íVá[ÛÈD�gªd±Ælïk8…·2p
p]y$ôÅ|Ž™Sà~Ò\OÏ¦Ç¯ÎG§žñV·Wé¯�Vù
UXòÐeÑ¡¬ƒÛçÆHmhÁôÇsÜ<Ïy¾cT{mSvšZ7‰“N<"TºªsÁRº3®:ðæœfö—CÜ\¼v1ä¢ZÞ!•h¢h~‰b5Õ%ß²Þ
´ai�åˆÿº½Ñ¡;{Ò‡7‡–_ô¤‹I»¹¾3‡¾mæöÞ~ÍVˆ”ûÖãìù–ãR¹øÖÓîo¹XkÜèîõä"€ÛítùC�VXNiç
Ë©_ÿwÉz(Æ	ë‘øþ•¥½¹GŒ�;;w'ê�Í»wÁÄc>l`ýu[›àj£(ŠæÅÏù/{ì+ð…Û_éñ½Í¿Áí/ö£ï¨ç—ÿZßw6ÿ×lÿUÏÿ„­Â*ç‘?xœ;¦~D}ƒS™áfEž &]ÆÀ
 [Å¿bxœ}TËNÛ@]×_qŠvÔ.#Ñ–G€HP’.ºJ&öu2Âö¤3ãºê×ôÃú%=3~(4Šûzîã<®÷éºÚo×¹Ò¬iQÉ<�ƒý`Ÿ¦Š²Z²¡L«‚TÉdT¥&«™�ÈpÎ‰å”�dWLó,“óºY±ø:D…¸ç&ŠÏ9g¢Ê-]ª¨Vnø©{O–)oqqHK-Ö+üOîNð«YäVÆ¾àóÏ�¢qeì•Š’¥Šiº’†ð}X	Kó¥jzÇý8Žç´Ö*­6ñîtF–Ëœq7½¶7"Ï#YFµçáqP2«kI#2(’\.^­Bú}"<Î«í¬E:C<¡+TâYy�y-ÉÒ*_ûR½R«ž�¤]9˜Ô‹@1”Éd%êÄÓ\%÷&Q•�´�<z#�ôµÁŒ\F`b#SÖGôç×ï�tUÒè&ºßž'“˜n;àî‘!Þ°~´+�SS7IXj›š:p)@´¤@)£f,l¥q¦g˜ÝÄýñðäüë0.Òpð� uv1<™~'ÇÏætT›£�4•ÈåOÖAp:ºi!}>îŒ¸ç£±Ž]C¹�6‘÷½æDØ·˜�m_ÃÊÂpÿU¡0âÙs Ç0—LVµ:
PˆU¬eŽ�èÔìâÌ;‡«æH÷dBL�“M>GP)6Ígž]�E èš} YÏ]†nÔ‚øîêöæ{ãÑ–KŒ¡3%@‡šŒi³Â�?4ÞuË)�6CŠ§­å5}h‹u~ï Ô6tqÍ?*”}‚�Åjº¢~’îpM�!šžQÃ:®:fëã-º°æãÀ/Ê‹=iº‘¢Ý}Tk£éÓHâö Á™*c·>n‡Öx±ðtïXÖ%¥œä&D@)©Ì7òiYæòŒ­ dMï åw—È®ù.¥ï¾p²R´çÎØÿ¼`â~“Òk	¼d$Ü'd­ž—Ô¿Ù‚ ÊÖA\4±·ð&ð¦@†.(ÊšYßÌð‡ƒ¿ñ—ïuµÖxešê# Explorer

Single-binary Lux explorer: chain indexer, GraphQL engine, and SPA frontend
in one Go process. Imports [`luxfi/indexer`](https://github.com/luxfi/indexer)
and [`luxfi/graph`](https://github.com/luxfi/graph) as libraries; embeds the
[`luxfi/explore`](https://github.com/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

```bash
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:

```bash
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`](chains.example.yaml) for the full
schema.

```yaml
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/mainnet/ext/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:

1. Mount a different `chains.yaml`.
2. Optionally mount `icon.svg` / `logo.svg` at the path referenced by
   `chains.yaml` -- they are read from disk per-request, not embedded.
3. 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:

```bash
# 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

```bash
# 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)

```bash
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`](https://github.com/luxfi/indexer) -- chain indexer library
- [`luxfi/graph`](https://github.com/luxfi/graph) -- per-chain GraphQL library
- [`luxfi/explore`](https://github.com/luxfi/explore) -- Next.js SPA
ÎÀö³’xÕXÛnÛ8}÷Wšv{±dçÖÆÅ>ØnvS4‰Ý&[tóâPms#‰*)9N‚ ýˆ~a¿d‡¤dQrœ8Ø À:X#ræÌáÌpÈð§„ÅÒ¡s%!u®HÂ�oßaœ…!$T4cš^rq~&S±k’2Ã˜H§²˜�à0›76€Î“�*8âYœ‘àÒÔw¹›[ÓVPCB¤„fÓçñ˜M~·^:�T·?£âÊ „	M%0üã—1œ|<d)…1‚€
¸	HJF·î�³É­$Š	¨Ë�µ]†CçT#^ÀK-ž’Lá�Ì<-¸u_€àYJ¥=�ÊX<&Q•bEc}«)8vAP(x@Rè9‹(ÌAïã™tþ‘ 9(Òƒ‘µê)­à]á×HÑ¥¤S&�_/‘=©œV«s`™P5+caà4…ÃpÕ×Æ4M“	Ñ�Î›Ö^«�6ÞÑ1ÉÂ´%£ž‚Â›I*árJcí� _3*S˜r™Æ¡_Ç<…ˆ¤þªÒ7eB}\pßh�†üÒ�aAœkK�sà¸t‚Ê
‹�†�§Ó Pf:*j`?�úœÅ(üë~'¾Oãtäs¨<ÛØñÇÏPÌ0VF™ÑkõÕ‘³	JC>á¹T}ÕRÄÜ\Êf|²øÿ¡£‚ª Á°e† ¤äWt8�tyÉÐkŒâF.‘
qB‚þvà_ �¨Ÿ:eà‡ÙÜÉgéš¶:°·»µ»gë9EÊëz0Ú”Ó1O¤ûõ½±õ½£³ºº@‹ÖÔöCÆ¤ rw~|ÿ†¿z�bLúÍkEþÂ¼þÉÿÚa•àð5XØÑÔoöUAÓÒeîÒ«ãŽÎ"=B$~~¹ÁˆõûÝ÷Ç£OÃ~§©RIv\—$Ì± ËXêz¾ÛwqÞ­1aE, �‰R\ëTdT¿/"¾”`RîLì4¤F8$‚ú<J°”9pÈ'˜ý‚G¦¨l¦X!‰ ©ªv©ˆM¹JZ­~ÎQz¿õ½çP”.Uwÿ`aèqAÓ·Xxt�€s[ó=ü cåDŽV”pŽ"c‰QÙÖš·Öø(=*'1+MaíÀ$<G’ŠÁdÙjI0Té¢ª"K}–™‚z�mfXšJA¢è•‘á@LÎˆtÀ–UæÕø86±«XÏiRu ° äü¸Š+C{NŽµ<t†‹$KJa		™_‚dËVA*5Hêg‚¥ŒÊW§lÙBÃ˜„’æ^™
›óX–Q;ð«•T¡¾³š¢Ž&˜\Jê¹4´òÈäVOJ„¶k¥Í}ùr[$ÌPÏ²«{=,(ó:”/KP¢Š>Àcq¨}¥ÎÍ‚c�³RÇÑ}2ÝÇàðê8zO†£÷_ë8>>Ž��Á�[sQàMôŸ>ŽÓÇà¸®ã8{2g�ÁqQÇñáÉp|XG¥QÀ{�¢QømÿóÑóŸ×.ØÍÁ5ç¼	‘³üIS¥»±ÍVõ�;[‚³Á`­^ 5×»œªõV+‹ª<?i›¬ì�«·�‚¡¢<N,o¯ý-Bƒg6ÙS_Ût,žKÂ±ùÝÝZÍ÷A÷øl=Æµò:çzúÿ�õ’¨‚÷Ò‘eæ[^{s³Ua^&¾öÚ„ùÉ°¯Ÿ*¬·Û«YÇ	kE9Ž«3^ØªÄ—¶ôP”ã EÛA„ÔsþañX:°½§~V{0ì~:YË¥»îƒš¬Uÿ''°ç³|PGZì5W#V‡�µº¦w
®Ös_çt]iï<eû]),ÕÞj¹ËþÖ¹Wv¶$±_®P�³ÐÁÅ²?ÇcCLÂÅþáá^z¸p4ìãí Þ‘P‘¡:æµ÷»PQ<8	š™ÃŸ‰¹}[TÆÝ=I³zP]@Ôê„!‰®—ƒg9ûhˆƒõ.ÜÖ­¼Ý} *ß?p"Â›7yI’æl«¤Ø–-,ªÆÖ]¤C©MC7Á 6GË’ŠíÍöî=tt?õªt ö¶S˜pØÒþ¸‚ä¤ÀÇ¼@cÒÆ7°E%¶ö=[ø`XÅ¥njÔÝJ¡¡­±R,�HÃ�‰œ^ñXÂy³½sÏ×ëžìßIiv¸˜<µ¯&ÜÜuäÔ’”¨Ú[¯uÀÝÙè‡ÿ9¨]|äš›wôQ÷ô½ÙefBÖ&JÚ»Rï¸ýEÏ^ÂÚÙ]�ªwRÛ˜<é7Õ5§¤4p<“Ø_bi�«�¼“À™ùí‰á¬[ÈÀ>B—0··ÚííÕH»Ÿ»wÔs4´¸,NÁK[¨šú `É/ÑƒÚ“R`–Ó‘‡O‡Õ(S×Xyð7=šÇÌ©—*œ÷ ¬j+²àô,‰Ác�ÉõNóÅ´Ã…Xó¹Œ¸Ýôµ 2OÏ4ö¬Q¹¹îéà¨J€££9É¼�ù1hÝ}5í.Dÿ‡ãå\�*xœ}”]nÓ@ÇßsÞ•‡„²v[ñ¥”VjI‘ Tm€Çvb�íU6»îî:m@È‚p.Ã8	³Îg‹JÉñìú?óÿÍ¬îý>øµõ nmÁ@‹Òš„œƒce’±KLå…uàÈN%Ç¡í¤Î‰‘Ôhgp)<æ²L^Â¨’*£Õ¬±V“l¥ ÿ`hÆÒ€/,a
R;™ßÐM©Œ%õ"ó~þ"KÈ¬Ñ,—@–-MÉ:
ÞH.%ÆRÆ¯JK™¼9Œƒ8„Âû²Ç»{/¢þîv_…ÈEi¬?ŒØ]�f¥”2¬”‡¶6Z¼y3è„Úk.ÞÒÁuaÁ(ø¾•š¬�àœ(ØŒÏúG½÷ýh’F­%”n€4Ž¥]ÈP9‚�ÏLÐ±p×Ò'Û³5˜Xºñz9Á1A='Ë´¬©¤óA;h8™ãSÆfáÏ÷à<zR¡SOG3OñÑñ lÒè+K ŒWeŸgýlÃéÙ‡Oý“¨ qBÝ[’¼P‡µ9Ífí–fÛàæ8“†6Ey´Ž„ÊOÖ�p�îÆ8’qVéÄK£;­¹Öª!]x¹ólg•AÕÆ”#LÆû�Ÿ�¾¬¼�ƒõöíÝ	!„!šo¸–l¶½bñ´�Þq§!„iÚàM©ÜÎ¨á²½hÙÃ€>t3¨-të%—©t*ù…Aÿã½?^<w×ÒÞ²Î%æ	Z/e‹‰<Ã2“�ö†,ë½Wõé†ûSã|nÉ‰fˆÚ'†à®”ô$Ârý8ŸÞùé]X
¹ÙÎ†ç ÎñxLø4Ô2)Pj�9kçè�M”òÑeÚj•tN›O´r¦ÙÀj¥)+Å�H7^:¥¶»zž»Ú×	GÊCŠGèè¢ª­
kk±ùþ+Åzç'aq~Êöa"s‹aª\xÃ4³2áû•šEwù‡rï¡ú|O±^bU}=Ï/zÇ]q«6†ªàHÜè~»“,€ü‡ã=É_n$çÜÉß|7¼~{48ù_ëdßZ6kÙæ…xœ»j{ÞvÃeI¦2ÃÍ×$3•Ëõ‹3Óõµ4RõÒõô¡©‰9%: þäTé@õ“dB@Ú¦ËØ1 [¤ê¼xUQAnƒ0¼óŠUîà[K¢€RÔ"ŠÒ#rÌ6A1¶eJ_pj/«ÑÎxwfmÑ-GK µPÍŒ.}+šàJ:…Ès �âw4_­@
ÉŠŸ$3W?ppNË�¬È)48x�VÆ­|›—íë–ÎeãéA‰¾{8šŸ‡«¯°aŽQ2×ç¢"Âo¬•6Â‘uZ`ôÃ:A	:N–8‹Â3Fù§(‡Ö(Ù¡tKÄÊÓ¾NóäTdyßœÓ–Ât‰~Œ$ºoeîÄ¡už¦;rádOŒæ«›<­>‹ò=~ŠÖ~²«vu’•ñóoUuªwIRÆ>þ*?‡ú˜žÓcÜà¥¿ú¾™ö23ý@/ZZ§´Æ&þœkIü8Tð•ÄŠGä�%x„ {ÿÜ¿�timage: ghcr.io/luxfi/explorer:‘lžWDATA_DIR: /data
      HTTP_ADDR: :8090
      EXPLORER_CONFIG: /etc/explorer/chains.yaml“¤8)U)ý¿»xœ•YënÇþ->Å„ˆ[*áÅŽƒqb¾W…ã¨–�Ti¸;\N´»³žYR¢ýÕ(ú„y’~çÌÌÞD;¨‚˜Ü™s¿Ÿe%“K™)QH]ŽFº¨Œ­Ådt0^õÆÑ¿Veêº�ð53Õe6×åb'‹|¾½?ŽF‹…xjÊ•Î„v¢^+Q›j–«­Êðrc•	l¬¬µ)EndªR±²¦ÉÌÝœ	Žê]¥"1WÛMR‹�£ƒg²–Ï´ü‡c]f"þ]âƒq
�óTÛñÅèà¯oß?NSûðu]Wç0RCª$0ØïÄ3Ð×l•ÍåNà@¬Œ'Ç�ª¨ê�xˆÏ¥JI?Sæ»ÑÁ+Ëô™ZÉM^~CòK:=O=‰ôZÕWÆ^::=�=œ2€øS¶a yzÆ�ÁŠÜ›™ÕUv«åÁãC€Žê†S€ß°`Ñ›Êû[—³Ê\;ñ$7É¥KÌ¦žY'"@Éš@‰ÖÅ¬–™«•¾Ë�ÎSa7^:‘D]ªYjõV•â­¹ÔxVÉÔ��Ó)¢k�03%ÓZêRÚÝ\¼…tEUÖ‚¨ æ,BÐ:E²]k¡d²ª”Ë¾	¢‰M™*K¤²Ò‹+«VúúÑâ+ñû¿ÿ#(L,÷¾ùËü.þ»÷àGÊ�GsqT²âÁcbRšröâÅÑ¡WˆÈ•r–œ¹Ü”É<Éõ@sJD‘¤U)
³)k@LœR�Õæ™9)1°›àô<h’¯�Ž”wWºNÖsñëv\ÉL'AÜC°ìøŒ‰Ds€bÝÖAZ ’¨;:�2þþ¯ÿ2£&·q§®eRS°ƒÝ½4³³UÁCB×bËüJîœX*UÎGQò¥1y½à
ÔNÜ‰\»:x°mAê¹øÙ¦^m­Bé‘eýÇéÆÁNx,P a(CÇþ8ÚiüZ
öoóåô,|í'‰6L”¶"Pÿ0?úNÞçc’%:qÖ¸ëRíÄDÍ³¹;�—-RÄŽ§‚K–"Vk€ç*©½§¯ÖFÌ7×çÑ�çìñóK0y„©í®2šòˆóa 6ž·S5ò3ó”*éÌW#˜jØ‰eå3Ö(O7f':ïÞ¼z½Á¬ G>ì%oÌŸŸDa2ÈP¨öÜWÜG}W² “®=„4&Iú’zªAVj ÇÔ
ƒ´ð™Ì×ÆÕ‚¤7;àÏpðUÉ|VøÔÍ}fFñdS)4:HRBØŠÒ48²›øÄšmÅ€Yº�‹¤¢¾|süô3¢Roê‹›!Š:‰*”mò“t ÐýA\)‘Q#�¡B]—I´ƒrktÊ$“#ÐÜ]×/Q…¯ïAŸVÖVŸÌVIWjõKéˆŽ1'gÆÔtJD1ÍÁw¤Ý³'³%¦˜¶¢ûØœpøOE£kî}3™e˜ddm,$|,Üû\×>/•ª/�ÍcÑ
…1_ÍÀº	•FÕˆ3q@^¹N›ëÝTlµÛÈ\ÀwO«@ž11‹r9ƒôJ+K¾îêÝ�Ì4ÜœolŒëfm2Hë|G„Ë4ø‚a[ãÔ�R¢�øèT¬ F[{¿ú\ñõÉÎA[
S‘íAÞÊ«¦4“P¡lFM¨¬[òo'?¿TYûž`ºaHÑÎ_f®R	l�ø
³T;ƒjCsÙ"]úÚØ²:õv9“å®�SüuS~»“R.ƒèÑ«¢Ê7ŽRoæ“�«M¡?ð`ÊƒÑ‘i�!ásÙÝ¢HKjã;ÎgÄÆE«»µ¬�%pcž6
œä›lAUhÑÈtôl�è_¼E�_D¯PóoŒJeVVU¾ó~"kyi€àq™hq}“aþ×<öŽ®âmç Aâ¨£ê7gJX7ãfÖ¤³‹PÈ÷bq=`ÅÔäDÁwßŠˆÅzœëtÜÁjÎ€IFÙÏ�Tòã3ÂÕ¶˜ŠTfSDu©$Î™\–rŠü­t°)å
ƒŠ@Eo?ÔŸ–C`Ag�ë×ñ	¤+×Á	HW~Û“]±DNpH ¡.|¬cäàO�<£ŽÐÃªps^ø«qÀê�±%ž=ÿgd®¸™¢™%¦¨br÷ú{ü6ÛÉ#*êŒBKjKýq5h<ëAWæN�ˆ�|1®2]ÄÎzsb66Q{ã’oÆ=+Í¼’>è§P¶¨ëÈÈ©OSì¥GA¥ðÙT�@4h<Ež×l†hÉÛî¥•ÕÌù³!Õ¨–Ñq—TsxqV? |5\û<)^ûö‘^P’˜KE+žÿ|¡dÍëW“)|Ü£ÒexKåÐVTÈ-ú–kd©Ör«ánwý2ÊÎ�H[zŽ1a_°[jHýYWç:Ü�EôÎaW\Îtžpïß¥×'4¬òsm|¥i·tÌ±Kº#[†ˆj÷X¡ïäÎ^Ûvù;ÚòfÉ.vA÷>j«y›N!'Øãýäi¤ëdÓI¤O‹FøÞhï¢¹šƒ=:5È¼±u •ê´Õ_!;ðöÞ‚ñýÀDýàfñ1²šúÍb.N<"­}ºæ©‘×Þzccªè¬ÛÝd½Þ,ç(J,+ø¨2Cáž¿B{{B€?‘Šú¦ÔîJV³í7SO,<Þï\}‹r¯’�Õ5Ö„©X­JC�Ì+o4¡Š93{ŸwÛec¢Öm±ëíÙ`Dã®Øì‚	ÀÞ¢ã&òüãÅ`Ñ½ÃPðnô5#Ám8NLSºÅ»#Áµ‚„pkZE`m‚?9~ŒÍñ¼xe2Ã£ƒÎÃz›â'	Ìó—ó-È;œ¿ß(lÎðÂ‚åp'~do"Zˆ+øëÙˆ—j~ÕAŒ‰_xã­ë%¿mÚÏ˜WômÜ¯}ÔT÷ã÷›jÛhûø�“mñ©É�àK¾9Oè*úA¶Ð}2dÎÔTo‰¡qsNî„ÊŒPÈøûQrÜìE!.´|‚meéì�Íû‚×ý$˜ë€�Iøô^\LîÂ+ßYb´‰µÎÖ9þGÁ�™×ÔIB8È´qŽRzgÙ—snÚBOžÍssõ
Cú-
’nÎùÝLôk„ôhê{IpÏè‘XFØ=ÆŠïhiYBº¢,6/È¢‰(™0ª^ò+Lö0Ux‹^×)L‰µ¶z%—*¿íO:íD�¦ÑÌÐ;ý!xÊ§­QÃsgz�“û`J�Iþ¨-QÔ!Že�¦]¢ ¯:ù:EwŽ4…Õ•M°ÁbwªÖ²Ü`UÄÆ÷µXï*Zuï†;_TXj'WôÒŒws~„ÍšºSLÐ’¸3òÒÄum‡­¶àš� ÛJÛ“ì¡ð¿`ÌÂ®÷ÔÆ“‹žÊÙ‡»³ïÏÂçììãÝéw÷o¾¼ð?iPW
•ýÜù¡bJ¾•ôŽäËç¯9ÿåñ—¼_O¦,†�Í�Â�ì0­6eÒ¡9ño¿Ø;‡bò4Î¶Ö{Hþ¦µŸŸÅƒ‡Â¸ùÈ@•„GzÅ—_<¥Î	áÀ*$H~=ùxƒ–XÔóçDp5sÍ¿ãˆ;Wã)Š‰ƒÐÍè€–¬²€ÚÐcþUæ]YH‹E7Ÿx¡þXZ(þö¨Oò§ñR;ô�L‘(óð‹nOõÙœ696ÈsöÀäÖí”._ªkÂá-l¬tŸF&ßõpa‡/:�4ÿI"GOØgTZ´Ùm`Ž¡Ó;éÙÇÇ‡¸óVÑS±‡*hÞ°•a€LÉèHÁÿ·QJ™ç•xœÛo¾×|/3S™áfæù`z¯( \íò¯xœ340031QpN,JÏ×ËÉOÎfX~yÓ~Á	Ÿ3cÂùÛ'Ý_VbË>a‡!²ª’üÜONñÌBË¾Û¿&ö-ŒKý¿µÉ°ðTU�«£‹¯«^n
CæË³öÕvQî[î�Mç1h-K¶ì21 …â¢d†Èû^š;ýrú�{d“·þú¸ýÄŽ$ G8­½ˆ3xœÌ½[sI’&úÞ¿BVó¸uÜ/k¶vŽÙy:ïû66&óˆð�Ð"	 –Jýë÷ó@‰L�ªJÌNwU‰ A*=.îßç×ûô¿¿­wŸúú�?áOzÙoi¿®ôððóÓÿû•ŸxK{nŸÊÏOÿm¿n>ÿíß>ýÿ{ùèÓ<íù©áÛ}³ýôHO/ôð‰Ûz¿~úúùo¿óv·Þ<}ú_ŸÜßþöïÿþLõ;}åÿø�¿=Ñ#ãÝß¨î×¬ê¦qýíìÓ¿©Ïþ³ùío»ÍË¶Üò×õn¿ýù?¾í÷Ï»ÿù�|]ï¿½”Ïuóø�íËn¿z §¯ÿ¨ò¤»ÏëÍj�gúã·¿Õo\¿ï^åWø‹"£sJU—žb¬ÝEŸ¹ûÖˆªrU×Ü9©T³v†k‰©PŽ-G2‰~û[ãg‘õ©®y‡ßøïûô[Yïû}Ý}2ŸÃgõÛßå­Ÿx†á«þ²Ùòòmùâ�ÝúéûðÆ#?ÖoÛáËçõÓêy»ù'W³Þ~`¿ù¾Þ¼}µzÙ¯/·T±Äøú?n¯ìv7ZØðÙ/¼°ÊºÐ°¦=Sœ.Î'nV;ÏLXÈB)™šQ%V-4l1ª9ïS°QM.ìáùEÞÃZ^ÿà2¼j¼]ÿÎ_Çûº>›¯ÃŸ›§Ê_*?ÞÝ=âPÿŽ³öÎ²ÉZ\,›ýœ°»Ë.›%Éô�-åÚp6}ê¦¨â[¦ÐbÄ‘KÞû±j­”jqLSò&­›2·–íp¡Þ–m»?{±ãíïëÊgïœ­ò7Ú}û¤ ­Ö‡ÃL;ïóùøÎÄ‰ßnöëÑá¾Úo‡ôzÇðÔ›†o~ÙïÊ–ÊL_›o‡÷‡]9}AÛ¿¾ht¼/ëý†û�åÁÖ®öxÔã‰€F[ÕoôôÄ‡Óð¸~<üÔ3cWŸö«Ó3ÍßÇ-=µÃIúFz|¤>t]?ýö¯Ý¾½w ©n7ã›k>»…� +=ÜTULOÍSC¢NxÓpÂ9Ì�)è\¡2©&ëšñ¾k´MÁûZ&�à¾lŽ«µûù„S¢>Góž¼XèÝ~$®¾¼q�nJÒ‚}ý¾«›—ý?Î~Õ¿åZ]$ÒÐá%ØÊø"AiS¢Ö¸E1�Ó7.Ñëq»T=¯on†ÖŸóA\Þ6~ûêË?w›§³—?ðÐÇ³ñ´~W¿}U^Öí¿Íº<oy¿ÿùüÀP
¢>uZ‰z8²æmeŽÏþözËý—ixçìŒŒí'=>\,0
o·›íÇnòýß\¹Ëº¼#x®ÏZå÷$Ûâ‘x{…“ìÂJAÛf!GpÕëpÓcñ‘£-ä³…BÐ^—DN“nÆ6­¬f.¹G£dCOÓ8éÒÔþuµî—÷	�ŽGyýêMé¾‘¢ží¥*�åR‹[oã¸‘cC�•3„/½§êl"C¬¼i†œ×žl‹NkÖdCÊ.dã�š^¥+Ëw2(7Åƒ>:0‹K\°½
†‚Sa_€N¨‡Úmµ¤R·Ü»	)©NÔ’)TBÌÕ`%‚WµøªöÃødQ¿ZÚÇõæ¿ìp—yoÆÖùcçèô$—+«g&¶à\d§{Ò©öVc…®)™–2²ÇI’#&&9DU¨ŽÍû¬S¶6ÚifrµTÏ´;^¨	hssë‰‡¡±^`Ýjë]»ØVãR-ë«u2&zEWÌælc@1ÝµÜS‰Ê·éu@}?è;Ï²¬›¢‹Õ?ÜÝ�ŒTc‚¯}©Ž¸
Áf_"VÅšäS5Æ
×ƒ7‘|�rï¿@F˜æh%ÏˆÂÁÄÜ`ß½I%Në7<ÄW~š!s„áÌDÔn.ŒF¼« nR„¯—Äà’=N3‚÷­Ö$|Ù>ÏÎíšF\(­Ó³à>pX_ûòÐÚÅm‡ú±I±‰uYXk§®[Ó£W8¦–\¡¨BT1y $*ÁÎeÓ‚1eú¶^ÂÛ¸écÜBV¿çñÊ²ô’dÜRmb3Š|SâKª™*°QÈdx…"ëâyN©ûP#¾�%4öÆ’œÈÇµRŸ:“"-ï¿ñËîÆÚ´¶5ë'“L³øº`Û=€ƒä«ª19ÇÙ%¬�k�*8àA­Å5h{—°^l²ÎŒ.ªâôQùº~\³â=Œ ‘¾ƒáÞ1JåjŠEƒM—f[NPäâÄI½pÏ©r+0ïRrqwÇ’õk?ýð¢$Gû?§¥í­ÀˆÈXbJÙG«2³¯ÎS°â°ÄÚ•âµŠÖ4 ;×;éŒo€ª€�émá½øHpÛ®õçQž/ÃCÜØº	és±°ï#å†{§))§åbqè
É¹ë¾jm .Q=�
ó‹µˆ®Q%+=ÍóÎŒÖ¯¬Ä§ßþÅÐt›çŸŸd§­¿µ:¹ý´[ã'/Ï·^œ
&@Ðf�Å(d5ìÕ5´ÀJë9V¯¢R™¼)¶jà3@¸ØC©%ë0¹F¯>ï9	 ÚVO›ÕnßÖå
}/í«5–¢ƒájsƒ’*ºZÅXzäÎÊ[_¸6Á{‹ó]ˆqŽr£kª¡Ø¹K<ˆ1!Ãà8YÚô¸^’àc§k4ÚdÆa-±)kptsI6ERàò°O@˜Eåàn±†¥
,gÊôŒ6âæŽý\	ÀYQYßäÌ8]¿b»û‡X¶ÿgË¿ÿ/›S‹lþíøg,3ùc¨º„’:›š‹Jº›¬â4	:üÚçíúq½n<"ÓáÍÝæaµÿùÌ«Á•¼½…ÙnŠ}öËÿÛ	¾}x�ìTý„³õíˆ^ÇÀúÛËni›.ž¨_úÃ·/ë§ýØ[³~ú¹úÎµÒ-mÿú —«g?Ç…ï9[tŠÊx¥-ô:T~mŒ›í­-LWÜC×)XnÕCRŠ”æ`¬#¥fàÙvK?O¸ý´Ì7EŸ»ÿF‡æÇúéióã† Om»Y·Õþ_�ötå\]ÜBgüÏiˆâ :¬.J•ª­¸”T ¨ò:4ˆgaÀkäb€¥3´yq�»šSÌ)¾ì~îöüøEÎ3o÷ë«û«”&�Ú9·ÒœVÎR51Ð«ºá^SŽÚ°)XP¨e‚�ÐÑ:2È89 ™ož~~Ã–Ž�p
K;mÓM9Ü•”Ásí^n—1d{±>9Á–=ø.{ä»óŽ›30³¸YÁ6šÛ«í÷•88Fªaq†R,îK­N3GÒÀ¯›ê£'C-¢¶Ò¦J#Ó«ÅI´¤\r 4¾µiœsxöí?žùïoo/£÷¡Öô°þ�ßß·³wEMbp†—O/�«²þzÒ¾ò¸s½ß�|~²õËiå×ÙWà‰¿òÆ	šÚ·8‚áš\$¨,Õ�£%åR¶ÞëS-ŽH" ëTŠ¡Y¯[§âð©‘ƒ+x¾òÌS[pþþù\¼?l�›Þ‚¶þÊ‡H ÀûÑXB£l6»Ã›þ/íÔéoýàN‰´w¿0­(Ó,.zÁá†;åÀ|Å×¯‰…Æ—êœÏ”{ÖÚ&ì ôXL¡Šè�ã÷ƒbsâ.8-”ŒŠÄ¿a„B‹ÙFÛ#[pCPFKíÂÒ¡È”<•VÀ”Ü´ãÏ‰;Ø¼Ç›!|R­öUyÀ¡ÌÜ[×Pí$*2ô¤H3n¥ï@Æ˜BÕ¦2“œrû,¸ËŸ€H…a«Ãhá¹:�ö™JÌ*×`lÉ½ÕbHÃ‚`É¥a¯�ÓÁ¯w4Â_ŒŸ«³»ŸÝB5rN UX«žÄab9ÂlZjUáaÕ²'Êâ&ÑãÍeCÅà'âLÆµñ;©Ú<¼ñ'D_þhP+±à²+šÝL7¦§\š¯TC¨¨ÎEgœª%kÑ@z:4•'à£™¸èµÑ¹²2ÇçÖRìÇ)$÷Ø &•�ÉµTY‹Tt‹Š•êP%T™åõÙV&ZfrŸ4¾:Žuö~œ]ˆ#Ùü%I—ƒgÁ%]‹ß2”€V8µêª‚¤®R‡�À6(Èª¸�"@A5±x�æ5Á/HzäœcñÒž± 
¼Ó½P/5öd¸iUU�ü�^š©N‰yï�ZkÉ	€r,`.ÂW¦Ÿú«®¶\_†§¿òb.Í#mqÖÉ´l©w<kö¬²`ï¼Ž5kŸ2eèpí¡Ðv¶XSee¢«3´ë/F¦†%€\L×`miÚY›­eAŠƒ&\ÓÎâÁDÉ�Œ6èf­B1}V…‚¢P¼É]ÙŒ�ûÝ|&ÁjýøüðáøÔ–äðï½.:p0PU0SUH¶‰ÞXí\õÍ ¼vgsìµÚJà{Pð=�Ï×�xIé<íZâ\šàÊ‘–¾:Ì@sÕdä¼3Áfmí�òÅ‹Ys÷Áh waÕAÁ\õ®U¼‡ü/ûÍ—«Í×Ÿ—JÚšb‡÷sø¿ÅfG(hïX¿ÕÔ•«9ÄH¤´³¡Eg(ðNÂ×þu$úò¶ZA?°Byû‚?¢IWÎ­]ŽÚ�uµ­`LÅÛ5ÁØAÙ7 ™Pfýj¼\ÇØÍâûV’ÉìØš’5ã*6 Nê€R Y…è»OÀ[7ú­Ãji”Ð¸ºÊL²ÌÙµ(?DyÓa¯ï²fäP¿Ê/¹ÈR_•Mûyxõóùdxõ¤?Ò¾~;þ�g… &-kw\øq‚}ù±¥çÓß¾ßü8ÿ
+þóâõ[JÏÜ)}]›±~^~Dè`ò¸ÆÅ¬]o Ü=†„MÌ&Øà´é¥·`]mÎÃÄ“…¢Ž®Á˜McÊñVÿúN¾îÏxÕÿä‚‚2—„Äë�KÇs}­Æš�q/®zSÐs†£ôõ  åÀlJz¯¶ J êN�Ö°šq²½f’Q¥:Ž3Ô²pOë}Ùü±>žÍMùç)|ðy­?B„‡›k%å(cÓh‡Œ*Ð§`¢…èÊd€éKl!åœk©`Õ8 -ÔÎ*7¼[+£6 “œfTääã½x¨+7\‘;ž¡âS5«UÛƒõ�TÄ]ÒöÄœu�ÞúPl®”KÔQ’LéñÍâ«M±Æã!ƒñš’MšHvŠ-”¸éPíX$x•záZrËºûèYõRðþÜã¯÷¸�c`µ|½$D´FÅ«Ð­Tº4WI5M­`T¸~‰%x›jÙC‘•ÆÃ;j2Wh¸ºY¹vúÀØ/-ð_Ï%�Ž™¡ ¢©7¡B¦\,¸°’Ò n¸YRÁ3(°Âõ€&�¨Ø¯ùÍŒìúYÚ]SÆB[�d,âþ:ŠæÅÖÚ}ö5›¬<ð_•oU§†�²’:1©š|~s‡,Ã¢V1Ø%„³c{õDK¯Zbàtâ¹*Ü’S§Dà~Õm{€zjs9gxþñéÑwHòÖ”Ãšj€ÐrQÚi�Óè
À\t=CÁÄ*¶ƒŒY«¡ÀÀÍ¢ÁñÂÓsž¶éýåiÿóè8ië—CÆÕžÆûÇÏÝ¸4Rë³*/€÷ãH¼^>ÓÈª˜j|)Á Ì(L¯c'›”UªÈ[übØNh¸zB¥yeiÆI:h¯ëjð
½#éPÕtB$xà—'<Þ„à‹§ö±ÇfÈ‚ZÅÈ R)¹î‹èyã²JÉÂ�Ð8^ê®‹Å¹Õ’Öo;ng¸o§
„G©¿Ã[‡Pø úÞãX
²ºüñºyêë¯ç°ð·ë,¾iÏüÈXqÌæ£«öâ«å­|?}~­—{ËÏ½‘)~^Nz(¼¬$==ûìƒœ>°{)»º]—#0}yYßp/Þ\œqZæò¹Â
7ØXqƒS…¬2 ü•Ta×T„…EŒ7¥4�›¢8w ²Ì±·�ØPémð…ÿ¨/;ÉzêLo›|Æ°®Vü�•;ÔAŸ¯ôÿâkE3Tyi`¾ù¤AxU¢’ 0cKÆ[°Œ±¨ä{£Útí–-EÕ3]µ{�ì7JaîÍ  p:žöëµ»UŽ9õáËª„å „@ÅìÅ�è$a9m’‚>mŽ<leg0e!ç”#A$Uw¥Ú0íÕùÀÒÌ-ÁËã3=lF¥SzyP 6œCÔP™:%à[’‹ªøìb	bs1€î(Æ¶•bµâ²¤½*-TyúùAŽW»1•FÚúi¬ÕR•z®`¹R "KÞÀZZñê„ZUƒ}L4ü‹½Ì6ÄtÐÿc‰Ó~KŒ4ë¸ À/ëMuP@§�W‚$u¹„K¨ÀT|·ÞvÍÅEP¬C¹…Øƒ›Çƒ“âòáãòVZÒè¼mEÅ–¡¦ ”g˜Œ‰3 r©]Ö ,…0.“ŸŽùŸŒÛÜy+úÿÅY%h;Ô€e
ÅäÖqQ4N—ëBómc-ÉT\ìÐœà0ÝBK@+}cŽÓºá�”Ï:FÔËs}#e5`±tÒ•mÊ‘¤ºDé…VŒÜŒ(gÒr©Å°&Wã³ÖÊÚœëŸ›rVìw;Åðèê3‡Åy�”—±³V|Ô>agTÐ™-DÀ>*fŸ—õ†;æ¸I˜CXƒFNõ6sµê·íæis5¶KWÛ�Ñ=ÛÐp¸p[pæJŒ` Ø�gŒï¸^‰pÝ"ä�ìIÁ˜%Àíf‚p—	¹ƒw™žh%Eƒ«mžÞRÏ‚Ëopóžsóc·ÚÓö+ï%YÍƒ±ÞØé*¾ò¾-Í“Œ�\Vg¿œeÇUªO°�UzáÄGÜÏÚ$ÁKÔ+K©C�„Ï€F}Èñ+(û�˜÷@ÿúùe·—žW‡õ:†gÚkë~ðbn�˜|aý´~¹»XH;‹•üÌÍU=æá�TÅòA¯¡L„Œ#ÀFYÃv:oq§ŒmÒú*ïªdf¸ª c-¶Eª&sÐ’öö^ÍS}~¹€Ò§â‚‹š�w,–Ë°ÿÀq�»³8Ý0Î7ŸÉr2{5:Vè�Ï�M“D?qµ�ª©fFp‹cÉøÐQÍ9SŽË£‹–OšM>×æqø£V-³
U€­÷’¡Á’o o)Ao"qmØÓ±ÙÕXÝ§™ÀdØéµùƒE| ZõÍËÓ8ÇC²·OUÑ¬³gÀ’¥Ô´cA×nD†ÄÃÂøT/ fq	ö/Ø.†˜wbÚÃ0bµû¹ûˆ=œø©1m^:ý»M¦äR³)Ò:¡–Á:ÂÅ†Õ'ÓBW �xlXÖ?8eA±m
"þ´(g÷y\Â¦ïÏg;{6¡w]pVA€?kPIy£*6	t×BÉû Û�L®Ü%Ä#a'~:§ø�­ÚVkú5³¹C’`Ž¸Ž-;	d1i7Vã¶¨…L&zÐUîV‚6Š9¤ÔØ2„ƒÆ™Î5yÕ¼³âmv»"ù4§¶gãÐ‡^<:`AxZÈØÄSèwÖÀg.åPq½l!qdÓ´Ö¶iÉIÂ&DÕÛú4È~“ãÔ^â]�¯[–Èµ[<Á@Çäì�/EqSL`N¹à|r·`çÖ=äF½v@@¨œ¢ ¬9Í¥RÔ­xfÞ½l4RÒ�XÜÒLxSCeØl2ˆDu6K6«ª†�q ÏjÅ>ê$åssqÂºýù¼—2åÇÇ+Õ¯×}@ÓE¯L·k%-eë$¹MQÐ©%mk«Cr®/À91z‚vì&”™«5öªÃþÄ‡†ØÂÌák´}óXlš8Ô—F- 8…š(%Ð?‡äÒL�€væ
m©”bNÁª&Q‡ä°�Êä4]¹|ü/¯i/§7†|¦wÅþ2‘rÙ³×Vê˜EµÀ°3)À5{èq/½JæŒÓÜÌ{ÖÆÞc$úêpx:ý¶?ý~ Oxûˆ<ßÉÀßCÌÇgv].åý©Y‰§�×NÜf¸ ¸*KÍŽÓÍBÆÖ°HD<TÀ/>h@&5�ó¯È¯e·5Ú}{¤Ë2_‡è{Ž)ºX5ˆs¦ØT$)AÐ>ç Fz‰ì¥ÀEA‘A§á.ÁBYˆo“—ãHwŠ²Ýü�Ò,íŽµ]2ùBÏë‰(Ñ3m¿Ë²=lö§¥›[!ÞB:¾NÖ_¼÷Eq¦À_'.ð"‘–T”ƒýŠ€^J(�ïÉÖ’k1©öT%Q!ÂNã‚Y]¦¡Ö³¤;õÇ‘¼³
ºËxùòœW2x[ó½PU1Yhv°	 @¡k°'¡S3Jw…zñ£šáH­6ç¼Ø~ÄŒòg½´ëÉwhL0!0~×¢ŠftÔƒJ“ãÄ^*ƒŽbB€IM0¥{p¥>Óâ‚¦ßV–s�ÒëÞ¯hÜÁ “ck½³:g¥@5l@£vìÑâ€x€½äuÔ€è66­‚à£$t(Ú¹b¿ËëûÖè¸ùHJ &¨Ž¯ÎôxZí›”gÉî+“¬­t[khµ¨0­ó>˜X0)âP£´tJ^MJâ©€ž>Ú…T(hOA¼àÒ<±wgœQØ‡‰J“-šMžqßŸç¬è:/ôÃï«‡õ•ØwÈÕ¬âšÊ`&v"ÒlrÀ½X“{j‘tp	Ð'Ë0|ðÅ0@ñBo°_W½¡•ñâ1'9‡ZyÛLn)œ3G®�ÊÚf!Cr*Kv!Iuœs�4ö1ö¹"Æƒ\Åóô:¨‚K£«1*y_xRUÆ7‹k_›TÌ PdW:¬KË¥‘ƒˆ®ÔXUsu¶ÅyÛÂ1IµKûÆ �g{RE‰—�Àb`ñô]‚îƒMˆ%¾Ø¤ªMg‰"W×ý4]xÏ÷Àÿùÿ <}÷œ2ïœMÀ³—?H]vs–K;XiYèUÒ1À^z©ðPîX�.}øæöh»½
ÙÏyq¯—«ÒC?¡b¥½º“U‘ÂBÇŠ»ß´w’¶Z
à¬A£ƒì%£fú¼¦\Ÿ"<»ŸÇèÎ�ªYÞwo’Œ¥;¤¯€�û]­Y¢±ì‘tÁS¶A:äYëJƒîNÁôÆ•½3U[USž.¦—tª-K 4ý2øp	ÇßúuÎ7èüF‡Þç=Ç?ýör³Ð crÕeò*Iúß¸âS‚:"2â4…ê·0`ãz´™�uJŠ\(ØÒšRUÕÃt˜þäq2ÞÖp[]øRüøüðÚIjxñ‹-žF{3Z ·¸> Í5€o¥+'¹É0(0:Ò‚ÓF#ã] °&$6JIÿöÜApºŠ–[K<�w_÷÷Wèµ=ØÙÉ¼}’Ä= �z˜/ìUyXfåºÂ�ÔÔHKÃV#ìÌ¨ã{”’”ì“ò’IÝZµK‡�ÏèÍáù'úy-žJl³Œ¤hE4HÁ	7€ rÊÔÔ´È•ZíZâëŠ42Þí±A°|§g:Ðž÷óz«F<¯šÛ¶·3qï$Iàwìœt¯*UM0Â`5�‰�¼0É)®É$˜Ž’;¤ï[,9`vš¾¿%I©_»IeÎI—¢ä_h·“–Y›§÷Ö£¬÷×ËGWTå¨a4¤ap¥„ŽZð0Pƒ!ÚÀÝ\ÒÅ²
*»Z ß|„vPâý™;É‡™4cüb—ÞO›ö"î7“�Ïp¸Á¼;SPÀù*%	ã¯Û@Ëñ"(Å™÷@Ì3ýÛº®½šÛ²‡íWõq|w—'¬95v¸³ÞAµÀ<E¦Q&Â�K—¢ëî:©L�ãvKÖ™sH{ú_ä¹Ì‰øôûÕ~.ÍTmÏâÐ	,Á8r“%AžÞU’Rké›Ý$ï6™­CJ�!i*	Hgî4n¶¼þú4iE—/“éR»”¹®S‰ðµÜÙ¥�AÓ¤§5 -¸'Kòì q•ª§2�¯t!Â
8kË7Ò©'?}ï¦ƒ
É€�j�¡[!jò°0’º•„û²¢À·0I6š’«NI‹ŒÙNÈòxÞó}œI»xïDmƒqÕaì �pu²uj<¨öÐÑ¨x|¥�†IW±X°
ëf¼à™!s[·¥¯ë‡ëÉ‹Óój¤„(´P¥É¼Æ]’C
.î~EÒ®·Q‘zqÝx_!ªk¥¤Ð¤Ìn·†‚¢{?<‡æÉXyb‹Ûf\³ZHUÖQØ�” Ù`�cš¡*—@ù4Pk¯y.ÛùT°2Ö‹›-`‰²)�™BX¯d@˜‚A•¬)÷�dÔär¢ÂÀ,½DÐhŠ˜”+ÓDäµƒÈÙ ¶Ù¡üW`´íÅ›Ç’™«Y‹§7ö´»|ãXð=w”Gt÷e•àƒàÔ|“®Y¥9òR ^Â5®�ÊJìOº‡‚7‹¢Âåí2²òcÓ`FËóžðSõþ‹KÞºT¼)ÜN'%p`™a5k”�8`oÅ)’×Y»^çbIEq.Ò•Zl.Å÷ê´Ü]ò1ô
"Å:¸àÆY4@8¸åÖHµ ¥(V:ÛZRx=Œ“Ò]h®ök[ø'ôúÚ¹µüB8i	Ë2ú3$-Mñ¸7a*&K%’1†[èÆJªoé,~.Ç…št»íj
E˜Š§ßAŠqêpóJ$xc|Ñ^š�Tï,P2Z`n@ådzÑ³‚.ÔFœÃ4�bÿ×zÅ\\ß»/€¸– §è¬¤áÅ`tÕ5˜�Ù[’Pµ`¿Ò)gJ1ÐñøVl.º^æ<¬øîBXIÓ%®`‚À@Òý€rÏR_ÜmÒ|Mc„„×JªÀ5x ]ŽJòo
1\ºûÑ‚ƒ‚ .·NEñ“ tŠ´ž"E[më"H‘áI[Æ®a³@št›éDúK6wd^OÙA1¸ïM;–7ßHíèFiÝetòÊ§¸4§—¬¬×½Ec¢�y6â0ë&:j
-%cÔ¶Ik’òm—3ˆCÇÉÓnú·”²çU¿M¹ÊÇ]¼mh ¸*—"–€C÷¡®RÅe"ËÕt€‘j{29¨R%¨|(¡»6Ýlª/ÌÚÝ˜äsô3’vy�„µk¸R|ˆ3à6°‡Yz`ÕP�Í›
Êì2 G’â¢ìM1HçÞgË‹¾™k¦¾tdÒ�%½?%<²·¦‘¯OŽ“Asªáè@^Aë`Y{j�{îÒñ¥5ß=Oë„³¾HÇä¸÷§™ÏwPžú‘ža×N­\�×û¯Ì8MÅß³ü$IÉŽª:K`ùC×Hg¥�'Õ)ymp3]¾HözLÒ’,B+Àþƒ M;›�Ãðd¶Ó¯éoˆ(§Ï‚|J¸^�Û‚äj«„cÕ‡QÆ7Éüê£’É‹3�Év�CkƒF»€¯üñôÒ{Úù(U�YúØÕ"�
}PÝ»âÀt¶b, Ø±€´r €¶îi.�ço×û‰±6ËG‚`pÐÊUô‰žrì°ÚþÐh'àQh`ÃIÅ\©ðI¥ðÊÌá“qaÙPÝ¸tH·g¼vƒ…ö5ùÄÄ€ó¾Tpi»Èg/½�Zê)òÞXCñOUŸ¨¹÷á
 &&D!i.—ªd4n»÷¹°± ­!ª‹82g©J0_ÂÚÍÔÆn®!Õ·Íã˜UûÅ�Û¦-v?Ê�‹âH�žJK ˜9H¤“N¶‡„ëò©€à¡é¬‘^ï=Ï´§þ•¼‚ÁÜ½z((�{í#HG”ƒÇ8{œ8(wÕƒäw#íÇ]Ñ²Ô3ˆï½i¥çšn]ÁcóÆ¢ŽOäÒ6?V&AàÐ«*f&§Sƒîä7Ò,�+¸Œ’ùÁØÈØO!ÛÂív1ûù�û÷ï•‘‰F#×öò¥~ª×ªpŠµö¥'…SL&Ð ‰ûÒ¢\Oùž\V†Þô0bRÑ›‚éÊs—Ôð×^‡û÷rYhD§åæi#‡;W»¶;ÃZ†±Ð6)Ñ×E'˜V« Xåîfš}þ¡ÁèFXÞ¥£½iö¿ui5f¢Òq:ƒ‹zIèƒÞW°¾®ú>TáãEè±L’@ûÞEû.ú4Íå±õ^=ÍÓ‹aŸÏ®ó�>jëyÙ!èÝÊí6i(Ïß¼\‡ÅOó/%ûä’aØ¨ÂÒâ¦‚°ÂâL@×—.#i½çH�UèH@!ZKèi�øÖÈvrÍÆhÿœÄ}oU®
ö–ïÏ"Õ`¶I 9—äMÚ©åÁ½fÛ›äe"åt±2cÝ-„ 7bÏYMÃ3µùº6OtÈù9ú2Æ«rñí™Uur¸*£[<î{/,ÞÁ–À¸èž€?+(¾IÒ~'k6š½äº’ù¶€ÒÒ,Eh²»Ù¡bbØÚ�m=S`})úê­¿¿üvhÚðú™´“ñOí4Zü„InÙLþêñÚ.L—i0‘ŒÕÔ­aŸàoh¾™&]º¼O^%é×šmt¶ëPŒU0ï`–y¦:ýFaóY!Û½³ol¬M&ºÆedIÊ¨#�XÎœ¬œmCáeTZ¹jçÌÙº=�'.¯‚uMÎn�n?Pk	kï"è¸IŽ8ç™¬às	½ƒû‚i¥*�s´éÓQò—§µÄøq6Û¡ìôÆÓfû(#iæ÷Nå®{¹.ßSv *ô`0SÐÒßû^¥zÂiA+1ÅÀd°@�\t^J£º™>—0µëýÏÕ®Òå¸-ëT²àâU`&˜vUEß Uó’:˜s* ÀdÕXò;/)„C€Þ$5í¿9¤|ÞíÀaï]¼Ä¥¦$�=p6‰¥�¸-DV"4Mf‚ÊÈEèédzöÞe9ÊÜ±¯YZ@üú=ˆ7d[A�Ãx¿<?L4‰XZ‘jÝ"eÐ9Ü¯;eÌF×\¤°SÆdŠö(£é\ô€Ÿ=è D"mö|—b¾“oóŠø,MÀå¢b›­õþç¤t3°ÃMµRöxKÈD“yÝÐ´�éZ¹¥N½´<�¼OC'®J[Íq@ä{ç`Jzs�‰tl{5Œ�'°¸¦e^»†´ÒÅ¨jgktCY£v`Ä^;évññœê ð¦³ºßªTfj{gÅÞKW×·T¨Ë�Ë÷N¥(­QPJ4´§†B‹6â|gÁñ-%_kÔ&¦\VÆj0°bAËª4ë›3³ÏO£ü\s‡tÏÔ£Á}«¸‹Ö#h‹çwà¼º[v�i�¶�¬ùw–É«=×¹ë¯c6¯«—v�Õ›1Aq2¹€@Ä”$U076=…4*+`¸^8ã|‚Ñ³êF›N7™8±Í²9ù–Ï,d²Œ{i¥ˆ½ 1ñµkªflJöNÅ,p5¨Ö˜vJêJB6`
Óºå}ù6t�O—ÖNwíR´Rßë¥Dƒ®Ôx³vG[Sè™*ÞrVƒ'v Ê ³!û¹ŒÎ·‰cö`—¶z.5I~.…Y—ïÊçä©$ì�!Â%ÚåY*@-˜„–!ŒmZõÝîŒt$ZcX–öN›\5¥mÕD`•hÛÐŽÓÇÊªf ïr®³KW+KR?>@Ào”Ã4tQÃY¥àÝãR¡È\@-�&p™ ÿœñÒ™×©n+‹‘»XežQÃ¿ 2‚NØñ:3|îÔKO¾ørè&ðc}¬ÔØÅãˆµÂ…ÒZçêqÏ¬rP2þWšJÄXû¡�'€¨ª „ XžƒÌÝ“iƒÓt÷¢·àŒ€òl/xkµ�b¸GŽ<�–9v¥H±2Ô|ŽE(‘
¡À!@/9Fíp�ð‰‰1¥Æ-f›æbŽçý(ïÝ°êÞH[Ø*ýð «ˆ(!w¨˜ÜaŠ5tˆ“)8ª†�<À–ÉI…dexêœ¢\®3T–>hY$X+âÚcd©Q©K£ë ²ÉThÉ4l²�”¢Œ(ýâ\5žþ:¿fé®ŽqIR0‡&§YÓ`a-ˆ)ÀmuÙ å«dmÍR•­É¹@öóü7{xÖOß�uh«1,Êâ�£ŽÒ‰´+Ó½µ´Ÿ»¬*ãJRP"!˜(K6‰{R0O2Ë+ª¹Îƒãå�Õ–~L˜!(éÅG�% e6^
÷%¹IBg^êMô •1s.6J�> ´Ê>3€C’9à�ÒœëaSéa&s|yµ[‚HC�æ’€P(C*)s¬æ¨qjr® ­Y\‘1”h““âê$Ý_¸þ¹Äñ“�?èû-¨wþ¡ñ:,½™®ë5i©^¥ë•3Z«²,É�ØÀlƒs`‰2õ
TX€ÐB¥y|NœßÌC�§«Ó¸x9c”Æ,ý%’.1k �s¡º2—æ›Œ¡—	2P©>¹FÖÒÅO=�ywNôwuóÌ0”ÛÕW›q+;÷Ù,Þ3’ŒœUÆ•ÜªrUräet‘Â±þ¡&}õ0±d-°oÕ‚uµ¹©Xfs^^þøÂ<?àÌn¿ô>Þ²¡”z2Xñ:!æ59ñ²{À¸SÀú«DÀ~_·cDðuòß!Ü3³´ÃàG¾jý±|…w’ÑÑK¹S•À`Ê�ß³Ã2Å>ø ¦>JçÄ«„E¯kîýŒ×S:(¬ä8Aú4áÝHÿ8M¹¼”5.?u�£ ­ÒHø²*ékelˆ@Ò!9
Mw©Ž÷”Š^Vz (Ï¥cÎò¥‡dùQÛ1UÊT4á6 Nrl Õž€ÝJ’T,C\R„eÊä£'EE2ØµaÜ‡¹§__eÙÏzéäè� é‹ÇŠ'Í,ÝŠ$ÿ&7–â6PorA‘Ö9  XLE'3¦[ORè<W…'ÿåëïÆCÕ–wït×“8§ªÂ	‘eïÖ“Ñ€Ç:ËH–Åc®©pÒ&c’÷jºûuP¨€mÒæîÈúiýûøÀà“c¤°¼·GŒ�´�	†¯ÉdgÛ¹rQØ6òCUk‰CH(vèé)m V°…îñÆ†½™�¯ûâM�¤�½j­DZØ­#<W$lNQ2ftÀÖ˜ã¢@*C«öM0+M¤3	¹íáˆy¼*™JË»«@f‚WRYD bÙ@‰zêVL´EªyAÃ:[ÕÀJ“x½ü ¥«¦‹�^Í™Xõ³œûq&ŸK·2ùÆòß#œì ÷ÿ‘N“y™FŠa¡ó Þ‚äixçuî>F“¼´oÑ¾YëÜ¤»OûÎ²x§*~-Ÿñ8�®àüpl‹¬´N`€zè Un·M¹l¥Ã��AkÝ*+ý’b� E¥"ý{¯MàX¬ØÖg4ã(ñ¹£éyËªê”øòörµßò-Mwù«î¾†rzäÎ`}uÝI³9I{•ÑQBG‹ëC)¨@,#U¯­¸<BÏ®J¢ø{køc‡�/ûõ5MO‹£#ö•]è¤�7	NH«TQF¡ÉÀw#y¡ »ÉK ñISM³ÿÆ,½K‘–Ï£MÁXKÆ²VQ: Å^ƒnâ¤²S‚úOµ´®kÒFI¦/`ÒÐa~vÇYjÖÝ“–)\Öbj²Íç&õÝ5G %Û³Q ÁÅ ¬$#¨ uóœpÑÙj Ýé~¥ëe<än÷pþµ°–Âïœòªvç™—;®/C6ÔÃ#ÿØl¿Ï¼ýúã{~|îë[3¼ŸFÅkQB&‹×Rˆ›Ì
¼÷B1eÀK7¤´59ÓmI5iigoÁ‘œÖâ5Ë@@<�çÞÊÇ€í†°‡œ^É„ô~åúiÂ3}‡v¶JükI™.¥¬2[w×•ô�Ò*Je€“¥ÛI‡}…-…™õeön¼¬èi·^íaOÇÎ�;5”ñØ?Ô§‚4î� @‘�­\q†<	Â>�³$˜[z,”¦¡ÀOý°¡v2û‡¦Ýs»÷"q¤¯ë§ë\Ý¥ý¨ÃIêŠíZ‹Ó·�-dd{„Á*°M°ÿì çö\›T£ià¿.åõ³‘Ž™
‡¬ÔtH’ÏHsç»;'¼n^À0ºN 86VI1«°KÙd¯Z [a­-‘I‚û”tÀ0ÒªÏ=›Ç.ó•§qñ2Œ2X’tgñ2˜²¨Ø‡Š“HÀhÎQ5¥à‘a¤J3)HONÊãaž¦è>ºKÇÏ\Y©¥c´PMÚ: Î
:ÓI'u¨X`+9{§ìeÞ¸DÃJ5Ø»Ú`•‘©3‰ÁgîÈ!ÞrKÔ/õùe<ÿòST�ïj	-È¤„fT‚É�Õ’ó ’-‰ÓÅÂ8ÛH¡tPM–!JRmb¡‡Üt’Ç4÷˜‘uS†±Ã#ó°üà0�!�‹&£ãqý|w Ö¦4hÐ{°MéŸ¸Žºö€Æ®6™&‚÷gü'û8'Ü«+u´“Ë'Ù.ýVŠúŠÇÙÌXÐc{‰ S«q6Éè4"mXf’r8l¨á.µ¯y.[ê„¤®Ò�ÂÒŠ%{“%v)yÒ^€lYpJ¹H8qihl¬·Éðiç4ƒ47W¤Át¡Âzßèëî“r‰ÿ~Ie.ûËÝp‚Ÿã@{v rnÿ/îÞ�Ü€ „P®«ê.x–&5Jß6Üî&-Z¢Üs‰ovfÕ«m•9¶|�®-—èûÞñÀÞ€­K²9�×9 ''	5 Ú;Â‹DØ#.¾�6H8îˆÒu.4{¾×ã>‘Z-_Zœ95fÎ0BNaSm�ºdÙË,ëa�dây#Mà¢‘!¸%ÆP�"ÚŒ¤^žïçï_WÇá¦C/ŽŠ7nïâžø‘!Úx—ïö N¥ô p2¼<X‰>c]¬LÇK+P0Ä$¤Ï_�½sW)Lëê$ªùéøì¿úæ®}ûæŸY’åÕ»—è¶t¶†Ú–_):dÀ-…K^a¯¡*]È gÅµàÞ&ÉêOŽ[6¦a×ä’ä[K’?¼$«Ò �Ë�[à˜üÀ~n®ð³L»©¶¸îÅÒÐ)•eXhƒrôžZ“ö–�éñc£sOg[ñ]Û‹×•˜=g«?ÒÓ~]ö�oH{áOƒu=|ú²ûþÛvÝ÷ŸÔ~ èú‘ýXþò*!¤I†ë:ðpÇÙ%Cº+M¬=
©qÚÐO ˜âŽ+$åé°KÀÑÿ%û1yÈg÷C¿³ñÏìÇÔ_v³ïkV”‡�¢RðoRú”ØJ�”Æ >Dc ¾ªCÂ†R·\3Ï”“Ì÷Å–ay&ndô‘3‰ƒ´š®¡‹>� m& K¥¢:»D%‰K&Ö¨ª=’iÂ§«bgNÙG–a"ËæW, ƒ­¨LªUFDB8ÌÙå(•b­˜Re¤d‡¢TÚ‰YÆ+€+U3ÝBáªYÒDûkÕúkW¤CµÐ¨Àu4Fn²Øúì
þÙ…]Þ:súH°·¥ûƒô.?Ì^0+ì­ê^ÆÜ“+¦;µ}´Æ‚ƒÙJ‘ï³°¿¸šŸ~;5k¾Ù�ê
ÜÿØêJNòC­µÀÁAWCXYíJ×¶^šÎuõ$!ä ‹#C `ØƒñRÀ­§�c‹r=9xG8þýƒû1Ùž÷u“n¥1 �Iþ:¢`dˆF¥öXnÈñ¶ðåPÅ’¡¬{/’…*™ÉNÅ;PzS%'<^Ö±A?ÅF]
¾šÒs5ÿM·0ÿWl¡Œ­à¶æŒ�K‹û¬UÀ›Õ%^šÐ%eÛQÝK'˜o‡aƒ�Æ°2Š¢5 qËtvæG|¾óbGË'	u-3Ÿä)f_a£…ŒE×ª¡§Ã’ÎÝ ºa©Â…NÇñÔ*­§ý'¿"çLp|ùöeµ¶è6GÇÈ2Þ±%n2é �cÁœj* ÙÕKu"EÓ¸5o�k€	3ÑÂ×�3•µsÂŸâS÷v�-Ýau°2*×êÒÙ ±A˜Š¦,Înç"qDFÉ†s�°ÉÚ“tK˜ñMTÿ_–Æ†åÃèV…¤€ßñŒ¹FØA	Cø(rÖ³m¾ˆiÉæ`•�3Nqî)û™�O“€ÊzÿúåÏ=¯vë
ñNÉ3ÓÅðƒÚ»ZŽÕÇËµæøîKŠ#¡²Åj¨8—½ô«RCs',ÑHrjS7øñ³º
Õ¦Ä%½Ì{ÎÓÕð—/‘As6^ùþ]@;®ÉÐ.¯vï8µ ¤ül	'ÐZ™ÄqjUÊÀbÒ×j¨Q2´èt¾Æùìè�O‹¾úäµ/vqß’Sºç(m§ìƒ6âKL”&îW‹¥•¹KzJ¶è34’ÌàêÓ)—S�¡ü7HË J§¼ûG|uº�§d½=m¿òþ˜°n­Ôn¢­Ûâý’½Ìü„}wE&¨1ô’ÂÝ Ê�3n�±ÉHšéÊKªO%˜ÕÞÔ\ŠÕ3í¿Éä»''©€Ñ2GWÇ¬’V,Lz”qLÑCsÂØéº‘uƒ‘0
,á\ä+à7n±øèhúÔ½r°Ý‰|ó¡Ô^P›eÌ`­c¯¥ÖeÐT¨Æ§ÀÍšdçr_žÇSŽ¥`ñì_iHÓzO÷)Z¾ò¬rªµkJç[Ì®âh…@ÌÀìU)ß§î´+æ,qiÄqkƒÙZßÒ2gÆwÞéÙ¦0vI2ú€Ãœvn»'|“°¡‰Åc`Ì�ÆŸÑxi-Ën¦Éúe­ôÐ6žö·<&£ÏÝ]j[³L]’ñ„Ò»X†¾€ÁãŒ2¥k’q[ Ù ‡)ªÐº4«xKG#lÑ¤#£Û÷Â—o¿ëî‹BÙirÅ   Õxv]’ú¥—4<%zi†"ó}ar86éç �«n&Âsé:-ÑîÝzÿuKÏã©ŽaùòM©È¨)Dé�Èr3ª€÷ûJ†ÈFÏ5H:(,†òÚãš˜†Ò‘‹mÚ}>…ñÂ3vjÄ>'õ›ƒìŠP/_·
òa¥Î±ú’ÙK6-`.©oLÏÖYénå PpnüƒO”™pæá“t°íÓ­4ÍÉOß[nÓm"ç††#
@"Ê ’¦AJ 
AB±Ë¤‚2µØè�{\SŒFzÏOëú¿xÃÇ~Ñ+Ð±xO£ ížÃÐPú²ûD2�Ö5€éÑ¦¼+¢ý›ošˆB6¶;/Íö[›íkô6Jäî5–j¹¥ª½–ê½dËI¸´´G‚½V†!L�«wâÇ�£K‡}�æêÏRî?w4EÚMf©eGP<¶¥O>³‘$b`(ž*à ”¥Ó²ŠÜÏdy®�Â³4Êí�c^f–ÏÛ³™ø®q×Ã�ñ·¨.:@mvØ’à«'—‡[U5Äi¥@�Ü¹¹ÏÏ¿— …ëk°t7ˆÆ2ªÖµÜmÁúB×©$Òh°H4EWèÉ¡ºôª;Ù,˜à[ qÚÞý‹¡6Ï}7	Ò[1ÏÈ«¸¸ÎóY¢­Á‚ÁbË$æ\$Æ±J¹–.-½°8“*Éq€"Ô
ô¹u'Å}Óñp¡G �õëÛÁÜ�:h}:v² §sòGuL¯¶Ç‰ñï.Þõ”<á™KŸâ¤ãÐã�£o!¹à3€AÉà(sóš¡a’Y;–Þ9ƒ�AÙi�íìaUpÝ¿-—UVƒ­kVÐ0í€v•ƒB¤+Š�áª´Ì e.14;€o•q›±úû'Å
·‹ÒðSûýÏç÷Ëz/}0žœ½ÒCË¸
…Ë”¡·:è�€s%¸â“µ>¥ƒœo€B�ÒÌhÖ‘Ùÿ˜ïl^nóÙ,Ÿ´¥�t`K°ïÐr
´¶h{Ø@ú*`|ÌUFêÂX6 X+�±$óºç§½´Sr¿r¶ëÇõ¡Èkb”²^¾k©*Òg¸ã²¹ãÔÎ¾ÑUÙ
Ãj4©.*¬Oúþ[PiKùA€çâoóæ_]Ð§n»g�u__œ|Í`ýë[-ê¯œ´—¾æå.6p‘®‡B'­@p+¬KAaÕ¢wÞ–T, HÖ1Uc\.PÁk3Sæ³ß<>|�.ø� ×#ÞÓÒùÜž£–DÖ‚o] Hê¥xÉ™è\´ ²9‡ƒ^U²JGƒˆµ’lÖ›�¦‡nß7iÿÆ/WGÝ.^ok›•¢²â›è±çÐ¤íp¶RœžÀÜ¢dµE.w®ÒµØÉè2]ˆ#Pò{>âSÌúªDùl´àYDàèóØoÊKÿhÆŒô¨ß��}wé½æªñZ*ÛKÂÉÐÁÆFÒÅ5µ¸& B²]º8ÕZ¢fÖT¥çAè¶uŸ¦±¸þêDöåë³Ö8÷¼†±Ù³à¼|ñ¥~#üóöú�Ív÷mÝ÷oiÝ»§?>Iù²;¼�uø)±ï¿�JCåì&7ÞÚ†ÝUHG/>	0Ó0 Q‹Òä‹@5uµ°º­$ o¦"WæÉ@g÷¨t²Æ¨­�ôLïì·‰ƒÇ¨Ý§£ ¿&²Y¼Ô°�9éb’R‘F¼ªAíJY‰¡oz�2Ï!CÍ²Â©Ô †2”Æiéÿ÷‹"ßŽÍ./ë‡«ˆ÷ò{�»æ¥W„©Ã`NªÕh–¦hÂg©â¦é,éT$=ðm�¯-ƒ†H‡‘bfÆ”�Mÿ8LîœaãÛvªÎ~Ír8ø$�ÎÆ#y…gŸŽPôu�ßÓi0sþö‰¸ŒÑàè&þø¶®ßÞÛ¡¹~‹O+“�	ø¯Û· �X”/ÒÊtØ«•é$óÂ‡š{Š8¨¦™\¤ç…/Ó`à¬¯×äÖüÕô�Zþö¦a>q+F�‡Ë¦ &A}É¬IâŽ+KÑ€Jõè�!l¬ãA3ƒÎæVH¢ŒÄ†Úr¸.nÔË»ƒ”�‚.zƒÑ”©Ý¶g›ƒU¸×MÈ2eÓ);l!oc	¢sR aYå>Í,G±ƒÓ<(·«+ø�7É=–¿P0Ùúï&-C¤ÍuÒ°jÅ´*é2Ðt=…ÕjÁâ¼0��çdy–f_h _—sùk��Ó5+ÐJk *çäzÇ>;¼/•ÊEÒŒ}óNúwIé¹Q)E‰µÒ|™Öí£mž—ó 0/�gæ¹¿*´–³'S‚L‡‚ô:7	�u’�*ÐÐ
¶¦M&<J<U†"TÎùšÿóe]¿¯°øô.ŸBƒõÆc9Ý6G^%ÇU¬¨ri¦ßbm(3�Ç<‰°=€7tV³"l¦F÷-Þí¶SCú›Vº:ÁÔ„­²±T§UK2f³¶FZÚ«9™0ë$¡âÿÓ4îR¹Î²-µõË¸éÆòõcÀâÞ4'­Ø½NØ&(M¨c³Ò.YÍÞèAÓ}b™û›œt"u1ºÜÔ\VàÀ%Æ=Ž–v¬Y˜7@Rl¤ENN‰ÕYœ¢\³3Öæ–ºžð±B—ƒFéâ‚56òv7�It;EëüG®š0.œBÕâX—qÔÖ ÅÆ†HqKTRîìŠ,Ni5­H‡'é§‘©¯±Í¨>‹€ü’È×éhaqOnDŒø¦é°ŸU:ëw˜0IT¥^¡+ìãX{|6‡ê«¸ÆÍL†âWÞ‹›Éæ—ì÷Þ�|ÀÇJ7ž¡Ù�—`ÃLÏ&'­¼Ä‹»·-
=íP¤1ÃæùÈÔ†½u¦ÁÈGöñ"oÜD|i£`(CCVªkX,™0’«â¢Råä¥[h`gÐ9JéÈbdpGl2Å7ÐôÍ½òxÌŠ*4iì¿Ãàm‡9jÀV(…”4©f¶Ù{Øg|w0ŒQÙ$Å¹ÙŠ“\Ú;¯g"|ôm#�Ú­�ÔóÌvÕ§×}ŽóÞš[‹óúK®“ÒwWc•t$-IbêÇ@†	ds†`E±\¤¼Oò�âk—°¨‡f‹C+ýiWùHàðÙÜ@­7%–%\Øh¥J°Bµzî‚ç¤c®Ô¼´ÀæÁKçŠ–%f)ÊT3p]\–íÏ‹_Ùý‰D½8~—´ñìS¤¦¤D=¨\½aXí&(ŽÐk$Å[ü¿¨
É�óÝºØÀ‚Í\öì¹´W&É,M¶º¦¢Ç¢T‚ŽfØ]e`—¡ Uq¥©žKPZ+ji	%ÅÉÆÝð\lö†iùNÔ ™,±c5,I‹‘L¶ÈÄgRÖDÒQ4º�Ò‚dÒ¨<7.iËÿùcìT¤YúF5‰Ù•"¡:!H�dVu/`ü((°Aë6,–qÈJÊ’>fF£ÓŽƒÃ}1ú¨Eßü~§Ìæ/ÛÑë?;®úuŠðÛ`äÁ33Ìj“¯Îª„_Ýˆ§fÓg]³§þå âq
äDQÞdm¬¬ýÃnõÌ�¯~Ã·€â¸+þðòT?Ëíä'ª_~léù$Ûa"ð1Çée{ŠúÎ¬ä©ñÛø›ŽÌ«‰Àgo¬Î›$üàò6Gxý„CwKwõ¸ní�Ð´]^B±Y™Ôá´DÆ9$d›[Í¾ Û)›#åRL5.AiVI•$ÁúÁgJu&IæÌÅw«Äë™<]ØË­ÞÓîûê0ÌƒÿØóÓnèPðÁÈÚi)·S]b\àä.@AeP�@Ò’Þ@i1lJrRšc+�ÉZCÓ%É1Ñ…•kmÎxca`7O›ù}»ÖçK;q¼oK;ˆçÍÃºž†eßXú·ó:‰~v.~ï½_Ëü•R`»5IŠšC—X¯¤@ÄÄ¹FNÝÆÔ}Ð´L¦KUŸÄ›ßòtNóÙœ­ù1\9'÷ÕŒa¿¸°_láÙàJ(Íô*9»1Gn¥S^±ÜœAE“7”p!AÈ§é›™‘§¨«o·rÑ°W¢åË„T´Ùf
š=i4Î¡°2'��JÎ¨j¨�ªâ]—PD(ÒDüuºHãÕ¼j{Œ8¼²7=n;×BsÂ¿Œû´ê{Ô š
•qˆÜ‡¨»’3¾‘vÚÚ¦dX�ŒéJÒ€J’ ï}”ÁL`c&}${óó5_‡¶ßW½RÇý³7Üq)ÞNE'Èsü¹³vµñüÉ¹Ü×ÙPÇhÆ!â2àø×}pXŽ¬ÃïôðBåˆ$ûtý¯÷6ìø{ÆnçÅ³S*oihçÃ°-7!”RV"}4²Tâ…HE`¯Põ“sáB’ 07mmøÛ×OÿÝjzh¾iéapÏz€
ibù“Ô†Xe`yó¦8JŠ�œN+c[!ìO<K¬¹*º¿±m¢‡št*úú0‘/¹8!Ñ¹ A±€ÂšsˆMúv•š;ØGªÙe%s°m H±³¬�ƒþe=×ÆðM�ŽsžoFŠé�¼5à@±Kq€ëV¢¶Ô|��U¥×s˜Ä\€H–RD1ÒÂp^€/Çg¿.XúüIôº}HÕsA�àF«‹7�E“é¥«Ad»“ ‡*U•ÚÏÕØïø�ýÖTìÖ‰›‘Ô-Ÿ–ÕIuiøkB3 ß
ôÐõµ®mÑ¾¤CµžÎTŒ�:rU>«–¥½`·3Mù�’JPÊÜèþ �¸s|›>ÛÅ/•jU2‡¦ÎV0ykœÆ.V® aÑ’
ÍùÆ¥Ø¡G¼Ô_Êt…j°ïÓêd"ÇŒàˆÏÞÆ\Œv<R°O_ÑqÄoéuÒõPoG®öPkÞ`}îJúžôÄ%;WÛG¨#	¢ë}T †pÅÓ¦9nˆ:uè‡„”¥½&Ùûfu'mmpº�.‘aþ¤‰f ñ|«JÃ µ”��mUn&Ê8¾›Jê˜xïPI-¶�Ì±ó®‚m”’ŒølE+ØõX�iÔÞ‡'<¾Ü!„óâgž+=å›ž‡È¯3¨¤ñ¬&o^n¤án¾\oãÒÕ!ÝâìÂív’¨X‡þQY¦Ç)–\Ü¬Š±Ða0œâOÀÍÇ»Fòªaq`›f¶q77ÃÔ,ngJ•¾Ä>8[j‡;èC6á@MÐ©4áQìz/`»:ü1L–óv:ô+
æl6æ”.Þ~Òia‹1‚L¨Ü³©&–Ñæž}.Ò“5'\ºR«öÊ Àfqîå8w7lŸÚ2‚8ËwJ9cœ¹1ªIúÎJšDQÖ²¯”«ø±’Ôf·È1X|Žt³RÇš†2Îö+ßlêäå©½9Gï½¹FOgv\ÍÜ˜þüÝQ²*¤æx·¨„?[À¢â>»R‹ä\$™BVsÀ Ø˜‚ó"#Ø‘iãß^�Ù%¼rÿÖ£Ý*k¤Œ•ow:(™¸ÐÕP»Ö µ‚•I¦+Ó¸Ì�ex©’üš[`kÂ»»5ÎoBÆ#>[zOÈB*€=Ð±Œ"•Xe06'çÈ¸– ¼ŠRñŽÛ®p±Œé0µs•K)¯zå.^Ä¤Š’„™"Ù•”+x+Sú‚®½e­®*à°ª< Ç€›ÍV²½tŸI}:8f7hÛ®!žQ‹× ×(NšÄ
 ÏHØfã0!ÄðÎÂVJ¶ˆÿ8€p¿KÕ4«žV^‡(Èk³¯["N5X¹�¤�êWëX Bš´{‹)F‰—e“]tR—ç ©$ã\`–¤šÄf¦çí_K>]¡£ÅçMÇä 2•Í ±>ö]’!Ú¶÷2.X2d}ÕF+H×#�eL¸…&Âižv^®÷ºŠ²ì}Àgy*»·÷¶Y—uÕ.ji+‚=‡=”R×�ÞE\Ú8Ü”UÌ’€¡?Qo$E›}:´xÿ8Ž÷½.Ê/­ÄtÕ jñIÑ²ñ0Ë@±€Ú e±,l8šÚ¼¤”è3qÉpÃ�Ák†jPPlÓ®ŒköYlâÛ±c¢/ñ;QTY�/gƒVö‡0ï»‹øebÈÊ=Ö2%éz!ÿÑ€MÓŽ}s8VÙf.$¹•Máz90‘¤µ€ªÎ B‡N×óÓöá5½€rùI�cæ‚åwÿõ–¥Z¼-mñ)Mš—¢“Ù£Uš€Ô*ÓHMŠ]Ë,B%Cn-,¿Œ]‘b`ß¼Ì´›¶•W­{n\2©¯Û�+¬€òéçWèé‰yF‹÷d+=™LF"­Å‘o Vfj¹¨¤Ö¹R«	Xw”5åV 	AÏ+7Ø i½{æ½®Ï/¯sGåu[åCæ¾ºåa:?]	¿4'�æ#zj¦Tê@�"¬I1ÚwfR÷6æ½“•–KÔS®Á
5$‹e¹Ÿðö¿@xŸ"Ð{L$a^î8û$c»ÉpÊ  ru²Î¹3Øn�NØ®Õb²†þÓuc	?ýö�k¥ï7…ÅÍh«Ý•+'ÆÒ¾´.çÙV’±Q[ e'Á‹,=Þd–%ª�œMê2™Ó�{9T—s°05Ó‰èõƒsb®¿JîÎïë61ýpz-¡G1;¯3ê®’¶Bˆw'¾=N|àý·ÓO^$>œŒÞTÉÝiˆö‡Ó!Î3GnÛÍ³%X¥9ã‰ËžâçI§\¢¬îñWp�ë~ýÇêPÞû÷Ë×ÇÚ×ñ»g&ÿðö.“‹?Súö[Ç5@¬Ã:<KQ‡¯^ä#ÂKúÕÇOØ¥ §WòÛ§ÏÛ›xEºãîêæeJùÂý{yÂmÛŽágó»Þ9�œGtt>Ñæ¬‘ÂÍ•½¸…<“¯{qv�_¯føô�ÝKÙÕíºðëèŠ›ûøD«o›Í÷ÕI×�˜—[Üy@™¥=¾3Z—èW’¸|M*6ü!ƒ'‹µL	æ¢GhÀ:]ë’þ\¦‹Vßñ`]kz·xueêÙ�Ë¡—$ý%V¨ð’«g/#$¸Ö¬øM
ë
6A.ù`³¡´¥„i ÷6uN´S7äQ²‹]~Nh•±ã™‚²OVZM5-9m’�„.V‰ Xˆ¢ô,‘A/Þgo„@Ç9×.5ïÇÐË/nˆ«t…ã"/ék}‘a¶Ñ²äx°”›ô†çä•î ›XU¯„%ç™¯nå_Š*†ûí *®§:é;dùÈ�£€«b�táf m.Zâ±z 
öŸYæcö¢zIâÁ“V“Àžs[·ÇS�ëïáoC¡žÉù–q“jÁÑb0‡Ì2VØU”³Uv‰@JŒ)ÙT}/nöñN¸™ÔÒj!¬:�t³,!ô.©¨™4BÅêƒQ'Ü/É©-ÊLB°ä RJ8ªö#e¢çLøƒ}€Æ²ÉóÒ¤®Hü¢j-)P¡•QÎEÐ�5Áæù¤¬õ��é÷Ïº.”fbKˆþ–}~ï¤WÈ‡æ¹F£cpÒˆÄF,�­*÷Ž»—‹)Ë¦¸ºÒëe Î²s%Î“	ócºø”7—´ß¢ÎÔSÅ….!]Îm‰bŒBî4mhZƒÞá˜'V<}’'r;§âzS¿¸›û‰ñ2-®l£—)ö‰K0°ÿ¸íÉâ–gÏ0•°ŠÖ{˜—^¼Émlº’Œý¬[aÎ¶N·Ôùs‘»ýh`Î á–.ÖòY¶;æ¬%­Õ¤`îwwÆKªs€h`·!©â“&¦˜!’TªdÃ\àz:Ã}|]�þ�Ìd�}ƒRJ:±5Nu†ùì±Sð;˜�Ý�%–žºË°>Žg*§_ûÏoÔ)…dÔ`oùíÂá‚R/ûÔr§KÀ4h'Òô“ÓÚe“Ì“¤H3Ö>Ó2æ¼)›ä ¿f	RØ~ ½véCöåÇw…Üeœ½i€†�ŠKU2j{ƒÆê¥RR
ƒ�Øa 29Á„éÍIq.Ñé­üe|ùÂâãª O�ÔãY°(£
{)a$›³„vµ“
ÏžC®¶øÊÐØNZãÌ4L<=ÿJºEÞØ§ËÏÝ]Xr Å'~•�•*°ªVP.2é¹æ¡Ós%MÑ�;†n¤mu&+¯™0}vÿZPaÿmÔöePTW0~iÿi*™{.|;L6$bwq}%a i#ÜÌK&ÞÏ$-CµÂúØfÄ\OS˜ó¼ï³‚À›ò>o6ciÿOo×¶GŽ,ßÏWÌûB^“äÇ,^’cÁcÙ°­™�ùúÑê–ÔÝU-_ª`«/B‘IfF�™‘yûëI˜M+ñÄv hÃTŒÖ;+�¨
æ� :Hoˆ91!µÉ¦¬€ÔË÷�ßõÏ�·\ñ¡yñe¤‘í•©Š¤ÜC­ÕAB–[ê®Në]éR²‹:¬€ìÔ.lôág÷ê3Œç\ ;dï¯¿ñØê¼¤óT$zÖAð·§æˆ‡s¤ÿàm¯@“”zÿ9ÎŽ�ØmŠàµEÖÕ<°È©ì‡a{kEÄ«Úà�‹šuuÂŸš£cgÅïž$^Ï^¥âo^+?&`•Fñ›?K/VzÒ `^#›‚¤Ž	ê±t5A²(P)ÜÄò�Š"„ÿ~y{Ï"ö¢~:ÿ<	úŸŸ~rÐ¯ä¶ùÛóñõ�yº–Kß¥q‡ ‘"à|gCÂ”2¦‚£Î<³‚³& x*a>}ÏI©@Ì*Ðõ,Ü±ïÒ :ÂÏÃï›9À(<ûp«Ö]ËÞµ`*b£õ–ié†9=¡ð0=;®vâ|¶ìk®šìáï»ã5Ûå)Âæº3«ø …8S±œ&lyð*‹¥-MfP ¹Dµ›®Tƒ® ¥öI6|9è}á½ÁÅYŽðúTuûR.j<çlg YöÙ±®+Ó)œ?|œR`Q¿�3yÖ;a—ÀB5¿L8Žÿîí�wö¹½»ŒÚé;ƒ™·–çqf™b³X�²žØ‚Ù' ³‹af„X ï¦[;yº997”/›¯ÃQ+{´žRnC¦	 DL7Gh¦Ð^æžŽzå¼»À°L×Á]¬NI+×˜�¼¸Ñ‹¬çså�÷ŸžDGŸœî¥œõ’<Ãâ¥Îá�ãIûËÅç«›Åâg‡/ãçTç°wr¼§R¾qpÑUl¨<Í‡¡àe€Ãì>
J[Ø$w�e�Ë
žÃ9Ø°r�¸Ø÷ýxwsä‹ùTÛŸ	pM,Â"ºœ‹`éIi²í&BSM˜�4Ê,;ªµ³ÕnL()�÷HÐ¼RáØ[Ê®Q Ä¼xÄá˜’è¬­—Ä¦lËVƒq�<#@NxQ;�YA
>»<W*"ßaócŽÀ•�Ü¼˜¤Ú3/HµÃ«lÍÌ’ƒõGJ=$™<qra`]2"–¨ðœ+•K—*4?¿öº5W™©[›½ôI%„æS²=ÜÖK�¡Ì]� Ž’åàFØí0:�ó¶Ú\¦®8Û—Bù5Už¯÷~a‚®RãöS¨�&P}ÑÔŒ¹ˆSä„ÝÈCl¶”ŒØäl- ´„5‰÷¡oƒ[)Šz#!÷ÐAcà¡8�l~Ì�Á%aÓ–gwåû™`aŠ‡‡O ~Á®çÅhPSar>ÏlaM9ë¥ÈHßéæ ¢WRÆ€:S‡DöîK]Kaw¿0@W§ˆU-êA¥˜•å»�óyn—c8¤Ï¬Z’É%—ºa›gO¬¿Ét‡â¨¬jK³4`´�yÀ�U<H­påÎÀou3ºe}–õ\‘z9¤½ÊÕºÌªÿ}|zcAAäí­ÿ³‚\Ï¥žoKhÝJÒºlq;KíÅ?ýú×ñ™ž±þ¨ŸýþZ k)§è%1êÖªYìÌ°½æå˜XõÖÍd¡Ø�c3×Újj®S¹\zÆ±É²É%°ÕÍ)qP� /û·íÎÐÐeÂÜªÿ?âêÖbëí¿X±­0^é yWöÏ2Ì©g+»£� ÐDO¹[DNÀ'Ï›�¾’ÊòöÆX¨X¸ZÜ‹zq§ë CÖÒÒ>_Éß¹~oÙàø'.ÑéÖœ°;Óóh:Äev. …^Y¹×’*<¿º’[®b˜È|pðpŠÀ¨b
âÕ:£}=ê½Ñ„7ô¥+8¸õXkß“+á×§©-#âŽ.5FO:•£b€YÚ„�k]WO†ºº²Ý¼ÖßG°<Ìl²<ûÄŽp“?fD¤�]§qlâAÕ»iTÉ›nŽb 4—.#æÓ	ð2<úÍúíÛ—ûöx
-§—ß8;»þòÕ¹çæÂ²³3áœÅÃÅ—ì‚Í¦bjLiÆÒÏ"D	,ÐÑ†Å„"Ýau¬dnü"“|=Y—à7ß¨F†çá�õTªVUÉ†öÜz1ÑÁÒ¯-MÑ�‰ÂëŽm¸j\*çú™¯ô®Þò)Ü^žoM%KeŠÔÜQb51õ¬9DÛÀ§›WµŠšl«ka²Öx^†û”}\áÎ—ÿm|5wÔÞé3Ek­Kì®©Ø‘©}®Þõ ÃêK$Yè˜‡Q™}æ‹ŸÁR•m¬Œøù2èÌÜ?¶Ù¯¿ý!™jfë®ŒiÃhpQãPŸñª/ÕÏšqÑF	†M¼ºóÀäLÝ]Ö¸Übðg)ëû«£9« ©²qº¥$Ÿ÷nL#…ÎÎ²'æ§EªÁÁ'R¦{p�C÷Ñå’Îóñ¿•„¿4Ig‹á·§Õ¿š·ÿ+3Z¶¿ª6ì7Ûga·éØIˆÇF}"À¦€(‘Ù¯=cIñÒÚU#NXÎh–¯ªß˜Ñò¿�Ñky�CÚ\\@š³Þ››È~×1Ùl‹\ƒ€ó1QO«@ò2ÛCÆÖD¨.L8»yvó£ÛòÕô\�ân}¤S‡™&d°ýAe¡
ú’ ÐÄ‹k 1–"¡FÚAå&šáx§çf¬yù@àcýÆÚŸ£¶õã]}øzÇÜ±�õªÿÝj±Øë²ÀÃ§ÒŠ—ÿc¢Ï.LwY¢óÆiÛ—¿™ùx©’µ=@Ð ª¯7#ûÄ¤¸»�
 3SR¦Á«<»!%WÀþÃe—µaMŒ„Ú¡ý‰ìé!ðDÓ´i: lv>w™¾Å”Çl3-å�5Æ1yqÐüL�:Rjïà™Í¼Ÿÿ±�;`óýµ½Áƒ¸‚QôÌ„á™9÷V�îÙl¬±;µØ…g¡€ªO™ñiRèj-uöJû–%î[¯�$SÀ¨ªë‘¥³×&€ ")k¸‰}ëØö>öàq¥G n64_y:KÍy¹¤©×»®�YÝF§®²{ƒN­,UC ƒ�Ú§²ƒÔ°€Ô}Àª”»fïØ`°Fb¤4!ð¶ê�©±Úˆ�Õ—½ØÝ]¾f2žM
‚z6¥b1
Q· ù!$ç'á'Ô‚:,=Ø8ø¢#/çXŸù]?öA]5ÑSÅF»—²¹~û«5“g‰`†õy3Ì<^¡MÔ€ã‰mÀ…Ž		¤8þ‚D,nŒs¤UwqVvr­[³yÎ2 <|˜§šêœŸÝ›`éyaé;XŠ‡×S¾…Ka(öÃšª«Nû4Š‡O_öîÿY*)ÙAâ¯FV2žßy“)z N7íÄÃmëŒE>%eX(föï°‰R�–´s9}í”¼scÝ�	b\%mm~”|±ðéàÞ¹bQ%vÛ´²÷M@ÂÀGÚœŸjìp8©úß‘eíF‹µ½çîa{!{çFÚ&þ,[C0ÅèðþÜšõ !W×f™QƒÕ±rÖ¼$4êú-Ê�éô·.Ý ~°y^«žš¨9™ŽarÝH@H´'Dç9&»æ ÐÙF¡äž¤¡‡2Öº=>Þ_ªLZ³¹·ÏfŒD«³b‘�j˜&¿ÏP™­ÔjQß}ñ	¯0ùJ¨À'éíòAØwtÊ{>[�	Øv€Ì¢î|	¯\½+Á(åiÍ@´å) Tˆ}” Ò±³wÊPD8e¬XçÏþùÃõÎæ!
ÐA¶Š›©5 =7~Q€¾¸â ž…cçù$ñ«�À¹n²5Vn�¯k)çáxohhåL¼%E�Y€×™ÙÐeÏ|…á¤�ÁrGpƒ£œXå!¥Ë¼xmƒœ)ëî}˜V¦3f²žMF‘»e{ÝÚFhnðFGm³Þ¬ÝìêNõžXøëôk²-7‹õþªW°}‡|ªYSnëŒu=±Sf¶-ÞŒ¸È“rxµäá¿²«ý�y6ð­f–Çž	ë�±}½ê‹€½ÿ/¾~÷õ¡~þúþÓ·»Ï_ôÏ{ýkûd¢<rÒC!6Ü6¢™cØí
ê…1: ¿œ��(°Çx@¬$3-|+¸µ´ï³S—ë±l‡A\ôðÞ”Œ0 k½šA= ír(V•ÒjÚÙãÙ ãzÁh�’�õMá¨³þX§~ëÖ|ù,SXõª£ï3 6Œ”S+B©Ñ˜Y°ã!û»'qLåÞY?œu}vžt;:ü�VôÛãÇÏõ�£*ýâY÷w^s]Ì
“ô†‚Îb;²ËÄ�°¹ä%¯zƒE 5Ÿ¼ºn!§ÚLl�iöÀsžyHF+H’‰% ”6�–IdË¹¯Ñ«^t×Øžû¬}Ï”\w‚ÙeMU;3»Zkñ©Z¿ˆé¦òï'Ü ÞäVb2¦QÖa•Ð(0ÊFrËnþe…\éîëãçÏŸ¾Ü�'¸ñ�Ý§�}ZJQÈ0n±Rø$e×Z‡{1	q0vz¡x¾£:›Ï3‹Ék¿tO¼âV~mg?¼ÿ:š¶˜ŽÙc„É4žuÅxY“sØBÑóÆÑøÖMÉT·î@;mÆ(IËn;í™KT ›‹”°b²Tk³—§‚â2÷<ˆøÄÿmÚ	 ˜£X Ò1á7&›Ø8íÖ¯äP¬{ƒ5Ó½¿ïç’¯aý)„À´`KeŸš>\cM€�–O#ºd`2>…´@¢ònsÔf_^äŠÇ8å~:æb^Ü@œjá×t}
¶µ¥cÏì×†@ÙGª£Tg*$,	7µÁ–âR�$V}–`ƒa;¬e¬ƒåã¾§g¿»—,wŸûÝ©–ä÷‡ÇSR*ßþo–w®?psBÿèÞmj÷ ö�×S†‹~”V˜‰	>©sÆÀˆÙÀº(“­ÔqG“Ú¢m©­nç[ó°÷˜¨Ìá0‡õT	|F-Öþlä„ùp ZK?ÏêzME{ †²ÅÓ-¬‰(�†°�Çw(ò@æ�IwI}¤$BTË9´Jµ'eK-¾Í@¾˜X†Éub’AHovùV¿ü®ßŽåOrsažJ¥.­¶}6èþ`²ËA¢zï³ŽH¢ÙÌÁýF)¾Ça¬¡Ò\ÙÔ‡‹Qê÷ŽOfÄ;Ø˜ÍAèK*sý³�uäžÙxoV?™N>é;øªc›…/ÎØ âZª÷Ë²ä?kããç¯í¼y²A„mi#€e (T�ÅdoCìØ„ô8Ý%ødÀÏQx]²Ë5Ëç¬ÇQ¼«õK/áüÌüùñÅÞ×ùøõÏ¾ô>Ý0¿¿úÞÚŸ¼ÝÚW_Þ]{°ã'ÎÿüOYí`ñ�­Ö`«™‹ÍŒg�šéÖ8°�™òë‡„\{�EIÐ|U“‹-%‹XÎ¹Z·ÚqÍ®ZíêýWV[}�{ÕœWß:3ç�w×žøÜœßµ	/'bÿÍè@j˜}8Ôû,9¨7© øJb€Rð?ëÙ÷Ö«MìØœuš��3òíXùÆxvX¦Þ�Í9^îRß‘­þIŒ�YÁÆ4°¹�xÏŽSl„ cD ÃÞÈ0µê¾s<4êþÆÝGoMaÃ]Æ¶éÙ}•5GÁ¨C@À—pÈåíØr®ð¤OØ=éç³ƒe§�Y6ž�gG?)‡îáây9‘J™­�J7¾ðŽ­Ä�°ªžv¹Ç÷·JM±Ø¨iè2áÑ~Ñaq•š5âÇ›B(bF• =ùPÁ¶´€,¯?7�,Â|èXG¬-§…—|ìS»$g8
Up÷à§”‘Xš€=´t5v]»__ôºû//U�nxB	Ó|ç±^�v#(¹�du‚ÑÛâÀwl1ËH¦rƒÈœGŠý×Wž1vçýL¦‡æ€o…™ê1›™ò´Æ¶Ÿ­¹	b­‰ð×-”,>ÖAlv$;ØÄŠÔ¶nb§ô®ŒÊ4ª2SF¦ ¨zÄoœËXl	·Ò³Í3,×ß´ÉKÞß(ÑêÍ©1æ5$€t¶ó`º	Óæ-³ÝÀe©°ÕNn[°›l~kÓ¯e«°ÿ5Èk¥‚˜é‰¹(½ÅÌf¿ÑéŠ�7pvÂ^Í•^Kè±¶wIo…üs`´¿eLã1n’”%WçGÄ#cîÕ6û�Xz1‡Êð†¾ ‡0§=ñ´'„þV�¼9œ]öF€aô€by¥%t½Ôøcº?a@üÛ"{—ú¬¡Æ5¨¢ÕaÖnõ@éþ¦ÑQ6¹Ø˜,�Ü$#ê'ŸK5âÁ @óxšmŒ-¤™]r]efF‰þ;WÚÿÆ—a{L8°:3YMàEëoª«Q Ž«ˆÛÙX¼1ÈÌk�%–YôÆ2cû¥–Âæ¥ƒ3_1§ìqM­0(ø™#{Màû¬ÁŽÖýsàÓ°ðb­’ÓòõÏsç·uVƒG¿Üö©>Ñ�¬™Ž€¾f–þYàzè™çF Ø=Ö9Ñø«ž†‡A™�±"XôúÆøåPè·ãÑ×ú˜ÿþgwyjÃ3#3»iŽ*½Q2ÀâŒ®Z-•�Cµô‰)I£j`�Ÿ›+¥VtùÜˆ
Ê«£bòàŸîêðvó@+<‡•Ïœªíl7o+ëœ„®¢Š!²Õø¡–ÒA{àås^ŠjûŠÊß÷tÜ½¯_ßß}¼5ÎôË§þéóe*µl�½cÐB¹˜J,�v©®EªíœÁç<ü!†KWÓ=âpëˆ3ëò~<O*?�åEpAnA¬=ýÛombÛJsX*ú`c"¼	°Ó¨ìG¤È�Syt[}@d+y`iˆ¬è�^�öðäo÷n¡é·.ZÖÃƒMµ2ÄEØS¶© ðÞFeç¬ÞGŒ‚Ÿ zï�kj”ÉÚeEßÛS°ƒág30~V ÌÌ*C%#_\6H€KK!�ÙX�ú(ŽExj¹ÂNspÿÏeÚí%~�Øuì'•FÎ<êü18q"%Õ ph_.¼ÍÄj1‰}„±Rªy|ø7Ûí^|nï¶G0•¨€/W
…ë¬�iLWÅfz:bgô]`YË"¦DØD¬$Ë9¿hæ¯ß.s#vè4é�¬,b¶êy­\`ãšLrƒb½!	L¨l¥Z»®‚—h?—ÎG¿cÿíÑ=}äõÓÉ¢³Â7iI^:
œsJ±ÃÏ^ÈL·ínv_eÒòcÎžb³Å/7Fx+aêù—¬Öý‹ïýÛem~¢Tƒ„]gxâbaVõ	Ð’)�›gHà\à+	Ä™Ç±!½bà{Ý.ßxô§ËÏ~¿{nÝöŸÿû9Õ2°åxœ…WínÛ6ýï§¸sÌÞ"¹É€Ë�É’²HRt@`Ä´te³‘I�¤’zE�=ÄžpO²s)Yr›�‰a‹äåý8çÜë›JewjÁÓ�Q+¦–õÇ[þX•Ö±»-
=Ü³óÚY|�î¦¯‡ÎuhŸì½ÞÛªz^j¿ÄƒB•ž9ûÌéj³é÷äðè-ù ‚ÎJ=§°T�\m<>1•6»ó™­Câ<mî&Ïî^gìI›¤rŸä£×9ÇSgõÇ~ï–æÚ(·¦Q¶°ã”.Óeí.Í)³Õšœª`Á¬É0çœon o£ÅÞH©Í�'E^›EÉ”ªt8¼Ú
ààM¿ê¹,’Ìš ´�ÑÖ¿ÎaªfMÌp®1k¹vœ…r� BãÀãpRzÏ”3L²S�±¹öLç'[¾ÖÁÚáÙY±êãò426�5U}JÇmÄŽIKÂUtCöÅLÙöÄŸ.�ª”Ñ™”,hÀD™œŒ¥¼®J�Á¯Ä¯Ws[â¸)ð$ w!ÆI²=ÜÀÏ—–IpIXW²~3ìbNƒ›œ+69›L³ŸÂ³«¥rrãË¢¬9¥k{§­D3¯u$Ø¤6	KÇ*§‘gÔÊeØM�ÿ&!ª¯æb¸Ö Ñ•—§§ÀìR¯H©qD�Oüziä.Ù”ÙÕ
 gç¬#‰ �}UbÜº²¨69µ3.D·s§ïÛ;·ßD˜àqÖ°oo8ÀjÎ·|Ã,pÏšŒo3.Ëö{ë_ó¥¹à€>Ñ‹w‡;T°‚¸UîB²ªË Û\§ôYðž$	½„©J…%0Yù¸�: �³%gwàôXÌÈK³µ^Æ˜�“4�ào{Éq	 Ü£*b‰ŽEÁùÑäüð5y°în uç& %oèñüzÛþ8…©•-»Òà
Ô˜.ÞŸS¦8*‡=¤‘éA#Z`ÝYP©{¼#§JpÆ6)„b>f?Ä"²›5®SU—eÏ´(íÝß{ê ¾†©ò½L÷àV[¦d+ysiÎ…B±hÖ¸*€ÏgBu$zSÕqóXd&¢ú8ÿN×sYª­q‹ò`&û:#mr}¯óZ•å9ÔÑb<„šÀ½-TCò”Úù°�lÊªð˜KÑmñîøh‰„‘•F�Òü8æQ‡�àš¼é
1éâsÉð¬‰Ló!¥Ã<¤)(L\êk¨(<Ø¶@Ë¥2Þ§Ñî˜TžÇòvUšs	‚ÔF‚P´R![Â&lµéÛ¡Ñ^sLÑ¬µÿÝ§4M?Ï#3[R’Ò¤åòäðøÏ“t•§ƒí,µ@ˆŒ€�?Ù>1yâ8kc7UÂòàjnz�9^…’”®v¢hèÇ™‘œæ|Ü¥U»ŠÑ!”IŠâ3)ÆJ*W,„¸—©\²Ž&Ï/®¿š—°;_w@œs¦¤-±`¶B‹&ƒÇoOOO.OÎ¯‘àÐF[Ð¼oôq.Um ŽF-~šàíyûo{R:A–pGM:ä´`JD]%Ö­Æôß?ÿŠÏ›L ±r!b…z£'EMo`´i`Déos4Xi}òZª¸aGäRk±q(½LŽJ”_U¦†W}þ†Ò’Û†.YF›×·Z)ˆ­LKI¼E÷(¡oáäéãÏ=	=Í8ãû›¿¾á‹ŸlyæžØZ 'Ìqj±p-³nûÞ—ïyòøÓO_öã^{H™þ›Ý3A¿ìG|òÈÒÓü¼ÙÈ±Œ-Ç-[¤›lDˆ­´ÐI›ý¦“vc� ndœF¢Ðâp!½ôÑ8Agïþº…Êßž\^Þ¿½:<:;9ÞnãèÜ+mÎ õ¦éd\Ô"ó4[©;ngÛ5†Û¶ÖOäh…ÊŸò.ª›éºç
ˆíþSj6}ž	ý¹—!=Ý ¸?°�Äés¸ë¶¿ épéÎ<®ûTTøwkÐû4:8:N;üÅÆ¸)³’)<ƒ¨8¥aÂÝJJrñ¥ RŒ>hyçÜv³Û‰Ý7Jå¢Êo ’ÒÙõd® ;nÆ±½ ¸Pn¼‚ª=,ayKâû_£ëc`¯dÐbÒ2�t)éæy�5JÛüÊi�Ê`»_y™†Ø$µÑ±»?þ[të”¸­xœÕZÝnÇ¾ß§˜ÊJÒ")ÉiÓ?­dÙŽ[GV%9)`œáî,¹Ñrg³³”ÄD‚^ôªAa}‚¾BozÕGñ“ô;gfÈP²Q4eKÜù;s~¾ó�³¹#ÒÅÕX_å©)t1ŽãD¼ûá­8HMxnC³(û…VI¨í¶H²~^ü‰¿M¦Å$ÉT±‚³YbEX¨R‹¹:×V”3-^.®Dµ³èõh¾M²iZ-ëõ0M•¢Xd¼ x³rìW�Á`ˆŸj<ê6ûU2‰ŽM¦$ÔEéb[¨,â(ØP¥:Ø�ŸØ¹*Ê~h²²PaÙ¿ÐE'´Ä–ªÄ}æ‹´LÂ™ÂÕtZè©*F/»Piò­.ºA¯×( J„S#ž={ÑëÄ‘v1ñƒÚòƒçFú²HJúþT…³Jì_Y¨48YØRhH´ÌM’yU˜L$%~]fâÌœ'†ž–É\CGXÁwe×êp�”2x÷ö/ïÞþð?ÿiNëx;â~îà®ø9|÷öGÜåO>•d¨š„€µŠ:?õÐéŠÜvlhÆ¶§f4Y$i$ð´+nû`o·¿ÿ"ØžâÝŸÿ*DšLÖ#c „è”µ§o»_ýr™kñH¼Ù"?JB,Üúª{ã	«Ç‹Õðã»�áÐãÊ¡;ð×ß¿¶&óJþñC.òÇ¨4íÜ Ÿõ-6ò·­®}ÿ€ÏÆ%õ!ÏM?ƒ:/4Ü=ÒWºN•Ï†§ÇûC
ÿá(jÃÀOCÈ‡ŽÛþíÏã¬ÿw?g´îÜ_Î–Œçµ‹ØRãlìWè-ëYR\Â(Vh€é²š
@5N1 !õ �xÊÎ­ËÀýÐäËÀÄ<ÍY¯ŒÄPä*KÂ
îâ%ÖàHlŒýÉ’+Î1r ¤—f�(/{D‹<MBŠF»œOL*(lð¤îîGm¢*ùXá¨r&"�‹»økU.
�¿!^ ýÄ_|÷î‡|/ItÍ$3ˆ~l¥S«2E6®³þžˆýƒAðô*7E‰œf=áZV2µƒìzw	XÓ1h‡Áp(Ns�}h/ÂÇïË�@ 3ÒÅŽè¼|ýÇ1 qüê÷]¬5í•ªEÎt4-¡»“M:Õe‰;A%VüîôÕÑ±µK½~ùR<zÌi›5ó¹-qþº‹2&@‰ë`V†ÎŠ^[žîÚj&çË¸—Iõùãð<`µÐ~«N†MæyBé§“»áb�ó^&l\Ðw£˜ƒ&‰ 
GÃ	««"mGBîHaÎÅ¿ÿ)ä®õÝvüdOÂ�
«ûº(.ôèžQbÕ’�Fk¡*�Y*7»„®MwyÅGRX2»Ûd ýò†áØ2IÓ@³o‘A°çåDvœÐÉuàÕiÙG, ×J—ða¸]Yê"¸œ�íT2!št±¢UçØ”Iƒàùó
á(t—4¦tîkg¿
"6î0¢¬˜Ø®,#~ötÿìõÉÓÓG+€N‹A‘iam›ëÜ-l6aËÉÖVÒQ)éïø@¿_èT+Än¿__û!œì±3Ü~›n˜²YP*2¿ˆ3\¾ò(wÏ‘ˆ•fñÕ�¬´èH­drÇ`'°t¹˜°Õ2­#‚ÃÌá!CaâÀÄ+½_X8‡®õ7‹Ä‡ÆJ†.“¥Ë.o½FÖe�´Vâ0¦'VLu¦5§'ÇO q§— ·P)˜·“šP¥ò�`fpKÃö!!oBFB@9Ÿ%Z’%yh²ˆekbÇõa§LåÉÅ^µªM›‹É :BFÏÃ¾�]ÚRû¯`ÝúR-‡{ÃMþ6¥;•2 ›“2&š�RïŸ}& 1�{4›àÙ/;8Vgâù+îÒé> 
NwY¤Š¨AµU]:ý KX"~š.lË¾{Ê¹*˜ƒì«©&¨Öi,˜Å[RíšênS*Š“¥hè¤·ýrØIAX�¤�‰;Ñ°’Í˜O5­"iØœ•EÞÄN~8¿I/|©ÖëY5'GÄ]5øV.àÖ&&[ItîyF9©ô>ÖÄ—ø—
-ºÏ6öî»í)[Òræn”bZ‰7_¤iÙÖëÎ`�¨¦Št·–¬$	†y¢‹$/9šbE )(~Õz¾¿„OkÁ*­iÉˆ/!Öñ<Ê‡A è%6¥‡ vÈ‹¶Äh)u[¤ºôø®¯J(¾Ðuày
È%QH1¦W^"ì¦8ìà�í²Pä¸4†z½(‰cD~VÂ&sõ5² BNïô+ú>Í2X)„I�;”_šò•ÝãG¾ò¥ŸÐ.€éÛ}|6ÖÁÝÛí²²IK‡ý‰
Ï©ºÎàBdJ"6ªoŠ¹ÆÌX¼$ï¬,h(#-!'ñþyªÝ½`‰ÉÂÙÙ›uQÄ*$—1—–a	¡�„Hg£ gá—ÎfHnBÅÈeBC‰½kw å9ó”fžzü�ŠÌ|m®ûS�õí5n©Ø‚ #ä›*M|%]•.:p@Æ•[
*ïav2ò¶ðÇè°G¢4ð'¢»[§šl`‡5­® ›99HªöáqâcˆÅfjRÌñ 
Ÿ•ÞIe<äÿ�x,mÆÜ–Àr33’ƒà]£.°ý-cpSÉó:[LX·¶‰Çu]„¼1µóÙ–ÙQcÐúŠ²8VG\†Äôt¦#³yCA®á#0ØÉÝþ…Oj•Ôß(¨Nà\’˜ŠméÑèÙ¡H8f¤�?–ÄÓ¯ë@]ÿ\ûî×­Ÿë¶Q:\÷oúÜ<òÁ“®IúUJÕL¶Ùú¸

qË¤N¥™®ôÒßÔaã•44®†ÆÕPë$Le[Ûµ­S›ÔÊ©Ý,öÚ¤ú€mqìòÇÐö›q�W6Cãfhå¸÷KßâuÁš¡›®ð¾í)Û€£S8Dºo“¼Ë¡:3—]]!�Ç4‘X‘Ç•Žì¯ˆ’æ‹	R$=M@ËÀiÍÙâm¨|¿5*âYuiñÃ�ÛƒÝ ó,™`ƒ‹{ É±²å¬,sP Wì°,D�}´Õ_­Bí\ÿ"H&ÞñÙÙÙ±pZ³ H3:5&§Œ±;rwï“ÁþÛ=¤šé1’´²¢ž.Õþ1F¨ x$>ÝùõæÑö�&xtw/ƒÌD™«¯ww»nUÕÐÄÀ7¸6³D@N }< F9Æ
tiP@KáJ¥`&;|f'W�‡=á;Ž¤¬N'TéZ}\˜«%ÑÈh8l]ø6¦§c¾Ÿ 2]”¾'J+j×WõÜø¤•ëª:HJL¥éº5�{:g@;ëÏW+ëÉÉû‘ÊË#]’æ¶EeÏ®+ÉÎiÆ= ‰$wŠ“+
�‚Eîk]ÃÖpšk«NHà…ãdÝÑƒéÀoãgÔœI¾Õ
ÐÉj~=›~_ìÕ$ÆÈÉd$·TnêÂ?³µºün@Ú·œ�]W`AnŸ¤C«i#IÎÀàaK5OƒŽ¬z×œ�¦þ\OÈ«1Y÷j\„Z³(T)\Ÿ‰Ñy§_—Ü±‰¶êýP•®@EÄ¡/\�Ž!S ÛÂñø
ï—7 8&Tò,RZD¿åv 7B'&4ÏiYs´_ý#€Ç^€C�=£ìí‰^{­Ãý!pòUVÀèÓ�û;5³ñ:g»tG¨þE*Ö„•ý7ëvk´uúôä‹GÏ·¾‡ˆ3ˆ Qa)f¨(MÐæ ‰ÎqTÿP•Špb$NÿðµºpûsO«šÝñ:ÜLÏ\°LŠDÇ@+¢Ö%÷üZ\»ö< J¯çà%±ðÔ›ú+`Í.Ë@Ã³d®R¡ã˜$/Jíú¨,¶¹íÁY"2áÂ1WÓ§Ë÷Ð�Ãƒßà_'CŸƒ¾úKÓø%•L‰o%| ·¹Þôp#­¹�Èd†ÿªöÃ^ïË'OQ˜Ÿ¼úâé”v]ws©Ð+u
»vß“¨7îíˆ8lEIí†mIÃaªQe¨A8‹)ŠÆÛ)Ó}ØíTi	ó_³ôzõÃªe‰?	i{=„†�Q•O©IF=òÜ ÊSä ½^Ó%©ÞÎÔsé¥®a9]gâvfõ!ÒPÖ¯%éÜ&'D†WƒüSÓŸnÐhÌùã¨])�½�+P�"¬±½^µyŸ2º+p‰È)]€ëÆG£‡Ÿs]kŠÇP0.Ö©ê#ôP7ø](X³h4ªö•÷(0‰Œ"«í¾I‘ÓQ2Iqxz„›IÔ‰vxK‰?¤b²’�^*TjYqïþðãO†{÷?Ò;l®«ZêùZ×¾p€–RåF±ÃhoçcA7;M$½ä†¸Ô“Ì57#,5J¸PË4'C2°|àð XÙ|ï^µj\š±Ÿ…CÆT»¤úÌ!9°GM÷JgŽH	ò
‹¹Ô÷* A�8õB¬Ô‹s×¿¨›oŒ_“oynq² àMÂ„Ô¡Ü(ÿžÞQeaÌž$‘ˆSããixCo£ËÚk]«ø9ÖÐéU¡ž+úŸ„
dm@ä&):ŒÓ2Ä¬R�Ç¼Ë„ÜôQd—^ü˜ÅtHn”Tµ ½´‘€[0uâÄ·pÔbP9åë“—¾iXÖ+‚Ô*¨C£R9™‚ó‹¨“eôï=Tjê·aü’Ä¿£ÿõà¶£W(�ˆ9+¬és\wèf¾ÇÐzß/1!‹ìƒV�‰Ó$i€F‘“ÿ9VÛì›xœ»ázÙuC©8S™áä
ñ ªRÂD“ÐR›¿H|`ÑÏ¥>² ‡E¢xœ340031QÈÉLÒ+*f¨y]ùÛDŸ1PÀÅòžf­´ñús{šº�E»›xœ­YýnÛÈÿ_O1Q‚;ê"Éé]ÿ(èÄ…ãØ‡´9;°|H‹4 VäÊÚ˜"y»¤ÕÐ‡èöI:3Ëå‡DÙ¾!p$îìÌìo¾—Oàdtüú-˜\ä*ŒÕò…ÈA‰Áo^ÇixcÂ´ÈGÚ€ü’Å©–ŒÔ·*”T2ÊtŠßLï ™©Ä¨HòÎwÅ—šþÇf*z·J@x�Ž‰œ·¼‘F]'à¥‰„•X!’aºÌbæ2øLðœŠp+-²LFN>	H�´ªNŽ:³™„"3¹–béƒ€éDæ¹J®Íòu&K~`Ï©L‘W$}õ�Z‰Xý[Na¥òÈ[‰
Ï•Œ#˜>ýÈt^$ç¢ˆóÁ§é²¸0?‘€0ë$™äz�¥*Éa:O<'~ £#¸”7¿ôC¤_/Ò•ïŸj�ê£)ëS1›*ŽÝÜ@ºJø¸ˆ¢H"˜±AÆðAB”ÂùÅh©®%Ê%¦6ÿûÏ+~+	¡ˆcfˆÀÄ%ÇÂWâÙÙ[˜¥E‘­�"QóT#�Ó¸ø8{è.:^šÛðÈÓd®®ƒÏ&MÓJb&´3Ìß&çèyj1/õûÖ4MãÉe–¯az·™Â«£Ê€»vƒÀ™L¬Ð;>‹T(ÐKàb‚¬‘>²†°DZ5*Ÿ]¥7*­8¡kçj)‡Œfåç4TÒz_®ÒðŸÊÇÃ§e^è¤¶¹Z.QT!^[É/pKhý•ê´Y©B‹"	2:l	¼‘23Mås¦ŠÕ\B:çïeœ�·y¸ *ô<«)fni•q4¨-zq~Úˆl
Szµ�XSà]“Gp€†KTX£”ÔÈEE3à#³^ÎÒÈòø}j®Ó%*›Ü�Z>BˆBÐ�¦c15uÈžÓRƒ'SXŠP§„ã‚pkÆýå&IW†ÏŠÆ™i]K:µÝ~ÈE‘àê)1£c•œ“ÛÕ&ƒzÏ92�¶ŠdÏa.Úw$ÌéDhLNyºŒcx£Œ˜Åuº1‘B7 ¬£sRWa*´X°7±{0Ÿé»Ÿÿ`0§——Á›·“ã×ïNß`Ê0)Ÿó!'J‚ÌÔž¨åµÐQL¦D£¯*\0·RI4°DUÉA©¥’q¯WI–óýù\ùþÉ$×‡õ³Ôø¾˜eÂ \ˆæŠõËÃò?€-_uŽŠž’[Í‹8^6±unZgÿq/+fäèGîè÷Aýð=¼‚‡,fÚHSàìƒRWÂ@‚" }>Is¸ÅÁÏWg£¿tq%@_¿	N.ÎÏÞþè$ü©KsN“‘Í-³"‡¹`àÐ‘¢:Ý»Ü$ëÔd™UéiŸï�/'§N…ï­
WÈae5È›&s¢ª°ôÈ#Cò»Êi[:ŸöÉuåDÿ`EŸU§ãÙLCU†ÜÇròþøÃ¹ã÷çÒ/ÞSÇ°:Au5ÜÂ[™ŽOƒ…kÊå<o	Ç¼ý”q?°ÖÅLÓ¿Ûô]8„æ˜WÌDxCz»‚çÒý¸7OJ¡Xs´—åÚ‡ï¬öÖ¥›euÂ²‡tˆ#¸ëq†žî+�Þ |Ì¹–£.n<Òc>°º{ƒÁ!mø/žcr|vzõOŸk žÚÃþe ×æ6DAR):ÿùÝYb�Cõ¨Â‹\åŽ™qÆ`˜Ùï€âCÓg�Ùó`cÅsšç¥n-íQml)ðxfŒ¢–Þ€ÎÈŽŽdˆu×±†[»-™ÙCƒ�‰0}ë¿’|ÓÛX‡™°ßu—f—4Vi5ÚU×@¤Õ-<�y0ÇˆöZQj›!âY6HH†á3hWq®ßÌ­®Å‰XR~#‡Ã–‘êK‘¡7STbw….»”Hhý�ƒ)(¥¾<ÂY‘yÄÂ‡o¾µ�L‚UÓ‡3öJtÃÞj�›1:ÃÇÉv/"¬>‰©ÿ9”L,˜¸â—��ãûgüÿË‹"Ïp×«ý]äÑ°wW¹÷�’¼ÍžÛ÷_Û'¾ŸÈ:�À£îNäO¼>öw#ìçFwôxÓwA@œ Äë•ã9æ'Þ2½•ðõkÃ+‰Z“ŠÖys²*V kÖ¶Û?°ºyƒŠ}Æ2¡Â
ÙöŠ¥vu¯ônÓ³Æ&w­et�³�ë|ØZ!Ÿ—¼ÒÞA™a°äq‚’?RÕµ¢?5ªKµÇwNíÃ©w¸ÃÍæ¡öóMõkS/`°¾N3TyÌ¤‰g=ãwKÝ}ªÖ”¢(µóù©é?m)ºcø-Œ[llª4³›ó¬V^ØÅüº—h'¦.Ó<l[[�y§Yv
ãö!\¾kt±×2‘ÔìÚ6¶œ›Êéº1RgÏ£n{’å‚É,Á=zâÍös4uÇð…Ss€Í×-¶‘zXî¬>å¶>�Q‡Êµš²BÊ¦ØÀ¦]ßw�ÒÎFô}ÜóÕ|íÞ×|ˆipX¡ða!LûVÁ)¥l›"Fw–Fk7aÖÓåÔ©9­†Ä¼ÌüÖä�™oŠ§c+&rm>B®ìé•)[}\Ãî‘9•#à#üòÂÃ¶øxBÌÔ@¬Ab°Imõëq�§”@Ø÷?©âÇº¶W9^íÏÊ¯>ó¬Ã‚—ñƒ¼c j×µ¹Ÿ•_wi–~æ¾û4.Õ$dÓR~õ!Ï¼áà¯L±—O?&i°4Ó}ªR*¿ äú'}<Zu¦æeBW£Gíi;èŸ~ç×ÞîÑŸZd»IœR)‰©ŠU£Íl^itl¥æ©Ï|ÖÏÃÎuJRaÙ<U¶šô{—z³[¸è:ôkCTªòU”ÉöŽ¤ô7¨k>³_Ý}åÎ}êüŠø„\x9Ïö‡Öã†Ðÿ„�|T;Å&&XÌ¯C�¥Ï}J|vF«î›ÇBh³yéîz«©óJý]Cƒ;<wˆ­†bÓá}8áuyà£\0ÀwÛUtõNP¶LÑMS—ç­¡<-mÏ‚\l70U·Äµü�6ôw˜:lÛ•÷õ8¶¨ÚªŠ’ÿ°±û tã
gÇQÄ³„¬¾Ì�ª%ïê‚­¾'"FõUQûšˆo¦Ý�e³ÖÓÅv=Ð³•5ÎÄK\E¹Íõ‘6tÕ�04+qQÿE´ºm.™-±U¡ä\‹0!½š+æëÛµÀ­n­âKA°ÍÇÓåYÄŽÖj¥èWµ}çˆxÉ„û~Ìè*‰ëkºòÊS]2©×‚zí>�n•)øGïjT¯íçðGúZ¯jÜØŸE÷Fä{Z6æ´·O£ÕÇ7gDýë:²aopèb&Ø¾ÈQŠ#¨W$˜<ø�ÅFÈóÎàÒ2ÝC©rV€JbY£UöŒ÷Öí­C·¹‚¯ÛãûaÌ‡h›Ð>-Í¶€~D Ô¸‘;àß EÛ.ô´ûàí•æ1[Q|ß¡:ÂÝÅùï9JgØ9Zgé<ê”ÍÖêÞÜs'­ßgÝ:�íàP/u¾k¹qâŽyÏ1÷¤Óúl<Ñœ¤É­L”LBÉ5Aç[oœ¶¯Âé¶¼~E¥¸àòå/³+/œ½íƒõÁ1\–#O†*_)#G—Õk3,®¥ìÆ Ç½¸áqí,¥HÜ‹±U³[¨^…t¼rêåZál(÷„‡n,ïŽC�é�Ûw[Øô±+û¥�†.MxLÅÞ!¥|šé™_?ò|šÉÓGh±•š^C"%½-Æ†‚oø[C×î°Õáb|ÑÖ°¨9µ/N^Á¬·ù×‹þX¾¥�Þ­á¬¾+\byÐ!î©onì">üú`)´¯�Ën1ùIÑ�Ha‡Ûa]¬’*oÜønã~iÝÉx[ðþýœéKÝ™½»Ž¸Öb£ünq³<xœ}SÁŽÓ0½÷+†[«Ý¸‰Ë®8ìJ,ì…]±Ej'É$±êx"ÛIZ!$>‚/äK;MY„ÄÍñÌ{~óÞd½®ù*ïµ)áEUéÅb½†G,öX´¨-üúñ,Ûìîî¦¾�F`‚’*ìMX)Fìt&Âv5ŸÚÕZ)µƒ‚ÛNòí¡’# -áÃÃäÝ-[Uó%x–:E":t†¹‰Å§f×[Ÿï7ï>mb|ì} 0èÂè:GžlP°‘Ú­ábïîCæ|dôä]ˆt^·�9Êd	ï•×@¹£È³5�ñËwœYá´-é b. vØ5p	ŸoäÂš [ZÁÈnïE8A¨ÑCN•Œ à6{!�““i›±%ÈµEwŒt£ìZÜÏm;XŽ�.ÀRÈÖ>z•¬0ÚÊ[2´éÛÙ«­TJÝ³S¤Rxk17TBtãx’Ò³'Ÿ¯D¢ Ãõ)+ÉD‡ã|ÐÜ'/']ó&HM¢±"Z-
¶’É³÷ÞLÏ%ÉjÙ¤§9
A¢„�q7Ëùk×®Ò•8Õ9–vŸÿ_–{[4TÎ›•cèd$½F2‘®”°lÙ„ýp”³‚eÜœø©gî ��–{++ùñìÏØ�=ËPt6wR.SJc¿~ù
®�ŒzHÑ�ä|ú»<Ñ¢šÕjQÉÿxµÜÂ—¯²àl*O—+øö}ñ²ìMK²ýxœ�Wín7ým=Å�ºAG‰5j»ûËYwaËNë]×6,] dj†#±¦H•äHQ}ˆ}†}°>ÉžKÎHc7‹1 �D^Þ�s?f4šÛ£Y­tIU¥z½ÑˆnDñ æ’–Búý·ÿÐÛ·”DÖÂ)aB1–üi!MÜ	´QaA÷Ã æž�î),$É+m�t¤•yðqé¶ö�|AZÍäO]˜¶ÂSÏÅ=e+gËº�%Í¶t_7·�Ã¡“Z
/ï	æ#�0%£¹Ú¤‹Nµ-|aë0t~¯‰—n­
é)ój>ÄkUJwÈÇ£­"žÞ’p’á|!44 JÜõKáÂ°°&8Q„áZ:U)>ÎùCZÖ†-à¸¡˜Ï�œ‹`Ý!­•¯…V¿J7 ‹«áÍíõø|2Á² bnsº²äëô�n^ÊXúÎ’“§‚ôG$E±ˆ%¾ôØ:ùSBŸíÊ*’ýÖ�
xlÝÙey5¨¥„i8TÂkV3ZX(O^™¹–4SF¸mÛSvõ�²•hþ²Oƒc�H0&X±ßo)1`Ô;DÆX3dJUBë¸v¬�t|j«*Çál´'æ‹`oIÐJÃ·‘5»òQžç÷TØåJiàütq÷ýõ�w‘;¢ÑÊI/™¹«»A÷W½/àº<{{yòÝäˆ†—ù8¹Ÿ]Ü>2¯F¦Ê0jmêçTee&[ävÌD~Šé”…‰×Û/éÃ¨¨�ÆO»’Æ{þæàäè$�
ðŠÞ’‘²ô9Ô-®'T9±”ë�L…ûh"£Âvtw9‰Ø`‰˜)�¥71¤·z=*^¿nœ]±ë7ÆZË‹U­uJ4ÖjÔè”Óµ¡Keê‘'Œ7ŠåTPÌ?D£ÉÏ93Õ,Œð��ìUü”yrp)ÜÞûY'Õð…•î,Û™Ô]ÃÛommJxäîJG×�­©Ô¼vq;]U¡O^èCïÔK|J�«&1°çùw�t
¶×ûB™B×¥¤¿ãÒ+_|ût‘å5vÍÊ9ëRìÇtrzÁEÇº°sÐÈ»bÄâyŽ@1;°�¢´›¦lVvò¼›ËÏ²˜K–“¡v8úe—?þ{Š´š^ÿk€C8©EmŠ…,óžü€[€ƒúð„¾ñþ)
á´-„jh¹W`{uú³·fðæO@�ËÙÚª‚¯F=µdË©?î÷ÚïYï ¯í¼�J”¨d¿7ˆ~ƒgÎ�˜1ÿ"©Dô§'×�€HÚ9m(«O›P”®�AÖä½¤vêYTËö‚[éQ™ºjã“K¤kåìòY„øâE½AâÄôy¯‚;÷`¥qúØ;ðh„Ð«Q`ÀkWŽ¯ŽzÍ­}ûÐo–¿î.ÏD9LNo·¿én¯„ór(™fíþ_»û¥òÑøvóoÝÍÈ±ÝáRVFtjó`À2l>ö[ÇMC}r¼Á…»¥&s•ýÄüwšPN'eÉþ­(£)îK2ÖZH‡¢£kÏ6B/>þþÛiÆ�»m&�€0Ý!ÍP¥\½®¤€Þ»®3æ–”»ÔèìRÆµÛó“³Îóe9Èé¬ñÍ~ ðAiÍh)M™
©¸]Î¬îäµyu~{;=»˜œœ^žŸr7Š½30*ðŒV>D8”ÙíÎGàŒ5¥âº„ÙÆ)‚ˆÞžúm3!ù�ÜE”á¹Á•èeÑX[5ìO]·±ÛÓ†}Øt=néy‰ñ$fÇµw‰›ï™½Ù«qÎ©=H¼5ûÝ)¨DQª §rô±õÅ8ÿÓú1 ÇÃÞ7E�†<9eì„å6ãôB’0Cž†þ”m)HaE¬þÿ¼>WcF›¶hÓ­£|?t€ÿãßg]Èh]øOŠñºÏ�ß£M÷hÝëöÓçs“>çº=ZsG*q™2i3¬i@>Î®Í(C;læÜfFjR…§ð´ˆåâYQIoq¨“!pE¦N®¯ZØÝ¥Nð×dŒ+c‰²t£˜ä<o¥\¤³SÂ¬ƒÒp[	_Í“>wÎØ¸ex¯É°zEiòd@ÍÔŒÞ¤¦ÝL´qéK.-õŒ1v†vÕŽÔOz~Û¹£T"ð¬'ªÝ4ÀÉ‘ŒmÇ}w°›š|§hkPia·±¡)�|ìWµ¢ïïînbU’Fº¦}=�Qæ×…§wïÛà4ë±mUÖÑ•ŽŽŽ	)fæ(Œ�ƒÊ’}à�N½yçó+t„÷ØW½À>‹ }ç7(A¡Êúï þž_m:qåkZ
¼ü…2'w¯£)æÌ„¦êýcÐ‡Bñšcs
cÎ“øþˆÀõ®¥sÒÄç“†1‘0/Ž©ßOªEÙc�|<‰E2{*:h`c§g[MÆ'	6˜Q:¡�óÊI™¥é&¿aã8CøD‹ôG_Ä€ÐËøÎvôè·ü#{Yî-å'Çò>|6€÷£Æ uîÿ³÷VÅ»¥xœ�VÁŽÛ6=¯¿bàKä­%7AO›¦€“ì&A7¶a;h‹¢Z¢mv)R%©µ�"@Oý€¢_˜/éJZÛÝ¢ƒe‰ÃÇ™7ofT‰üNl$•B™^o4¢µ³&dKŸÿú—>ªŠ”§°•t3ŸN–ôv¹œ‘³u�Žì:.È}¥­ÃóJá@gj$oÏÆUEvg�ª^i•“V>H#]Fïå¶¬¬—0ØY)³ñCR†¬+¤»jÑˆžf4•}_9¹VûF—DNÞKçeZ9»WŒ`IŠ|‹Ýü*—ÞÓKmó;ŸÃãÔù	——î^å’’ÒÖ&,š§Yƒ2ìV=Xd4—v×É‚nÞÍKò–nÔJº'G<Ým¨™!ç@€dŠ¹—ìS0¯}8žðÞ3„‡ˆrÞ™
­éóßÿ´ôâtÆzcS#‚º?a<aƒËmUÆ1È÷õþÁ¥U­t`9³ˆäŠ³1[È½tCÚ8Qm‡ Pè J9$Q”Ê Þq!ª€`!û€ÆB¸W¢É(LdˆZ`•°>Z­¨¬÷,˜H+0v[«]›|+ÌF]¼Ë-ŒZaÝ^�Ëô§éüÇF{WgªzâÉÈ0âr=	'ávíeÁPPÀ
´”ÃÇìÒ�”¿VŽ%Øˆ—!ÄkŠÖïØÂ@-ú7Ôª)ƒ—ˆÔkÜ¤(»ž¨+·³Nm4èD•
Âmd ­õáª².DÆŒ�Bçô0âÛø¦ÅY½3vç!ÆÊ,‹ëÊÇ®à4AùÆ¦Z0¹ ziïû‡eÐÜSe<,é]ô;þú=<lTØÖ«E7BJ>Z¡F ¡ß»Ðõ^Û�àÕZ�ð¶ßÄÞÀš*n8I\Õ¹5ð¦ÎC“Gæ2&°í­dßfy]k}HwÊ5Y{�0&�2Á{+Mü‹ÚÄ½'þ6êäÊNÑÏÉßç±Y|¸²údœ¤èAÈÞYfšü:jg°¥ëSŒ÷…VE	²Ûø4Èzk°~NG£�µøÒÒ\cìýú[çVÛgtÙ5Æ?{·«±°&r—ðý•5kµÁÚl&¢”Wt~õ‘™´£°?„a,÷¹GÏ<1<3z­¼Xi¹€$C]½;èü(7WK^¾µ›ÍÙîæj„�ë—¶¨µìO€Øú	ò¸ o/A�V†Ë%¯�
ÚF�bUIW*ï¹½šÎC*•sÖqDº‘ekqvÐà¢ƒØAËi^cŠXCœÄ÷G-=w¸\×Å†k—›7W&7ûàeÂéJò†÷Wa? Ég3ý9ZghˆKú?§àéòPÉtZ‘s¼Æz£Öëþàñ–‡�Ú¾¾žüòØp.×8Uºtf!¬¢|T0:ÔŠÝm¥Isg½oß<Æç¬éè¤³:kmwé´1ÞåWîx/ÃÖÑ×7×Ë!Í¦þý€Ÿ××·×Ëë!MgËwÓÉâ+›ÅxJà�Æ5Nrê£`r"˜ZSž5$zñ‚úÝQ1MAÂfb[¨äÙ·ßñÎO½“U^S¦–Éà(¿äé€f ¸›îm¹w=v^wœæ+¹¶mã8Ž]o#Ôÿ?5vŠµ8LièêŸ	´ÖT|çÍ³Aì0qÇøYçþüøìÑ€�Pscy>ì!~tµ8•iÛ4�ç„Œ$ý¤€>y‡¨Ug�#Vøà#Þ”Q3†Ž•ƒ±²B˜T¨Â<Á·™ªlë¨Ã>ò1¶¾‡Ý¦æ½O½ÿ òn«å‰&xœ»-rSdÂZ¦2Ã�28€ÔfiNF _¹U¶¡xœ¥XínÛ8ýï§¸p‹I‚1¥8Ùf[g¬c«‰1ŽmØN'E·ph‰¶9‘D�HÙqgìCìî“ì%õaÙ™LØ EC‘¼ºçž{”7pñg>ôÓ'`Oq – é’Y0áÑ2`p-`Î#šlÀÂ9ó}<>µÀ#Ÿ=áñko`™ÐxTBÀç	M8“tÚÛÂ†+ÜoEy$­-8	°hkšÈ ‘1K$—
-Ù>UôÐ
>–Ã|	E)thÏÎ�øFÉ”l@$ðÌ<å�cè¢U«µÇ×p=œ}rÇ“Þpà4­³ól0ìºåÓ³3óÌ½õ‡cw6vGCg¥T,[¶½DÏÓ¹å‰ÐÒ§·óY¸qpë£¢[æaoÐuïÝñwLåÉ+MínUL]�Û£›ï2™/Í7r#˜O‚?0QXQh¶ K‘Z1]B³W{ƒ‡öÂ.±^ö¤¬_$<ˆTÅ©jÁ‘TXˆˆ=Àd2Ç:ó
OãžâZB"Qðßÿ¤€‡8ŠÃì¥à&1ùSˆá§ØhÞ¶`Š¥3#u-en<¯5(‘áÖœÂR´Ìâ4<Hc©FÃ��+WÃÈ<<zhÀ&ÛÔî§s�âk‰¶š¡¹bA ‹D„¥WvæOîuv`ÃÎFPK�´ãÖWl,wðé�(�N,xb2oŒ�"xg‘"’ûØo?¯XTPº`Ò¥vÄ¼”äù•i¬ÿo An„å]êÆ¨¦~ø¼¬Ki—*³>«<0]¨Êx<Ájë'T1Ë äJ$4Y‚Ï4°rEcfîˆXqÑ M^±ÜYªcÝÜÇ"
¶�J¬_@¥�²0ÿ JeBN,3 C†
DˆI7µÑ^…ÛgˆŒHrìéšà�gU…XƒÜl¢+¨¹Èª}oqßg­·¿U‰ä™Ð æƒöD{)¬÷f9$�ŒŽÜéÏÃñO³^×ùpq~ñaïá }ë:uÍÎ¦6"y¬ïmwîÆcwÐù<›|¾½ö�þÝ}m|7ÀÎ|êûØà‘ õ0VÍúN·76Ð5eR¶X`¢[ElìuÉVÇ]t+Ý¯ª½Ú Ñ!ˆÅXP ðC.¥^j’@x�Ž3«çG±YpCÓÑ˜@K;XÒhÁ—'$‹èH£¦Rf$™Úv!zÅ°¦Ë-¼†S!>‹ÕÊiâoÞÊyû[%ÿÏP]Ž†Ï`Áï¿cà)«aëbšï§³ÑÝU¿×™µG½ÙU{âÎFíé�c¯›]&/ŽV*Š )Ï¯4UÞÕË×—5?¸u€…¿` Íÿc¢ëvz·íþÄi¾Ïë–Ž¦Ø‡��>�¼!cê1¤¹oÌyßüpfÊ„ðÈpëQë�_¤ˆàë¥.wÿª�ùñ0Áú B†ÎHÈh†ß°ÁÅ²‚Íö�E;¨Â—Þ"ÒKxŒ=_\Éú9/ïeþ†¯0¢|r�Â†7D….î3™L=/“†û�I¤Ã¿š™X 6ÐŽ>K@Š5î½À«ñ<Uå"M�[c­,0°¹Ž9¦¨œÐ'Ý òë¡1£"¬•Òšª2ÃÐÈùßÏÈ|«²9_Ì(mÎwê‰ªŽÔXŽ«9>@–¥ñIÖAá£Ï q™™!qVÐE)s(ù<ÀÇÆÞ	�|¬ç"2Ó†E—I‰<ºiUÍ“ûBSÊ¥¥´/ƒÌªÐz>[¿ÊŽža*$Œ5È_®˜ùU³æŽ6eâý†©8§¦êüs™2„eÁï+CyÆ³»¥6YÖ÷ÜÑ’Æ!šUT%i€8÷·šo•‘fôWTP	†?k4t[N·��Ð(Æ V47SU*¬œ½ÇT�Ý
bÕŠª£Lêk§`¥Jç'¯©!Có‡j¨‘Éª¸hOiÆWîoæ	rG2ˆ#7i-à¨mÌ„¦ßÐ:Â)‚kH‘&sŠ
¥SŽ­ÂØ.ÍYå
K�xpTPWÆo(
¯T»ö«ÆÁ¨¿=Þxúö?þìôIÈRAóôôO0o% þ¥¸úµˆ_ÏÛ}Ãâ"ê—[˜J2þÇ­}„wwWY ÙŸù´ïˆþtÈÕ…ß€GÆâ\Õ…\ià¼@m	ÊŠ·~¤Ù	í<m­ŒR,�2Õô5Òp„×Ÿ–(A�oQë N£o‚r'KÀ=dû®O­ëTëSŸ/,Ñ:p4¡Že	öÂîá7üZ@˜£aD("ÈwðÄ"N´#‘Ò,™ê�#J7p¶ÎU¿q03ˆøTAjÁªÑz=œÜÝv¯±À™ó˜ƒ<÷"J„”£òs'¡ ÙK5�:È¤î }Õw»H"zÕùØo_Oœ:éÎúH…îÇ^ß½øÛl2¼wÜúÞ[s6¹gD%<4¨Œ³H9	üE@—ÍJ  ÷ ¿6­µþˆ’VAçõÝ†+JÉmÌ”óV)àw#"›­së¬ùs{”x,Q8c<S‚œ¼>—°Á–ÍhÐè„Ð)ö¾+v*ÛX™`G æ�¾�Ÿ+8»@R¸x÷îü¬ÄeínâŽw«OÃþÝ­›ýÃˆ¡n{Úž!/;æÜL§£Y»Û;­÷§Nk¨<‡Ìï7n»?½éÜ¸�Ÿ´‚ÁvIÖ4pÎO1ÁD'vÞéöB¢HÌŽUiâ¾Îpç¶k‚òë�€þ[AËÎ"Z	©ÌëìR V{B¶n¢‹ÓñçÑ°7˜Â—zEýkí	òFç�Kx·Hþ–$É# Lux Explorer data-layer image (v1.3.0+).
#
# As of v1.3.0 the Go binary is API-only â€” it serves /v1/* (indexer +
# blockscout-compat REST), /health, /envs.js, and per-host brand SVGs.
# Every other path returns 404 and the IngressRoute routes it to the
# per-brand Blockscout FE Deployment (ghcr.io/{brand-org}/explore-{env}).
# Built from luxfi/explore via Dockerfile.branded â€” see ~/work/lux/explore‘Û“U�³;
¬³ÈÄ2³�‰om—ˆâî‚FxâF¹Éü�package main

import (
	"bytes"
	"embed"
	"encoding/json"
	"fmt"
	"io/fs"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"V
)

// staticFS embeds named brand assets only â€” icon.svg and logo.svg
// fallbacks.‘.no longer serves the Blockscout
// SPA; each brand's FE runs as its own per-(brand, env) Deployment
// (ghcr.io/{org}/explore-{env}:vX.Y.Z)‘Úcthe
// catch-all to that FE Service. The Go binary keeps /v1/* (indexer +
// blockscout-compat APIs‘�)the per-host brand
// SVG endpoints â€” e‘¿.
//
//go:embed all:static
var staticFS embed.FS

// Frontend serves runtime config (/envs.js) and per-host brand assets
// (/icon.svg, /logo.svg). Brand assets read from disk on every request so a
// deploy can swap them without rebuilding the binary.
//
// Static directory override: if cfg.StaticDir (or $EXPLORER_STATIC_DIR)
// points at an existing directory, brand-asset lookups prefer the on-disk
// copy over the embedded FS. This lets a ConfigMap-mounted SVG bundle
// override the in-binary defaults without rebuilding.
//
// SPA serving was removed in v1.3.0 â€” the catch-all `/` route is gone and
// any path not explicitl*y registered returns 404. The per-brand
//“= owns the
// SPA surface.
type Frontend struct {
	cfg      Config
	registry *ChainRegistry
	root     fs.FS // embedded brand-asset FS (always present)
	overlay  fs.FS // disk overlay (nil if cfg.StaticDir not set)
}

// NewFrontend returns a frontend handler bound to a config and chain registry.
func NewFrontend(cfg Config, r *ChainRegistry) (*Frontend, error) {
	root, err := fs.Sub(staticFS, "static")
	if err != nil {
		return nil, err
	}
	f := &Frontend{cfg: cfg, registry: r, root: root}

	dir := cfg.StaticDir
	if dir == "" {
		dir = os.Getenv("EXPLORER_STATIC_DIR")
	}
	if dir != "" {
		if st, err := os.Stat(dir); err == nil && st.IsDir() {
			f.overlay = os.DirFS(dir)
		}
	}
	return f, nil
}

// readStatic returns the bytes for `name` from the overlay if present,
// otherwise from the embedded FS. Returns nil if neither has the file.
func (f *Frontend) readStatic(name string) []byte {
	if f.overlay != nil {
		if data, err := fs.ReadFile(f.overlay, name); err == nil {
			return data
		}
	}
	if data, err := fs.ReadFile(f.root, name); err == nil {
		return data
	}
	return nil
}

// Mount installs the data-layer endpoints â€” /envs.js, /icon.svg,
// /logo.svg, plus any extra brand SVGs present in the overlay (e.g.
// /icon-zoo.svg, /logo-lux.svg). No jcatch-all SPA route: any other path
// returns 404 so the IngressRoute layer routes it to the per-brand
//“.
func (f *Frontend) Mount(mux *http.ServeMux) {
	mux.HandleFunc("GET /envs.js", f.handleEnvs)
	mux.HandleFunc("GET /icon.svg", f.handleIcon)
	mux.HandleFunc("GET /logo.svg", f.handleLogo)
	for _, name := range f.brandAssetNames() {
		mux.HandleFunc("GET /"+name, f.handleNamedBrandAsset(name))
	}
}

// brandAssetNames lists every per-brand SVG/PNG file the overlay (or the
// embedded FS) carries. Used to register one explicit handler per file at
// startup so the rest of the path-space cleanly 404s.
func (f *Frontend) brandAssetNames() []string {
	seen := map[string]struct{}{}
	var out []string
	collect := func(dir fs.FS) {
		if dir == nil {
			return
		}
		entries, err := fs.ReadDir(dir, ".")
		if err != nil {
			return
		}
		for _, e := range entries {
			n := e.Name()
			if e.IsDir() || n == "icon.svg" || n == "logo.svg" {
				continue
			}
			switch strings.ToLower(filepath.Ext(n)) {
			case ".svg", ".png", ".jpg", ".jpeg", ".webp", ".ico":
			default:
				continue
			}
			if _, ok := seen[n]; ok {
				continue
			}
			seen[n] = struct{}{}
			out = append(out, n)
		}
	}
	collect(f.overlay)
	collect(f.root)
	return out
}

// handleNamedBrandAsset returns a handler that serves a specific named
// file from the overlay (preferred) or the embedded FS. 404 if neither
// has it.
func (f *Frontend) handleNamedBrandAsset(name string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if data := f.readStatic(name); data != nil {
			w.Header().Set("Content-Type", contentTypeFor(name))
			w.Header().Set("Cache-Control", "public, max-age=300")
			w.Write(data)
			return
		}
		http.NotFound(w, r)
	}
}

// handleEnvs returns runtime config the SPA reads instead of build-time env
// vars. This is what makes per-network customization possible without a
// rebuild: deploy the same image, mount a different chains.yaml, get a
// different brand + chain list.
func (f *Frontend) handleEnvs(w http.ResponseWriter, r *http.Request) {
	hostBrand := f.brandForHost(r.Host)
	hostSlug := f.chainSlugForHost(r.Host)
	chains := f.chainListJSON()
	// When the host pins a specific chain (e.g. explore.zoo.network â†’
	// zoo, explorer.zoo-test.network â†’ zoo), promote that chain to
	// default and demote any other config-level default. The SPA's chain
	// switcher honours the "default" flag.
	if hostSlug != "" {
		for i := range chains {
			if chains[i]["slug"] == hostSlug {
				chains[i]["default"] = true
			} else {
				chains[i]["default"] = false
			}
		}
	}

	current, peers := f.networksForHost(r.Host)
	env := map[string]any{
		"VITE_CHAINS":          chains,
		"VITE_NETWORKS":        f.cfg.Networks,
		"VITE_BRAND":           hostBrand,
		"VITE_CURRENT_NETWORK": current,
		"VITE_NETWORK_HOSTS":   peers,
	}
	body, _ := json.Marshal(env)

	var buf bytes.Buffer
	buf.WriteString("window.ENV = ")
	buf.Write(body)
	buf.WriteString(";\n")

	w.Header().Set("Content-Type", "application/javascript")
	w.Header().Set("Cache-Control", "no-cache")
	w.Write(buf.Bytes())
}

// handleIcon serves the per-host or default icon.svg from disk.
func (f *Frontend) handleIcon(w http.ResponseWriter, r *http.Request) {
	f.serveBrandFile(w, r, func(b Brand) string { return b.IconFile }, "icon.svg")
}

// handleLogo serves the per-host or default logo.svg from disk.
func (f *Frontend) handleLogo(w http.ResponseWriter, r *http.Request) {
	f.serveBrandFile(w, r, func(b Brand) string { return b.LogoFile }, "logo.svg")
}

// serveBrandFile resolves the file path from the matched brand (per-chain or
// default) and sends it. Falls back to the on-disk overlay (if configured)
// then the embedded SPA's named asset.
func (f *Frontend) serveBrandFile(w http.ResponseWriter, r *http.Request, pick func(Brand) string, fallback string) {
	brand := f.brandStruct(r.Host)
	if path := pick(brand); path != "" {
		if data, err := os.ReadFile(path); err == nil {
			w.Header().Set("Content-Type", contentTypeFor(path))
			w.Header().Set("Cache-Control", "public, max-age=300")
			w.Write(data)
			return
		}
	}
	if data := f.readStatic(fallback); data != nil {
		w.Header().Set("Content-Type", contentTypeFor(fallback))
		w.Header().Set("Cache-Control", "public, max-age=300")
		w.Write(data)
		return
	}
	http.NotFound(w, r)
}

// brandForHost returns a JSON-shaped brand for /envs.js. It strips file paths.
func (f *Frontend) brandForHost(host string) map[string]any {
	b := f.brandStruct(host)
	return map[string]any{
		"name":        b.Name,
		"coin":        b.Coin,
		"accentColor": b.AccentColor,
		"iconUrl":     b.IconURL,
		"logoUrl":     b.LogoURL,
	}
}

// brandStruct returns the Brand for a request host: a chain whose name or
// slug matches the hostname wins; otherwise the global default.
//
// See hostMatchesSlug for the list of supported hostname shapes.
func (f *Frontend) brandStruct(host string) Brand {
	if c, ok := f.matchChain(host); ok && c.Brand != nil {
		return *c.Brand
	}
	return f.cfg.BrandDefault
}

// chainSlugForHost returns the slug of the chain pinned by the request host,
// or "" if the host does not name a chain. Used by handleEnvs to promote a
// per-host chain to the SPA's default selection.
func (f *Frontend) chainSlugForHost(host string) string {
	if c, ok := f.matchChain(host); ok {
		return c.Slug
	}
	return ""
}

// matchChain finds the first configured chain whose slug appears in the
// request host. Matches these hostname shapes, in order of specificity:
//   - "{slug}"                             â€” bare slug (curl localhost)
//   - "{slug}.tld..."                      â€” {slug}.lux.network, {slug}.hanzo.ai
//   - "explore-{slug}.tld..."              â€” explore-hanzo.lux.network
//   - "explorer-{slug}.tld..."             â€” explorer-hanzo.lux.network
//   - "api-explore-{slug}.tld..."          â€” api-explore-hanzo.lux.network
//   - "explore.{slug}.tld..."              â€” explore.hanzo.ai, explore.zoo.ngo
//   - "explorer.{slug}.tld..."             â€” explorer.hanzo.ai, explorer.zoo.network
//   - "api-explore.{slug}.tld..."          â€” api-explore.hanzo.ai
//   - "explore-{slug}-{env}.tld..."        â€” explore-hanzo-test.lux.network
//   - "explorer-{slug}-{env}.tld..."       â€” explorer-hanzo-test.lux.network
//   - "api-explore-{slug}-{env}.tld..."    â€” api-explore-hanzo-dev.lux.network
//   - "explore.{slug}-{env}.tld..."        â€” explore.zoo-test.network
//   - "explorer.{slug}-{env}.tld..."       â€” explorer.zoo-test.network
//   - "api-explore.{slug}-{env}.tld..."    â€” api-explore.zoo-test.network
//
// First chain match wins on ambiguous hosts.
func (f *Frontend) matchChain(host string) (ChainConfig, bool) {
	host = strings.ToLower(strings.SplitN(host, ":", 2)[0])
	for _, c := range f.cfg.Chains {
		if hostMatchesSlug(host, c.Slug) {
			return c, true
		}
	}
	return ChainConfig{}, false
}

// hostMatchesSlug is the pure string predicate for chainâ†”host matching.
// Kept separate from matchChain so unit tests can cover it directly.
func hostMatchesSlug(host, slug string) bool {
	if slug == "" {
		return false
	}
	if host == slug {
		return true
	}
	// Direct slug-as-subdomain: zoo.lux.network, hanzo.ai.
	if strings.HasPrefix(host, slug+".") || strings.HasPrefix(host, slug+"-") {
		return true
	}
	// Brand-owned domains: explore.zoo.network, explorer.zoo-test.network,
	// api-explore.zoo.ngo, etc. Each "explore"/"explorer" prefix combines
	// with "." or "-" as the slug separator.
	prefixes := []string{
		"explore-", "explorer-",
		"api-explore-", "api-explorer-",
		"explore.", "explorer.",
		"api-explore.", "api-explorer.",
	}
	for _, p := range prefixes {
		// {prefix}{slug}.tld  â€” e.g. explore.zoo.network, explore-zoo.lux.network
		if strings.HasPrefix(host, p+slug+".") {
			return true
		}
		// {prefix}{slug}-{env}.tld â€” e.g. explore-zoo-test.lux.network,
		// explorer.zoo-test.network
		if strings.HasPrefix(host, p+slug+"-") {
			return true
		}
	}
	return false
}

// networksForHost returns (current network label, sibling host map). The
// SPA renders a "Network: mainnet â–¾" dropdown â€” when the user picks
// another label, the SPA redirects to the corresponding host. Hosts are
// derived purely from the request host: explore.zoo.network (mainnet)
// has sibling explorer.zoo-test.network (testnet); explore.lux.network
// has sibling explorer.lux-test.network; and the reverse.
//
// Host shape rules:
//   - mainnet: prefix is "explore.", brand domain is "<brand>.<tld>"
//     (e.g. zoo.network, lux.network, hanzo.ai). Sibling is
//     "explorer.<brand>-test.network".
//   - testnet: prefix is "explorer.", brand domain is "<brand>-test.network".
//     Sibling is "explore.<brand>.network" (or .ai / .ngo per brand
//     when known; we default to .network because all the actual
//     deployed testnet hosts are *.network).
//   - lux.network (no brand prefix, generic explore.lux.network) is
//     special-cased: mainnet=explore.lux.network, testnet=explorer.lux-test.network.
//
// If we can't classify the host (admin tooling on localhost, internal
// service-mesh hostnames), we return ("", nil) and the SPA omits the
// dropdown.
func (f *Frontend) networksForHost(host string) (string, map[string]string) {
	host = strings.ToLower(strings.SplitN(host, ":", 2)[0])

	// Find the brand stem and current env from the host. The two
	// shapes we support are "{prefix}.{brand}.{tld}" (mainnet) and
	// "{prefix}.{brand}-{env}.{tld}" (non-mainnet).
	brand, env, dotted, ok := parseBrandEnv(host)
	if !ok {
		return "", nil
	}

	// All deployed testnet/devnet hosts are *.network. Mainnet
	// brand hosts vary: zoo.network, lux.network, hanzo.ai, hanzo.network,
	// zoo.ngo, etc. For the round-trip from testnet back to mainnet
	// we need to know which mainnet TLD the brand uses. When the user
	// is already on a brand-owned domain (explore.{brand}.{tld}),
	// preserve that TLD so the mainnet option is idempotent. When the
	// user is on the lux.network-prefix shape (explore-{brand}.lux.network)
	// or any non-brand-owned shape, default to ".network" â€” every brand
	// we ship has an X.network mainnet alias.
	mainnetTLD := ".network"
	if env == "" && dotted {
		if dot := strings.Index(host, brand+"."); dot >= 0 {
			mainnetTLD = host[dot+len(brand):]
		}
	}

	mainnetHost := "explore." + brand + mainnetTLD
	testnetHost := "explorer." + brand + "-test.network"

	peers := map[string]string{
		"mainnet": mainnetHost,
		"testnet": testnetHost,
	}
	switch env {
	case "":
		return "mainnet", peers
	case "test", "testnet":
		return "testnet", peers
	case "dev", "devnet":
		// Optional devnet entry; only surfaces when present.
		peers["devnet"] = "explorer." + brand + "-dev.network"
		return "devnet", peers
	default:
		return env, peers
	}
}

// parseBrandEnv extracts the brand stem and env tag from a host. Recognizes:
//
//	explore.{brand}.{tld}                  â†’ ({brand}, "", dotted=true)
//	explorer.{brand}.{tld}                 â†’ ({brand}, "", dotted=true)
//	explore.{brand}-{env}.{tld}            â†’ ({brand}, {env}, dotted=true)
//	explorer.{brand}-{env}.{tld}           â†’ ({brand}, {env}, dotted=true)
//	explore-{brand}.lux.network            â†’ ({brand}, "", dotted=false)
//	explore-{brand}-{env}.lux.network      â†’ ({brand}, {env}, dotted=false)
//
// Returns ok=false on hosts we can't classify (localhost, IPs, internal
// service-mesh names like *.svc.cluster.local).
//
// `dotted` distinguishes brand-owned hosts (explore.zoo.network â€” TLD
// belongs to the brand) from lux-prefix hosts (explore-zoo.lux.network â€”
// brand is just a subdomain on lux.network) so the network switcher can
// pick the right mainnet TLD for the sibling URL.
func parseBrandEnv(host string) (brand, env string, dotted, ok bool) {
	if host == "" || strings.HasSuffix(host, ".svc.cluster.local") || strings.HasSuffix(host, ".local") {
		return "", "", false, false
	}
	stripPrefix := func(s, p string) (string, bool) {
		if strings.HasPrefix(s, p) {
			return s[len(p):], true
		}
		return s, false
	}

	// "explore.{brand}{maybe-env}.{tld}" / "explorer.{brand}{maybe-env}.{tld}"
	for _, p := range []string{"explore.", "explorer.", "api-explore.", "api-explorer."} {
		rest, matched := stripPrefix(host, p)
		if !matched {
			continue
		}
		dot := strings.Index(rest, ".")
		if dot < 1 {
			return "", "", false, false
		}
		stem := rest[:dot]
		brand, env = splitBrandEnv(stem)
		if !isValidBrand(brand) {
			return "", "", false, false
		}
		return brand, env, true, true
	}

	// "explore-{brand}{maybe-env}.lux.network" / "explorer-{brand}{maybe-env}.lux.network"
	for _, p := range []string{"explore-", "explorer-", "api-explore-", "api-explorer-"} {
		rest, matched := stripPrefix(host, p)
		if !matched {
			continue
		}
		dot := strings.Index(rest, ".")
		if dot < 1 {
			return "", "", false, false
		}
		stem := rest[:dot]
		brand, env = splitBrandEnv(stem)
		if !isValidBrand(brand) {
			return "", "", false, false
		}
		return brand, env, false, true
	}

	return "", "", false, false
}

// isValidBrand restricts brand stems to alphabetic (or digit-containing) tokens
// at least 2 chars long. Rejects numeric-only stems (IP addresses), empty,
// and stems beginning with a digit (so "10" from "10.0.0.1" never matches).
func isValidBrand(s string) bool {
	if len(s) < 2 {
		return false
	}
	if s[0] < 'a' || s[0] > 'z' {
		return false
	}
	for i := 0; i < len(s); i++ {
		c := s[i]
		if !(c >= 'a' && c <= 'z') && !(c >= '0' && c <= '9') {
			return false
		}
	}
	return true
}

// splitBrandEnv splits "zoo-test" into ("zoo", "test"); "zoo" stays as ("zoo", "").
// Only recognises known env suffixes (test, testnet, dev, devnet, local) so
// hyphenated brand names ("api-zoo-exchange") still parse as one stem.
func splitBrandEnv(stem string) (brand, env string) {
	for _, e := range []string{"-test", "-testnet", "-dev", "-devnet", "-local"} {
		if strings.HasSuffix(stem, e) {
			return strings.TrimSuffix(stem, e), strings.TrimPrefix(e, "-")
		}
	}
	return stem, ""
}

// chainListJSON returns a SPA-friendly chain list from the live registry,
// not the static config â€” chains added via mDNS/admin show up immediately.
var chainListMu sync.Mutex

func (f *Frontend) chainListJSON() []map[string]any {
	chainListMu.Lock()
	defer chainListMu.Unlock()
	out := []map[string]any{}
	if f.registry == nil {
		return out
	}
	for _, c := range f.registry.List() {
		out = append(out, map[string]any{
			"slug":    c.Slug,
			"name":    c.Name,
			"chainId": c.ChainID,
			"rpc":     c.RPC,
			"coin":    c.CoinSymbol,
			"type":    c.Type,
			"default": c.Default,
		})
	}
	return out
}

// contentTypeFor maps a file extension to a Content-Type, defaulting to
// application/octet-stream.
func contentTypeFor(path string) string {
	switch strings.ToLower(filepath.Ext(path)) {
	case ".svg":
		return "image/svg+xml"
	case ".png":
		return "image/png"
	case ".jpg", ".jpeg":
		return "image/jpeg"
	case ".webp":
		return "image/webp"
	case ".ico":
		return "image/x-icon"
	default:
		return "application/octet-stream"
	}
}

// fingerprint returns a short, stable identifier for the embedded asset
// bundle, useful for cache-busting log lines at startup.
func fingerprint() string {
	root, err := fs.Sub(staticFS, "static")
	if err != nil {
		return "0"
	}
	count := 0
	_ = fs.WalkDir(root, ".", func(_ string, _ fs.DirEntry, _ error) error {
		cou@nt++
		return nil
	})
	return fmt.Sprintf("assets=%d", count)
}
�~A©ç¶€Œrxg#˜Üü�ìK�net/http"
	"net/http/httptest‘T&testing"
)

// TestFrontendNoSPA verifies the v1.3.0 contract â€” the Go binary does
// NOT serve a SPA. Any non-data-layer patBh returns 404 so the
// IngressRoute hands it off to the per-brand“ÿ.
// Compare to the pre-v1.3.0 behaviour where `/` returned an embedded
// index.html.
func TestFrontendNoSPA(t *testing.T) {
	_cfg := Config{BrandDefault: Brand{Name: "Test"}}
	f, err := NewFrontend(cfg, NewChainRegistry()“Ð:t.Fatal(err)
	}

	mux := http.NewServeMux()
	f.Mount(mux)
“ê9/", "/block/1", "/_next/static/chunks/foo.js", "/some/random/path"} {
		req := httptest.NewRequest("GET", p, nil)
		rec := httptest.NewRecorder()
		mux.ServeHTTP(rec, req)
		if rec.Code != 404 {
			t.Errorf("path %q: got HTTP %d, want 404 (FE Deployment owns this surface now)", p, rec.Code)
		}
	}
}

// TestFrontendBrandAssetsServed verifies the data-layer endpoints still
// work“®	 /logo.svg, and any extra named asset (e.g.
// /icon-zoo.svg) the overlay carries. These are what the FE depends on
// from the Go binary.
func TestFrontendBrandAssetsServed(t *testing.T) {
	dir := t.TempDir()
	if err := os.WriteFile(filepath.Join(dir, "icon.svg"), []byte("<svg/>"), 0644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(dir, "logo.svg"), []byte("<svg id=logo/>"), 0644); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(filepath.Join(dir, "icon-zoo.svg"), []byte("<svg id=zoo/>"), 0644); err != nil {
		t.Fatal(err)
	}

	cfg := Config{StaticDir: dir, BrandDefault: Brand{Name: "Test"}}
	f,+ err := NewFrontend(cfg, NewChainRegistry()“Ðt.Fatal(err)
	}

	mux := http.NewServeMux()
	f.Mount(mux)

	for _, tc := range []struct {
		path string
		want string
	}{
		{"/icon.svg", "<svg/>"},
		{"/logo.svg", "<svg id=logo/>"},
		{"/icon-zoo.svg", "<svg id=zoo/>"},
	} {
		req := httptest.NewRequest("GET", tc.path, nil)
		rec := httptest.NewRecorder()
		mux.ServeHTTP(rec, req)
		if rec.Code != 200 {
			t.Errorf("path %q: got HTTP %d, want 200", tc.path, rec.Code)
		}
		if got := rec.Body.String(); got != tc.want {
			t.Errorf("path %q: got body %q, want %q", tc.path, got, tc.want)
		}
	}
}

// TestFrontendOverlayFromEnv verifies $EXPLORER_STATIC_DIR is the env
// equivalent of cfg.StaticDir. The brand asset must be served from the
// overlay even with an empty cfg.
func TestFrontendOverlayFromEnv(t *testing.T) {
	dir := t.TempDir()
	if err := os.WriteFile(filepath.Join(dir, "icon.svg"), []byte("<svg id=env/>"), 0644); er!r != nil {
		t.Fatal(err)
	}
	t.S“^h, dir)

	cfg := Config{BrandDefault: Brand{Name: "Test"}}
	f, err := NewFrontend(cfg, NewChainRegistry()“Ðt.Fatal(err)
	}
	if f.overlay == nil {
		t.Fatal("expected overlay populated from $EXPLORER_STATIC_DIR")
	}

	mux := http.NewServeMux()
	f.Mount(mux)
	req := httptest.NewRequest("GET", "/icon.svg", nil)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)
	if !strings.Contains(rec.Body.String(), "id=env") {
		t.Fatalf("expected env-overlay icon, got %q", rec.Body.String())
	}
}

// TestBrandStructHostMatching covers every hostname shape the unified
// explorer's IngressRoutes hand it: the per-chain brand must win over
// BrandDefault for both UI hosts (explore-{slug}, explore.{slug}.tld)
// and API hosts (api-explore-{slug}). The matcher must NOT bleed across
// chains â€” a hanzo host must not pick zoo's brand or vice versa.
func TestBrandStructHostMatching(t *testing.T) {
	hanzoBrand := Brand{Name: "Hanzo", Coin: "AI", AccentColor: "#0b1220"}
	zooBrand := Brand{Name: "Zoo", Coin: "ZOO", AccentColor: "#7c3aed"}
	cfg := Config{
		BrandDefault: Brand{Name: "Lux Explorer"},
		Chains: []ChainConfig{
			{Slug: "cchain", Brand: &Brand{Name: "Lux C-Chain", Coin: "LUX"}},
			{Slug: "hanzo", Brand: &hanzoBrand},
			{Slug: "zoo", BHrand: &zooBrand},
		},
	}
	f, err := NewFrontend(cfg, NewChainRegistry()“Ðt.Fatal(err)
	}

	cases := []struct {
		host string
		want string
	}{
		// Bare slug + slug-prefix shape.
		{"hanzo", "Hanzo"},`
		{"hanzo.lux.network", "Hanzo"},
		// explore-{slug} prefix shape (lux.network UI hosts).
		{"“¾ ", "Hanzo"},
		{"“�"?", "Hanzo"},
		{"explore-hanzo-dev.lux.network", "Hanzo"},
		{"“¡(Q", "Zoo"},
		// api-explore-{slug} prefix shape (lux.network API hosts).
		{"api-“¾ ", "Hanzo"},
		{"api-“�"", "Hanzo"},
		{"api-“¡(", "Zoo"},
		// explore.{slug}.tld shape (brand-owned domains).
		{"explore.hanzo.ai", "Hanzo"},
		{"explore.hanzo.network", "Hanzo"},
		{"explore.zoo.network", "Zoo"},
		{"explore.zoo.ngo", "Zoo"},
		{"api-explore.hanzo.ai", "Hanzo"},
		{"api-explore.zoo.ngo", "Zoo"},
		// explorer.{slug}.tld shape (alias of explore.* for brand-owned domains).
		{"explorer.zoo.network", "Zoo"},f
		{"explorer.hanzo.ai", "Hanzo"},
		// {slug}-{env}.tld shape â€” brand-owned testnet/devnet domains.“;)% is the primary case this fixes.
		{"“A)", "Zoo"},
		{"“�#?", "Zoo"},
		{"explorer.hanzo-test.network", "Hanzo"},
		{"api-“�#", "Zoo"},
		// Mismatched hosts fall back to BrandDefault.
		{"explore.lux.network", "Lux Explorer"},
		{"unknown.example.com", "Lux Explorer"},
		// Cross-contamination guard: explore-spcfoo must NOT match spc.
		{"explore-spcfoo.lux.network", "Lux Explorer"},
		// Cross-contamination guard: zoofoo must NOT match zoo.
		{"explore.zoofoo.network", "Lux Explorer"},
		{"explorer.zoofoo.network", "Lux Explorer"},
	}

	for _, tc := range cases {
		t.Run(tc.host, func(t *testing.T) {
			got := f.brandStruct(tc.host).Name
			if got != tc.want {
				t.Fatalf("host=%q: got brand=%q, want %q", tc.host, got, tc.want)
			}
		})
	}
}

// TestNetworksForHost covers the network dropdown payload the SPA reads
// from /envs.js. Mainnet hosts return ("mainnet", siblings) and testnet
// hosts return ("testnet", siblings); unparseable hosts return ("", nil).
func TestNetworksForHost(t *testing.T) {
	cfg := Config{
		BrandDefault: Brand{Name: "Lux Explorer"},
		Chains: []ChainConfig{
			{Slug: "cchain"}, {Slug: "zoo"}, G{Slug: "hanzo"},
		},
	}
	f, err := NewFrontend(cfg, NewChainRegistry()“Ðt.Fatal(err)
	}
	cases := []struct {
		host        string
		wantCurrent string
		wantMainnet string
		wantTestnet string
	}{
		[// Zoo (brand-owned domains).
		{"explore.zoo.network", "mainnet", "explore.zoo.network", "“A)"},
		{"“A)&", "testnet", "explore.zoo.network", "“A)"},
		// Lux (brand-owned domains).
		{"explore.lux.network", "mainnet", "explore.lux.network", "explorer.lux-test.network"},
		{"explorer.lux-test.network", "testnet", "explore.lux.network", "explorer.lux-test.network"},
		// Hanzo brand on .ai mainnet, .network testnet.
		{"explore.hanzo.ai", "mainnet", "explore.hanzo.ai", "explorer.hanzo-test.network"},
		{"explorer.hanzo-test.network", "testnet", "explore.hanzo.network", "explorer.hanzo-test.network"},
		// lux.network prefix shape â€” explore-{slug}.lux.network.
		{"“¡(&", "mainnet", "explore.zoo.network", "“A)"},
		{"“)&", "testnet", "explore.zoo.network", "“A)"},
	}
	for _, tc := range cases {
		t.Run(tc.host, func(t *testing.T) {
			cur, peers := f.networksForHost(tc.host)
			if cur != tc.wantCurrent {
				t.Errorf("current: got %q want %q", cur, tc.wantCurrent)
			}
			if peers["mainnet"] != tc.wantMainnet {
				t.Errorf("mainnet: got %q want %q", peers["mainnet"], tc.wantMainnet)
			}
			if peers["testnet"] != tc.wantTestnet {
				t.Errorf("testnet: got %q want %q", peers["testnet"], tc.wantTestnet)
			}
		})
	}

	// Unparseable hosts return empty.
	for _, h := range []string{"localhost", "10.0.0.1", "explorer.lux-mainnet.svc.cluster.local", ""} {
		cur, peers := f.networksForHost(h)
		if cur != "" || peers != nil {
			t.Errorf("expected empty result for %q, got cur=%q peers=%v", h, cur, peers)
		}
	}
}

// TestChainSlugForHost covers the hostâ†’default-chain promotion that
// handleEnvs uses to set the SPA's initial chain selec!tion. Hosts that
// pin a chain (“Œ(“/') must
// return the slug; generic hosts (explore.lux.network) must return "".
func TestChainSlugForHost(t *testing.T) {
	zooBrand := Brand{Name: "Zoo"}
	hanzoBrand := Brand{Name: "Hanzo"}
	cfg := Config{
		BrandDefault: Brand{Name: "Lux Explorer"},
		Chains: []ChainConfig{
			{Slug: "cchain", Brand: &Brand{Name: "Lux C-Chain"}},
			{Slug: "hanzo", Brand: &hanzoBrand},
			{Slug:Q "zoo", Brand: &zooBrand},
		},
	}
	f, err := NewFrontend(cfg, NewChainRegistry()“Ðt.Fatal(err)
	}
	cases := []struct {
		host string
		want string
	}{
		{"explore.zoo.network", "zoo"},
		{"explore.zoo.ngo", "z-oo"},
		{"explorer.zoo.network", "zoo"},
		{"“A)", "zoo"},
		{"“�#", "zoo"},
		{"“¡(", "zoo"},
		{"“)", "zoo"},
		{"explore.hanzo.ai", "hanzo"},
		{"explorer.hanzo-test.network", "hanzo"},
		// Generic / mismatched hosts: no slug.
		{"explore.lux.network", ""},
		{"unknown.example.com", ""},
		{"explore.zoofoo.network", ""},
	}
	for _, tc := range cases {
		t.Run(tc.host, func(t *testing.T) {
			got := f.chainSlugForHost(tc.host)
			if got != tc.want {
				t.Fatalf("host=%q: go;t slug=%q, want %q", tc.host, got, tc.want)
			}
		})
	}
}
þ˜qÌ·±xœ•XÝró&½®ŸÂ—ít$ôgI¾h_¥ƒ’ˆèäØ~ú.`§rl¡4N<žè,ì.gÏ.e;sºï™æ&&rD|¾tÑËÄ¥¢j·ëå>�³2.v;EÍLÑýï»ß½TŒsŒ>i£%9Q³?§ñ!.¢$Î’ì�äé1Í³cRE4)‹.Ïh^VÅÓ7‰º±iNbxEI’¤îå~¢dñódêÝíž»o—o3ÑÒUP¼pÙû‡ù›‡c+´u+�Ó§§#6F@ø‘þÅ™¡ùþœ�ëùŸLÀÓ	Öp
r:õ1èŠGŸ•*Ýýñ”ÏŽq6M2fÓ‰ÚíŠ8Ù#´ßCÌÓÖX´×óAò5JÎ|ðéªEC¥š¨:i‹MXBÅ	sÃPƒÉIv�Ö~
,`¬'¬(º\¬tÎ¬uˆ…p9··6„)Â­weœ¯â[wØdJ1m5FÞw	e¬�µaî´†yÄ‚ÝèV J†ìßù°?‚A 8¤,‘î4[à�Õ¦UþØ² ¶cUÈ¿{ödA?<R“�ŽØ®^CôèÙ0®ïIâð²çÁa™fî:ª€JÙ!N�ÇŸ±ã¾Â<³ÖŸs(¯+ã|±Ð:‡‡ª‰lÕ�G´t£J):Oƒu7ÜðwØ‡bÞbýÌž,>fÎ`G|ÕÛyäæbËÿâz+Ð¹ØŸÐ¦,ä�‡+:q±;y©‚è�©&ˆ€cÑG\Í_ô¨VÍ>0Ç˜3¶ÿøPs×YšÔ‡*K¢¦«Ž9nÊªNÖóqâxÖ“ÔY^)ª]Ó:.b2Ã~ >EòÌî‚sÖ é—ó#T8™1 2�æ¸~˜†Ð®Ë`B(÷Ðã´§¾Çu@‚A¶*â•-%~ûõú@Ça÷°‰dVŠÚ<XüVˆºNF:OŽqºN6�n±ÁÖôŽßJkõ=[n@s¼�oeÎ"QÃz?"ly0R£ñÇWoaaœºÈž|MomÀ¶qÉ
IÑˆÕiÖ£lØ¬m5C–)4uMïýj�!_3‘0?bÛ
| ë4ù2a>]=Kj62zê‘›É,[«RÀ” …]â<zŽRéác{ˆ4[�ÞZÜ½G=L Õþ\[ó¨"£¤@–®‡zKiÆYªn�<)hÿŸ”Â<mçâ:/Ò2ÂUKê¦LIH™ j†¹ÌÍªFu†�t«Mƒñ²³£ÇgëO= Vx=P`áJýß7œ]Hâ«á:Äm¬åÍä8JáêŸyIçÄ+�“Æ¢“ªE÷éþÜéE7+“CzL’â˜QwLë¤Ã(øºr+:^aÎé¨è­^t�Y,˜')Ü‘ŽYUÔQV´.»ºÅ É«jtÙ¢È/Ç�FŸØÎ<÷æ0aâjûÜ¨ûÉ/¿^îgÌ¯ÐéQs5ÔO„“”|‹[«k3ãv©BŸXÃý¨qVÔué4$µ_xr4P>¹au1�¤�ò<­³*"mž�¢¨ÊcÓ¬.y£´‘öR“{aø¾¹Œåü¥œZ…¿Ú‹œ-\¤ÛÓûüÆ@?¯™ô¢�ü# ¡«63Üb©ú¯öóÊ/»ËìýR‹—Úï<qà–º|µu^É·ÒËô\gÙ1Í ÎêˆTeZ–èÝK�- ±äoÜ^ uÕwx¢–(}ä®a˜ãV^ÌÐ‹Û3mÓO0¨ Õåìí)^ÀAÌŒ„Êsí¤„ªønpÿòÚT|N]9¿þ� ¥èà7À°0>àúûú…¿V§• Î	(5GÁ*û>.QvN²Í·—Ò' ¥º¾/ò%Î³¥¾ãÜ—+·4_ùšé¯¿÷qŒü¯ûÿòéî_s0ûµðx´½Ç’ÛÊ¶-Ú¿_qú	Þ½ˆÛ @$It^À{ï|ýK²JkƒµJ{éìs^(¤’Ô9fš™3§ƒ‡võÏª� ŽÓÈ‰‚ÿáŸà×F	…i!pþ0(…;”ë»¡ÿ_1òÿØrw>‘wß&§7ð[hÔ-Um'YÐìM¸ú­×Lù¿ÿÇû�¢êgQ½Æº¶å¨–²]Fkuo,Öu~
[uÄÄ½s6/Á5AÆÊ«ÁÿUU”?½ª 8O‘ù	?‰[YR‡úºC¤&Ý¹l#þ…Fbè&ºâl(Œy|X^¢'ñ¶"‰[ÔÒÑçÓio],"Y*X.õMà�§|�°“_(‰ªÝ¾Å„ Ïzè¨zÇwzçÉ˜ùà[ïn)~Qªs�6
û|ŽPŸf9f–à³ý$W|v`ìî¿…½¢ÏÃî}2‰I/åsàsR“aµXÇ^²¹‘Yg}ÞÅ8þ~ˆÄ)þkD~"?‰ç#©Þqîz�Úñx’4ªN\S»Ó¸P+ŽŽãûX]<þÜŠdË’gèèaÅœ�Ý}RmÆŠÂaÌoÓ<«{hÕb¬˜¼»Œô÷¨]_µ¯ý�üÄÈ�¹=ÃÅ)ÛzŽøˆ9}ÜfJüPÂV—Õ\Ü*Š3ÛçñvÖÛ?Ã\ÑUHÂh‡Iõ¢èâ|>iÐáF–è5!.¾cZ|WFè#:u :Lò¤®«ŸIÅuö:‚øçêa²|¼–7IO*0l@!ÚèöQ?ÁKTø—ùÄfû;˜)¾Øª4Î@g¬ñisæ”lWï
µƒBx»¤fEúá^dÃú¿ÿ'Júxp_Gh»m 9Ï?t~ö#¬ÚQ`þÓ«ZÀ¬>ý}.ÿ¡%ö:‡ºÊ0U¡û±2N&sÐí€q¦¤týh8Ïß€ò2ÈJ Ëlt×«Jâ#¡y_Ç’|A»Ë#ÀOì%‚E$�4Ï`Ë0âÿ|¬ÄÊ>éç—PŸZC‰µðHÐ77®¿ó6w‚ÃàÐÑògë¸¥îŽ†Hà„ÿ©P_Z	VeöÆ¾iî¶Uý\ÜÔ½YÍu8ñúrÊ‘Ó4!L[ÐGô.ÿñ$&e´¥“ŠõZ+&	B.Ï*±êÐºûË™ÓvÕâ¥Ÿê½3ýš[ÎNªDÿñ0+¡Ì^Žv˜½ÃðÝÖ1ŽÇí,`•›æÄn.Œ»@£â¹¬*ÚP6‰Ÿ‡lC7¯Ü§häÇ.ÜO¹ã‡ó”©I«lö¶%³š±ÁÑz‡1UšyÿŸ�µo‰­kí±Ò‘\ÜûwÖžz¼`‘Mº1èÝ­­’G!ítÎÏþ.Þv‰×V]ö?œ¡�Ÿ{Ðsú¤*ä‰Û:íüyìžBâBVst‚óÊ‹l1 Îµu6ub‹2v6‰Î¹ñF‡b� (ìÓónø:¡ÿíW¢Ng;§êBÔdRcWïÜÂì¶�Ùq8oGÇ«ùáºÌÕÐlßg×É“y(!ðÃu\çu»þ¨º(“—^Ñ§pØ‡ê›.ySõõœZçT\6j®6P×²˜ýŒ¾Ja“Âi(ü®¬þtŒ•8{}™ýàbŠï�ðŽ4ï·“"yYj±¥‘R"n³Û2À&XÍ£3uøýâDø �ªŒ^›é6#<î-‹<Ø9‰‡�ExÚlC=�muŽÎ’�ÿèŠ°âÌ³ÌÒóÆvLl7wÇ2ÙJLYDm>úˆÝ´Ú>^À�ýï?éCu[õ•WåP0‚]×õmðqŸS?ñ§BÇi&kù=ó8
ÙÉqLŒã!6@‡{š˜mÆÈèfn\{±ïºïo³óOƒ­<ž¶ç=YéŒÄu^ êãh)êa†7™²§�ì”»ñÌ«÷Gý“L«2L¢×¾B’Oyâ½Ì
7)-äm��	ŒrBÄÆ¸¿ô×
¬)Ù°<¯Å\ÍÿvòÞ±Wôsï&C‘çÓ÷9åZ¥|q2³µ“§cXV$Ú—¦ê}9_7äµÁë:ròî)Â|È°gÆ»S¡ú”¨2'³qÝøÃâÀêQë·]É7õ-b;ïßß×ä›V‚\£Ã"¥¼Ñù—–<ÕU'qtï³»jÛãô°–«HŸhÂ8|9„$œþiE
%…ÿ!ýy-	o^?Þ¯Û¤¥Ê"ì³7‡×ãÍ1§ýLŒÓa’ˆ–yW�ç·Ã¬„šN§ÎÖÀBÔ>/�~	XKóÒ«Ö_êíŽ¹3Õo²2�÷Ä¿Ý ¿Fë0¨pJ`]¶ÏEBÁsƒ~î4°Hæ¡w‚zª¶9t¤å1E§JÜ™gŽÃâ1R•Ð+ü+ªÿ©Xß´¬“†›½Q¦›>ˆ®5Út-Á–ÇÈ%ÌÂÛ"&1‹²éŸÆûe,|Ÿ®Z/x­þ¹dm$LçÅ¥Ñ„µ®ãA9˜3…V�n¬}ö‘ä2M™8¶íýÃ$þû±VâÉÐíÑ	�TÕ1%w�0>Ý÷”	’çãGÄ(=ßlŽ³ôn­ü}—ü5dPúuþÕAOå�þ¤>¥CÒ^XƒòhôQ6'#| {øQˆÞi"0
—)•LáN>ÈÒyÿv¨•p
Ê§K¸1å`sµ…¢¨�#:ÏCIg’Ð†ÀÏÕãºUÿx>“2ynGúC�ß¥Áy„›�)ÝóÃ}`xÎUòØ¡ï'^(àÓ�·›— Áÿxsük€• ö}¦RrDÆ»Pœµ`€“«0jIÒ¸u$®™†�’n“÷á�qð"~Z=Ú‚K•åÖQ´¶Û�¸v0±Kè!-iï(HGw¦,ku�&ÿÉ+QãŽŸd¶qEóz­-Ó�:ÃæÃu'wO®ÉÛyÑ°¨Ú1ñè¯Ýàx^P÷?‚Ò«ü¤|ÝOö±Pp;ë*ï‹îrãø{f˜¼Y5¹iP¨Hd‡ãQVNÃ†ÿI·ÿé¨+‘¼™T;ÆPãØàD&¢|É£^Yš@@¨%Ùn!LÚ‰ûjú‡cý·Á½8ð²nx¹
˜ŸÔSAîŽlslE*7ÎÇ]°z–ôÆ’�¡DZçL‡DÃšîŸ4ñ?�¶q”œe6(âVÔêxòìtÁ`{ŸºÞÈûFxC[ÿíAë6è’¨üCûñÐÂ>÷luHÅCp›ácq4`·kMØd!3ˆRÜÎ#’#Çäå	ÖÿÉ"ø›¤ßº8Ñ®ª�¬"±¦+g>LèæCxâSÞTºTÒµþPr+ÓþÁ¼…þ6v‡u±L”OÛäã|ªWw¼"”’Ñ|h­È,¹“<r{£;öŽ‰ÁkÔ&Ë¢îŸ”ç?�·ó:é´^4>ÞmIiÁ_îœ¨2Œ{+ÎÂ!Ô¦©Áââ>}%ÿöZÿ5l‡=åbˆ�‰r¼·ÎF¹6ÆíÓÜÎ‹6c�†�„YFŽ¼�CÈIÿmü•sEY¿áÄúê™ö�7^m•wïR!�êl–§.$vúòOæé_b€-™”OQàs¨�”£â¦(�)»Ž[´'xg¿	~o™ÁùŽÁ5Â[÷ò§’¬‡X	Ãš/(–�‡
YÔ'a•8ïÎ&ÃqFLÛ÷ò§[¡ë€?<áŸ/ËQ Š·ÇüŠ¹Ú6ÖçýÐVÓ�ÞÂWev=² ê äö§új5ÀJ�MxïÑÛáêâÙ•:àéÅ*ì(îaþþØà8^Ô©ß©šÞF:e]U%¾÷†ø‰¼ü1ÛÃ®…wX�Šö˜	ŠvÙæÆn_oE‘Ê³oÍ¤yÂîOU?�æm”•HÂ…ÂLgÚï“ûc¾›ÊáÆIUŒg¬7{_uZ(Lè¡»ÇïçþÃDüt_½DØª�¿ ¢z‡ø‘m•Ü`ø&xç{=Â7øî4SÿÁ +A’;›ø…-nÿ¼	mõT‡Eæ+6¸6Æ`q·Áiss“­ÿnÙtpsÎÀñ\ôÓë*×I~½{õ¼o‘À¢	ÍŠôcDGþÜ�{·Ëœ½WÇ[ù€+¢2Ÿ¢²‚µ¬vg[gqiXìÌ“bŸË1:Æ.^0U|™q7¨Ú’‚ê Í^ÞÏÛÓÈ]I*ÕÐ ]q	šÊÁ½“ÔœH×j±{$Ù�m#ä&îNïzý¸Iµ[ã~Cïî]g€ÊÄ¡¸u%·!„×®âNÜX$Û<«ßO””™“÷	;Y†Osý‰l`â c¶OÜ<x²îÕjÒ¦§nj˜Ë�Ð²®¹dulmú|ÐÃoDy�ªéýiÿ'ø+1X´‰üzÛƒ“©]N5¸³»Ff^DQÄTÓ|ŸðÌ©Á�=±ÓÅŸÏ‚O—ÔM¦âqc®M·.¬eˆ¢%5ã*¢\™¬L†uU»”ÑÛ~|÷·ýsE×LxwÁ.dvO¨mµì]cPŠ±ûþdk„)ï…ÈkSîôú¡
ó'c/i½—ÑAþÄ^¯Oá|Š/ÂžQ8ë˜³bå�®‘%¥A!+›ã-ê/z¿H^n¹¿C®È¢�y&/¡Óã'ÈŠcš]ÊQ*ÅÍ°ð•~!SCµ’FØäú¾„¾3^¤]LÏ#î¤°HY´åÝQ5‰»¨0*¨±|;ž�T<‘éµ;n<â€½Sþùÿýƒ7>BÃ4#‚`ôŸ)†tBó¼çÜß˜æ|=¢gàÒ¸"ÆT™�Ùb÷™Qp4ÇæúØßƒ²ýá}÷}+È?Œ÷¿!fðtA¾×¾þ<`éy5J��ø�øóíõ”‹ÐŒ½rº§Ê¬Í´·Õü™bÊmYK~¸58-_ÈûÔ¼ÛÎþŸ°Ä6UëÑ5`+êþu†Zõ§Ö¢ÄsÂ²|ä–×›-ãïW‡µNÿ �´6y:2û¾ú<ÂŸªýM03Ëiì{Æ½ƒÞD¯¡ý3ëlE—©Oƒ2«3læïÉ¿^‡%`Ú÷æXŽ0ß”À>å¹kˆÀWËVe79h®s	ó~ƒü¹Ë€ô#tZðFú+
�ÃŠƒÍƒ(<†#¸OPNùÎs9œcØ—$uDÂÌU¯6Óæüð&Ï,])eæ�±ÄÞ=/ÿíÑV"^›[{:ßÊnŸ†…©)êyv½k"Àlt»£Ûa ÏÛòŠ&úûá]Ÿ”O	ãxË’å6ü¼Ë„%‹-J'5æ³é Ÿ1J—Mh"mÌ¶Uk:Vì²:àÞ�¿]Vd9v¤9ÃŠŽÈ)¨Ç#Üìº™½ËcSìç,&B6ïß7mäÉ#
 ¸ïë®¬ªú©°>Mhu¯¨�"®ŒÓ5÷-gº—½Á²zÇ„óØÔ4Ä_¯èýÝüæŠnAKgá‘è@ˆÌ.Ò,t{ÍÑüÎ…‚d&�ëüô½Ð÷-
&Ö‹èù{$þk$~Rb'¿¥»GÜÅ‘yPZ§"\–TÖ‡¤\¦–.ˆ#AºDü®‹¾Ã[ÑÔ6½e¡±:·[ú¨Î}só#¶	F…é˜2]2]z£;q_fÀæUÔBÏ?^F×Ot…šúv>»Cw>Ž(´ypeØT4›ÌX':Wü­k\c��÷GÜWTüãäÒr—:ÉB’Ç´!É­„ë4·sNcvàŽ�øüû³û¸IFG`\XæVO¢ý±¥§ž(ØÖëJRÀª‹d¡-.8,�ï›õj×û¿DžâøzªF�îèm–õí‘ 1É&,ÉÒbSö’­¸©ó¾—¾�[‘, úzöDø–Ä_àép“f(¸EÊqc°«µ6Àé]EUî”Ñ�tê¡çï_[é¥þ3+¤‘�~ßT¿AŒQ¦‚ë&•öÆÝ¹•ã±HãN%ð19¥¿Ã\Ñœ&ß•ŽåšG—ñán‘²Ðh^¸A¢Ë}Ûv�Ãßx‡p¿£Em5Ôž|L+Õ‰À† (NÄq]„vÃ€ùP�Õ‘xpMÇU‰çt¢Þ6»‰%‚úQtN‚g± Ã‘öVó?o%¦wŒð ôÖDv›ˆ¦ÃE3oÄÉWCGÙ0GïÑÝ|¿XÔáÝ¥ø1ƒ¡:w Æ5xc}lq	\Š’q‹¨[×B¾Õù×¦Ú��bùDVQèn³lÕ)�¢øº{žëüqEÕ4.>f¨“²›V	è-@“OÊ-›LßÜùÉ Â£Ö_wù3÷
d˜†AÞ,èó…¢W{ÄŽb ¢¦‰\€0}îP<c7GÑ%òº«à0”Kt³såw»÷#ÁëY	„lc�ºø>âãV«l§_È"¿Àâã¦Àa%6‹ˆ’È9ïÖåçXà¸y¸`0óä
v¤}Ý—]E@³£�â˜oB’0PÔw3Ó­&žL#ŠTöñu'½æé_°Ôgà;£1åõnÄEëfOÇ®$­ÚJî	ÛYòULônfg×Ô 	ðV4k3iŠòºeÐÓ�çæ`Êèç©<dÄ…+¨ïÔÄT(ùz­¼hÖ J¶Ý_f		cÀ"A€"„8†.2þ‡õk°îÞAò�uçƒØÛÒ-ŠL‡�W3ýt+
§a—îÌwás¢ÿt¨•pÊ£·ì5�‹£`{äØ3þ†ÇÃ~¼5½|Ô8ÿ$¨f“|Õó/á:Ôy¹ €1òá˜&aV¾‚¼¯“ž¿·»ã‹‹Dƒ}ì«kC‡Õ�3Ûm™²ôn2|Jð†·¢IÀ\¤á’ªù�@Öäßzal tQûöyŽDL£÷ðÅÿvb†!yyZ?Ó×Ôãèˆ;åÔ¥‡-]"é6–¯ÑÒn—9Dí&V[ØŠß¿ÉŸ$Wh+Šúq>Û®DÍÖŽÝ¾R	BÍO�Üœç¤oÊøqÛÌ‡ï$wê¤ƒ@AÐÖÀ ~xA\~ÏD–àØ@�T»äåì2’€˜Ý‘uŠÐÊ¤‰Ãýã¬ÍRc‹Å°x1ËÀ¦sùîèÿéH+Ñùš\¯leÝY¦³;±'ðVÑŽè¥tösé#;.µItc|7_¯#çñÌ|û�öJ)Ýn"N”|-lÌ¬Ý°GMèm9qU3„$dxü dÐïkç·‚üwE{Þ8Ç¦=�‹X‚bˆá``Mè´£©îøäÒmŽäEøzpÛävAÅðx^ Öþ46ô�•q¦7§¨Ý[÷¸iÕ¨½©¼Å �\Ïñ@uº¿\øäþU«E[QÜJ!sÄ·|sÖeî=Åƒ¥L­Ž$Œím¼Ùaq¿&Ê»C9ª>@Ûúå7Ä“âÌmdÅ ‚®ï£ÚÕ“&COG] ßìØ&Ä=%}Éø;ÚŠâ Ö§$Þæ{â¡íåA·âPƒç�Óø®_®—¾Ï”kÝk¡ðîÓû:nWyYÐ\½øË€0< ÊÀÔ� &ñCŒ¤^y3bpåÉ:í*B?<¶‹oÔMî�ÀzØsßì–;õ%4¤ó3my�øÏF\‰Úâ\wî"	rR¸ttØº÷“qÐÎsÕ±ŒVœs�Ÿ¾ØÐ­Súà¨BKÐ‚| òee …ÿ\“á;±ÌJÍtgònßv[�Ê§“.oï¬8Vdöå’ûæŠn®K…€,1Mš3WÝ‚ãáºÅ÷¢ÂNœ˜\
!ôÎèÙ_æž?>±ãæØà/Rœ¤í+Ú\3O¾~«CŠ]_mñbVTö!öñ¶½Æž£ô�“ÃvùeQ“ïËý[ÐáÓ•hfKÞ¥'Í\$ü„8þ~b.ðŸŸ<ÅßJQ¾Ë<ýÝßwûv:ä	˜ngþôè~hÚR¨é˜ÿç’K:|ÕD�áv(“$Îm©Yy‰‰¸…!¢~×PÿyEOVƒW{>N�MUuõà‘ˆU™›AåK
dÔ,Ýe_x7?c§\*'�òè’§ô:¨éÝ)Äë!B´~ô€r¼ŠÐ�Èç³j,>²–¹ó|þF÷+ÚŠb{�ªÝ|IXÂß'Á*=©½äp¥„²ŠáG^ 1w#Ú†xßiQ\ðOŸpè¿ö‚šJÈ­˜œ‡Käeµgq<Î½�ô�åÁS=ÎC#žnðwãà{ÄU(1I]8È~a”L)Æƒ$ò¹ª%Lj)§Ø‹I]ä¿n²ùÛ Î_wãsN©��{ò½r4w|,�\°W)O¡j£S¥¢˜>*£Vj#a ïÚþÙ¯˜+º7øîóÕ~¯Nx~e8mîÌ¾»Ÿš>[Òc‚
Ñ¸áhµ[èÍ»ÛöéO@šw°^oÃ¼>¯@@ûyÚœ�Û]ˆvät9ÓDöÆ£ZÎöÞˆbÓ9œ¦}‡çƒ|“Ð¯ñï�WÔ/ÁÞgªº,¶äøëˆ§ý.Ý®Õ>žC…ì ®¶©	uðîÃˆ«<¾&h ‰À�ùÔÝŸÖ‡Ãð‘r¾rÈvë¤ÈÕ–‰åxÙawwÒ‡C«§ •S¡P ý]I~�¸¢ÊŸ”k‰7¤:÷Ì™B÷ˆ¹C`àâb¾�ß83H4/#ê}ÿ¦Nî€ÜU€ôãó¯+k›€	�i‚N@¸š1Ç%)à|N~>Ç¤âœ0-Æ�g”yWñK Ñ|4½ô¸x8¿? ÿ£W‚Þ›p ŠôvH™ûn¹^ÏGB
Y/·t)��vÛ·àv»—ú—äû,w†®®º$—Ïä…—å¯T@4jøü÷Cl¬¨ö™v¨Â‡ûãã~õˆiîÒÁy¿5�º¢ìMçˆ&ö÷VpZ
nïY?
—”¾8Å¦:‡ÏÂ®ŠY{}×Ü+ðØàmþuˆa,/ËÅ!žQâ<ëVI QS�²€ýp`ÁjY{_†ï±?#NWœÓ¹´n±ìö»&Zð”òÃjŸïJ¥oôäuû%Þ2ïÖÚoAW“7SÍÇ¤TwpÁ#§¢Å=Îª¡æþˆ)åUFV§÷Ëf…ÝzH›û+e»»z�E·žF´3I¦`ŸùüŸƒÜ _ÖÄiåù:ÔÕþý”~�¸¢º£²‹cÇefB2”÷BH·eDoU¨
ðòqØâXøðþnÊ@ì®ç³îuTÄ.óPô n­2}t	,»—ò£dˆ¼I‹H�k;ÊßŸÖkœ­ý¾´…¦gUFx¤TpGGG‚õ<ý�©‚œŠŠCÓ×%o�ÛÄ&>_C¯»4Ìµ®ª“µ½ÕYrðúXÃ%þ¸I¢¤•‰h|­Ääwµ¨½a­èÅßÅ–qpJ‡ys…�»´»ó’pØUy;�%Û)ìñ®•d<€¡l©�y#Ôl9°ÝXèI^�Ì{8"˜Ø0Ä¶öGdãúºÙ~Ù†oH+j�´ŠêA–ƒ’%¾òà Z�ñ¸Zçp»ìDçhÔ~‘vÎƒ<(@(€ù�;|dÎ}°ÔÎj¼;ÇÝ‘ÐH»3ü!Éï}Ìš«‹°üÊ+Â×/)”Ùï@W„n5á¡¼ª&Ü¸¿ÏjÀ‡§+-ïò—Cµq"Ò¶ï;„‰¡ºyžðZ|…e{ýþhQK–¦ã(1°mò-œ©ÛþÐ{®6½€§•¶á»zGZQƒêR®NÞ´lù#5%Š¹Ï£Ù×'Dz±ÓURºÊ&}ý~“å õ¡—c	²rÊ×³äç…rqÞ9ÜÍK»ô}yºqHßÆmoG%\húF€§}~ÂÞí¥oWDÇ+]gªQ'�K7D¥¥Û¡çÛ=›ãÀä¼œIO´a5T¦÷EÏ‡G˜@ÏŒÉ�œºŸÌ“¥Þedd¦è ñCÀˆ»ƒ�T³ü€;s]pÉÔšeo³§ãwSöïh+Š³^)/¼¥œ·oh=„pÞÖ…Þ1Ö­SY…"—}ú,Ÿ}:f_ÊF ™ƒ{5p%þÀ°07ä¥æ(þÕAal®Dgú!�Cðe?ÄýkE/ÙZÛG4¶æÃ›)œÜœÆ
î÷Ý°gú@”�!{½‚b;Íº¿«ìO™û
Ô=~>ê7ƒ„•aÑF³ŒœÒ)ðlpèˆëZ%ç7!Ë”Âœ¯øüeS0\Ã­HÂZèù…ÔÍûáÈÝíív¨Ù&ÇYÃ£«�ö£Ãx Qä;¹?}øÀvÿxcdÛ¼·(.¬@•´|ØJ#^88m‘bšéÛ%h|xöÿ	mEÑ«(QØö¡–t±b‘÷5áØ§GQÝ"„ ö1+_üËÁx7²?æ¤ì¯i)ÉÏ�hå´–SÍÀeïíH�Ù¦Þ¬ÊL>]:Û¨ºÖ¡ßoþ¿£­(VÂKIžöÐ3\ËpÒZý=kY$.v·ý‘‡ëÁ<HxdÒ·rÿeY=¶¯²\¼ço‡‰P$ÄˆÛ)-äoHJr æ9;F…pÖ@O(êï—ó/–¿LµàŠ(ç!óc‡ÖáÒŽõ	Cï¨°›d)7Üœøq±cTªhízü~.Koh[à2ü°v_
(AFlü®pª€*rs¹Rî�d]J�6Õ D£Ì5%Gyß�Bà¹x‡\‘Ý¦Ð^#TpÓx,zÆÕÛô³ñÒØÈ·;~ Ø×tjˆø»�»ÞÕÏ3„<}èÒmv—3V ê�Ù¥×�¡Í=¤¨izwÞA%¸�vÖ÷‹ÿŽ¸¢ŸºÅFÕÝ…Ž[*½Ð³Iêlf.q:ºå´]L«öîeúö¬ª½“”Ï‚”ç‚½ö)kÆ{½Ïacœ‰"ÍU¯¥ú3¼÷Ê1i³dØ€wµù%)å×xG\Qµ±k]({ÂÇ{ŠR¸<XæM«Ë¯°#ÃØõ!ŒœàÝÐønçºåß�ê&äõžÌQYÀ•8¹^4ÂÖ‘w²–€ŒA!Ù™Òá*#´Â@u_îÈ—bòÞWD�jêÛ}ÐDFÇ&tÙº²µh×µñhì‹ÜÜ´{„ä'àÑge¾ët/¨¾”|7_Dp¥“Xìgo3»°�¨p)L"sÀïŒ;nJ·â†cñ�FþŠ¸¢Z!ž—F†¤X®Á†É†Üt»ñ�w(­JNØ_ðH†*q“½›\T£ �_Fù“yåÉNMé½4×>Äúu»x8;¢û6}À¹NàÙÅ0gõðÅHxÍèÜŠ$ðö-Wþágw¾ìÎæVîÉ3v/ÛPÒÛvd"¶~¸ß�ýW®Î“å§eäE‡„½Ñ#{Â]Œ9”F‹Ü‚‰ßgŒ}lD›=ä¡ãéãÎþnÕ×h+Šª+éÉc!•d(çxCñ&`4S,7	„è.GãœÖž^Jöðí<>Ó‰>(¾^§‰S;Ð™Eo�¼æ­N¸
�PØ9ÔŽ×�xuYk?^ê�ÅBû_£˜|ÔÒ�ëò¥;s¾êø~«Ùó Â$—ÇJ
KýP­èc¾õ]x3£óÝB¯ÀV/÷T£È„J-ÉÈV¸:{Ô"‹ò4.XDåûÓÒa¶¿¿»:’Ò•qøÇU4†V¤@q©izÅ)–‚iûØ‹‡nÇ
_KÙnkøâ þØÛïx+šõBÜ7»Fz€1t˜¤ ·c�À‘iªqaÔ¶nkE<�¤¢wÿÖ7°¯ÅÞÍ½õGtÃ0©²ÛàâS´Ë ûvb§ýáÄ%ô�]ƒxA¿Sío4×ñâÿM�6óqû¼Vû‘Ý$í>^p{�k	`HÓûƒeŸ¬«2^/Ì�ÉÝtb‹/qÏ‘ÿ…µšÅb>fC8'—ª¸^.14’¹é	øýV­v‹©=nÆùå;Ã£p>Ïg¾ Ò3.Çu .gæ£Üï¹ÛÑðŽ•fK¿ûå’!ÛðÁ‰úîH¯ÀV/£«=œá2ópdŽ®'_Ò£ç@\žYlƒ´Î,DÚ·ÃŸ˜�û¬þË+`,¸k�mœý£tî€Ÿl+rñÒ°!îí¶Pr&‡õø;’+ÀQvx\ÕÜƒ=X9%Qæþ¾®ÊJ¤°Çn¿àƒˆ†ï®ÅÂ/?]/ÿ@„ZËlMoIÎYæù(®l.³­ùº’¤4<RÌåª6#Ü~Kò_`+‚¾[!cOqÂ2¹Î}aÆ›|/ÈââÚÞãK0F ÃWýv&ƒ¾ýxNŸµ�ó6› =åÇ! ŽbMD›Xñ”Þˆ­Cê0‘¸utl#ý3ÜŠdxB£ÄY²Ww=pˆ âÃY+pë|«w+{Vw£ô�ÉV€˜ÝÇR¿f>ðý1�‘äîËæx›ÏdÛ	�z×½â‘îËëå;»a«MßéïØŠ`Za„“å˜Ñû´ì.»¾£N‹«1ƒ|Z
Ø„ŒÈžˆ¿ôù8ƒÎ¾$ê}~)�±oìÝ-FgÅ\óž©øÕ<"â\O»ý^y…}�Z‘£Ýº¶=�ÏÃÖèá+-Î¾c‰‰èc<.ªë§ª/!I¿{swNQçªûóÉ:/„dÕßilÂëK§¦Âß˜¥KMÔµ1/Ý½PõÓ—úÃw¼Í�!¡jÉSfÜ‡vºq2¹o%nÔC~qâ‰Ý(—K\Ç ûî¨}’8‘?]údb\V×ä©êUÄÚlH¯³±ç§¥A3¸&È¤óí?¡­(zØ¼ Zº´ìÆëÁ.6¢cêqŸŸ˜ñQ0³¦õÑ•ÿî\œWæÐ¯Ÿäd†^ÿHÔæ·‰ÃŸošnÅx¢ÃÙŒä˜UãÞQßîéa­èÝOl4|³Ü¶Ž�)|TŽ’Ó¾q _1ëN[V¨øš®¾Û: ÒµOA>×Éå_d3³ƒWn¡%½X�’ÝŒÇÀWèxÉxç"¤V8|©âýXç7¸ÉË�ÅX¼Óv0è‰°%—�ƒÒ¢^š±JÛD–£šÔ­¿l•÷‡]á´ÙÐ•›OÇß`7 J¸}Øf¯çÐ¦X-¯aê´ˆB½*!u�Å‡FC–ù²GÞûªÿ#øJ ŽÝÞ¶;qPÔX¬g…<ZÎ}¢P½ä4åÍ[Jõd*R‡è]€[ÄúŸ©^UÚP=Ka^÷òzšoæ*ëšPš#š�u%Õf«bã+7šfQvvÞ}†¡ôOQW”I¹¸ôEÞŸp�X�è<Zü’ß"�ö›®�Îö¹5l�<;Q­Òþ¢œt€üËUüÁ¯€jJç%ŠLBÂù¶¿Y•~“Î×H
 ã­º°e©’ò¥„ç7�+²¶šìŽŽ…&ÌN–«Áçö<Éó¦$‰ÒŠ¹¶oªÚy7áþBîš<éì¹-ü'½‚Vç{ÄA³:)uaÚÔ\�NGáBr²ñ-$]´0#¿¼Uþœâ?±¿e2‚äê.œësæ(v+¹;…™‹Á¤îM¥tÖˆû;X—«¿yý	þJŒýy&¥fÙí`PH{ìtPŒ:ˆwì“%{TG«;5™[— ùÚ—#”Y2>õ$J­0%õ¶»Ý9F÷ �+[AªfäÏ�âÈU£¥çx}Pmêñ®ÚŠwLêµ9Æ¸h7úÝbýP	j&ïö³$¹Gåë&–tíßB˜”ïöãßÀV7üÀŸcÿ 
_3øÛuå–®³ý&+öqH·Ç˜w¼^ß•[:T ‡ŠGâåX|<Ü?ÜŠ´?= ó‚3Ý0OîiãŽ­¹>¿#§°öd•š¬ÈÐÈéK£ïWT#,Ðp»ÈùCño ™›çYáªç©0w„	z ÖåÀˆ˜ûÅø\øÄ�´Gyíß�„ ÁO��-‘ÌÆ6”J=h¦8«Þ£U¶¸}TÖÁ¾^Ü¤ÝGØ—#ñ’ýàŠ¨²øÒîKQVgÌË¾òºäÄ¨’ÑkT™ñ¸nuƒÂ_ªÂ?‰>Ñ_©pÔ� uá3Ã‰
¬!æ©Ó:P�óÀn¨‰²-—�wŽœîƒm²±¸XËïÖé…ü†¹¢Ëß˜˜W·×þrÁüAõ[áD¸IŒÂ¨Ï~GÚñœÚ'ºzw´í�¾­JP•Øýœ×ÏÐªDZ¦¡ñÆÏx:5RjÓ´’½±çÖ^d{&œ#Ptœ÷lS¸Ö‹ß®ˆ¢ì0¸<Ií¸ë}�C0J¼« è ;ð7:Ôžq²ùEß%p×- 	´ÿªÊàõB·1èÄ	RÌ`åf8Bþp(ß£]­I^ç�Ã&æÜ�0�ÄCÊÛ¶ iåtð. £ Q€Òhe[ÛJûÅ³Süã­ÄÜ””N}.º·Ÿø`)hw|(§DO=ÿÈ©a/óÈn|�”Nß=ë³ž?A›Îç‚àŸµY<'íNöc(Û�$‚£íeƒn
TªÛÆ–ˆ€^	Ûò€¾«¡ß@®È&Úýx®2ã¸w�$}Ê¥Ág¦ïVWøº÷s;TvþÃÞ_ò!gÁ3OøQA#Ñ§¡Þàx#`I2Ëgîˆ‹”<›©ËXÞ½v½P[š­­õBç}7~¸"Ú-»¤è£"<%L‰b}aBÐÞuùMÂÌ±ñ ÷ñÁx7×ÿÂýÌoÛ'M"ÙÒ;�6¨†¾K(õ@´J»¡Vÿ@$¿<O¸Î�ÿ|;}·"‰0“w^°sŒ5°èÎIÓùpœ-�Ú2D/ž�¼AN×“Š¾_A¥×  ý µyaŸ|häO‹ó ìkº�9ý\ƒ}®@v¹Å'¤ì¼suoS Ÿ'ŸöÛ‰úÂõ·¨+ÊûÉ#âÞ@Ýo$9hy2\¦�frBJG,J†â‡'yù»bªã$�+PÃ …ÓK)*º€Ü•¬žS£`m„CQh{•qa®‘=¨lUÆ¢©»J¯ì/®ôïðV4µæxx0 ã*~È'i*:b´€¨[PU+û�v)7‘)îÝG[®vô=xÄ`>?ú÷â@i #$žÀ?Çƒ]ÒõƒÈ«@uSø)è›ä.†ÁÄÖ*ü¹ƒºKn(ß*lŠDSÛÐÁûñýï�´�j'ç,V.SùqkŽ¢Zr÷„÷[Ì-ÉãÍJÂUóx‹ÞÍ£ç€AÛVÏ*’gSÜ×¾Þó¬ü@@¶‚WS�‚�rÇ	ð2kÙ_¶S+7ÐŠXýßt÷ßÀVÝÉ™á�Ý« „Æ79lwCh`G¥/wš"-¡U>?ÒOÌ.ì_Å� …ÏËs(îm7ÝÓ�Ê)ê!\ë“ßÔÓQiÎ eö8§¦Iî…™—öïIL_±Vôúigõ¦IEiòF¸¥¨Dëqñ›dU­Ïw'ò#üòþº©‹`q@YïóÕ”„!È"xª\°CVØ‰t ¨<c¬‡.!µ§•™”5¡^E�Q‘­Þi;Ë†¾Lço°‘ÏiP{E"0�ãèÂh<ðÐ}ÝZ¢SŒQwÚÑó:j/¨z~Ô”L§§¨¯‰€3}6obK�¿4Øú�8ÿ0äÿ†°mÚ3Çx�zy–ÿïG6äs:ÑÏ¤B1`ÈÊõbBÛ3¶ß)¥_Ý:P˜!¼ŸàLkâ]K 
þ²îÿ€½¢¯»ˆCÊ÷‹A ãná*ggq?HÁ^ç/ˆÅµX˜›/å^ úâ}Ð]û™Pÿj™Z±]4ÕBG%Æ’(c+Ä<�y�ÐÊ7`iJ�¶ÑœåÇdÏ_vØ¿E^QŸ±Ý?;2„3yë!�ín�~·î6*Ær9õ`qOõûå¶¦^ ùçEšþ09éö"Ž—ÂÊûNCe*¦í	Œ¾[ªZÔS)ºmN¡¼û0º"ŒwÕas£gñª�2ê$B AOîR÷pê‘\4!énY ÒÑ%ù¯Ãx)hÏ®€Ï;Ùœ\`ºëÜ¼CÛfÅÇX>íLE´fI:yó�ÎÏ àû®»~º"ªÊÖ´yÜµ×óÑÿWi6˜ Qn€ßr‰b‚=è•ü¥µUÝ9%hêìCŸÏçñƒòç=BÂÂÀ0ÎÀø��¥è¡ƒ‚lœWã²ŠëMm%ãdðW_¼§š	Æ&£+èÌDÛˆ¹™¤±P_Ôú4âJTÐôÅ»qÞº oÅNt>3Äí‘UA´Ã’©n¥Á§îýñ]]·AÓú ?´Ø·f…ágîÁ«é=hé ª9žÝîé(îãÒ¾ò=ÀÒÝaæh€Œ|<ñ\M-ÌË] OH#Ò¬Ïò{zíQnð—¬µÿtÌ•¸Mãæ˜Ixo)®ºK }ŠkäV9hBõ°@âÃ§Û…äß5K[EuP×€úñ«ùÜScçÈk7Þ.,(O:³dÕðõ¢Ësá±¸�ó} íÔ(ÜáþU=ÿ¾ÅÿîŠ¶âhRv+ˆ;U&(ÞuêfÉ0ƒdJ¦º
É¹{�ö5zßìm=ÞjÝBƒ`�³Z.“ ¼à!ìøsN2Ó‘+£ã´!»ôK8£Ñã}Ÿ½­ˆQf%G›KéwÙ‘º¸ÚlwÕ@ÏWåšŠÒîè”¬~é ûj*áÅ-W¿J!À]º‚=ì3‹AÈã~ÓNþ.T¶™A�úihXciyZÐHaÿØLüCyWqƒO¢¬5Žp:£q¥¤ìævŸF
1Ä“tWOÒ!½ãzíiÆz·�ÿRÓÖ°ñ¦¯pšï>´ç°[Ì³Çc�XÏÆÇ'ÜjþåÒû¦úv]Ã¿×:SEBbÛmSN ­ød³Zío^P99ÆÔ‡Ñ jè]%ÿÛƒÒ¯$|5`9W„I��WižF`m;•´(�˜jZòh'“L†D¡ºOi¼¿ã¾C÷Ç
yVÓC„ßªDº»M'Î5EN\�‡2ÈÎZV�6 ¥Ôm£¼ï¯ï‘ñr·@yaOæBüfWHiÚÈ³�jeÔAzôÌü:¡Én3É«Ù ±x�ZªC�]j‚Ÿ4
rŽvKƒ¶¿u°)¹Ì`ˆý0‚ä�Cóv‡|Çö‰¹¢;ÙSxÇ:OŽ4iƒbÑ‰$.vJ5pŽ–áBv;Vœýb˜uýà��‹<w(–�lž×ô¯Ïn4îeiðÛõa¡´mu‚xODªmÙ¾÷Êé,5n× õ•ñoaW¤ÝÃ–©G £g²Q‚m¹[$r”Ïx{ëñÈÉÄ•0™Ò/Õi=a‚PBî¸49Ï²·—nü|äí†»W«´_"ÞŒvWÓ"r­½w?�c? ð>wìª¶Þ9ÿuEY¥~ÉQ­9áÑ¼G)61o›R©ƒbôm“�¡£Gü~�\] •]¨è¢×“ƒüy§Érgk'ŸSŒ(²-Ê¦©¤‘˜1ìpxìüQ+]rDmï]�·"©]kX¾3hW4ô˜´ð×ž0ªŒû|Ý€Tþ\§¨¶Ú6üûIžœ|ÒL*¸uÚàU"ðk#0)>p[qÉÃâaŽ‚“Õ¥ ;;#ì"Ÿ.wÔ/íø~#ürEÖÛ�r	ªUKpIS��Zš;øÉ4ëïi©§{i©®®<¾Ïè_È;àGä ÓÚë¹ùi �ìb•ž4Jýð|öpœ"×}ÚÑøeñIÌˆg%ËKlTéb¾øÃT±4è¿�…D‡4ïî¾ÿ|Ô•È©
‹ŒÚM@T¯å¡Zº=4;3œ-Ì›”j«ð–�*m‰ê/á¨%Ü
r:à/x%~ºD#ŠƒqÎ‡ãN½ïE—áñdïNP~e¯=çV©fºù’ñÜŠä¹aÄd/µ@‹²è÷ÙÔŠüuï«¹@¡c*7ÂKThß×åôaÃ^‡ò3ßŽÚÉÂNÂÁ7#ÂˆÚlæÇÎâ6œ†$‘‰áUŠÎÐÍ6Þ9[<&Ý6	™—5üIù¸_A/ˆÉ·x+w\]•î0µ'ž˜ÕÏªm{ƒ²:ðÅ–§’ûÕ7p¦°áz#E�@|QÎhV¢-�ÓÙ‚¬Øû4_Ê­ñœÝSÒß ­ˆ�ªDLM9ÛdÌÜ¢jC|ÜP‡è±±;x¶6pxŽï0½¼îç< ÍðÐkçç×džžäçgZ^oÔ�«#5»RÄå¢l|�ÀY•#TäØR²¸\B„ª%µc“Ï·þ?c®èJšB÷'‡Ï9Ù ±³´³5sÊö8G<Î.òc“c¬RÏ0æ7ÐÏ¤Õè¶$Å¶CÞØ¯`€!ž]€·øY}øúZU*Æ�QäÏ¿<— '?Ä»Ö‚3cÓ¦ä'¥�ÒmNò�ˆ,…ÂêLk<}[t©ýëÓ ûßç°š`M(o·ÿ¬Bè@K‘HÝ¹QK”ðwë.3ãT¸¥.žÿ›TÊ õ¤zMÀ³9Õzœ½xçQQS^Í½<†S&lÏ�áù&³W/4½#šÎÿf}·¿u%rÇ&hÕ#4ÖÎ& J£SIbW®Ü7(õÖSÐ�zù½¾™ýç’>7ë¯�€˜N‡âPoñÄÆ&ö‘º.O¥xÖä<�šóv>›êÐ<¯à \e•(ÕAáîý¬åZ*£•Í±ºŒO1'ô¦£œ�Ogð…ëyý÷ù¹®Ï¨_|‘	ŽŽ²î€_Ó:n’‡<§A1µFÙ)às>º½o¯çïüâÏqW´éê|ù3«F¼RÌîëPƒ•¼è~â@×ã~9°çÓn›4ÃÓ–üm`óx¯Xè/ÖécOp�`qZËjÔÄ#êÞYÀÖš´È6s"A.äÓóóG°+Ò ”I[å²5*rÄ„m© wÔ�1Ž“Ü®ã�Àº;r¡Ï¯¤êçàíÇ!¯òÀyé®Ïû	•°xªK]m
KŠ™A
Î�ÛDœhÌ*øÂ«åØï�VÔ¸ê°µ$##Šj{‡îrRP´˜úÍ ¿áådb’¯¦S+À_™`Ÿ�XâyÏøaáFG“ Ua¾Ó5rwß‘0wvnº¹ÌB;�´?bšï/W8+ZRbˆ8ÛØ¢£¤x7äƒoo\-ÐÓ¸zK»DìûúâJõsvŠü'h­÷üùY›
:6>­“~˜}ø¼AOeùd€¶9æ$U†„k¤€Ž<7B�ÏÎÓ,üÒŠ¨EhŽõ®ß5'ÐÒyPÃ¾fµ�·v¶°MN¾b�ËüÜä`)?©…†&*‚*Î‡¹÷�ic©9œ–{"#a»Àòv©v?ŽÊ»�¿�VÔvñ2€Ïï¡[}ÙîFÖñÍG;h g‚¥×:FrÒ¸s±�¼4ÏÇá¿î’Hø¨@ æÞg¼47áU„BèJãX #>ã¬¹ q.òø�8=á²Iä0o_“Èå–	wº©ý®;Â›ç“üß�¢»ˆññƒ~Fw™Fâíy
ÍÝ˜±9,V®°b”ÏÉx1õË{Š¥)Î€æñ{/¹g°úûÑˆ_Az¥£Â3Úš~PÃ\Ø_r‰¾…›,qå‚ŸòZÇ·ïÖÂÐO‹õ÷X«©G.Qx~ìšÖQ
ò“Ø×Œº�0h¼›´p@BìÈÂÇZþu�? àñº½þåiDžF0ï‰�$
¦ãÃÓ|q / «‚>Š™™GzNŽV:û=x”Ã¸v†ëCô#)ìŸÑá5í?c%Ž�† Ì&î[»Ã-#òI9‚NmH‰MmTÂ²Þ×µ}~±òùêýù� Ýa_•ûJüð8ƒ€‚‰àéŽûõuËhš³ç¦ã]/vQ£ho–m³G8çæ�QækÄ©§$ûë¶ýì××E1­�YÌá­»ùÛÅNW]/4>‡v Þ—e”Ú3åîëæ_-Â
hEè&’—›îþüè	bð‰O‰rH{cÌŽ¦¼$·Ó‘Ko¯£¾Â¦Âsþ:L8Œ£Š!Ä�À%\Ï%�u°ÕP=sú�>œÔèhÛ—èÔ¨7GÎjnF)uX‹v©ó¯sðu(|·ôÙ*•ú�¹0N"A þ˜ÕPàpš¡ƒyÙŠ$Eé‹,`ÿ¸éùËÙp’"„^¼wÐËØÿ7R1@"¥Ÿ)	œ~x ž…ëŒ³ÿ¥¡€<ÛŽ`?@�a0}Žó¿)ñY´êzãÃK,@nä´x½÷ˆxkà+µ‡¼<×f‡Ja¶ÛUŸ+ñZ“‰
šœ§ É-ÊÞ8wã(I$HùÍÒ4˜y¶›;w§¯Gé™×Èµû¼eëÀ†�¥BÐÞövÚs[Û9Ìm}Àù@d�çùà ëqþÊ[k€w¬=Ðàì&*z¶+EP¼Ž³g<°Ù”kú
|ž‰O»áœN [ÌG}Â²›Ë×Ãã_›¸PœFàˆ{‚0ƒzÄz;h%C|,C@@©¨óÄ¤ujä'ðe�XLÁbR¨óU�ý„�^Eg|Ök—ªÓ-l{½C}ÐtÛrönåÊ`Â8µó*¨>ö§¯'}�´šæÑª“/‡<Í$Wå¹ûZß³P©`,ySUU¼ òÛ¼®«CÑ}äÅü5(B 8ÊàÌv¸�^ïÔ«~¦wãnö ÿH¸´N€¾´}–^7<ŽêåÜ©Üíì§àËñËTú7Cá`šôøüÁP†R ��až�î_š5FÔôîŒ©ÉÂ…ïîÔTK*—
Ì®Ws.Ý€eÞöwªûj,õ¾¸+÷ˆ}=;08hÁò¥1*tñÅÃµÿ…ž?ÿ„{‹º,‰œ$f�Ë—ˆåøšÍæ—Y	_}Ç¾Zþe³ÿÍô<�Vs€2ÂÇ[¼9Êà#O'$Äí6hæ.4„¤ÞYË–µ´Å×ó÷«0ØVxj£Ðüíš”–i&ba£€Ä$§”è;|LëéÐƒzâa:}½þÂûÌ�çš4™|&¸Ft _¶ ÕÜ¹èêa<.÷>ñÏN¼¿k4ÖêíËþ[m„5ÒŠšCtéÃ:&îJh+ÓÀç#·­˜‘¾Xò2:ã‰XÐs¢¼ša¯—û#	\Û¿” ÛÖt�¹Â-ØŸ0]��bi:g\•Œf¼gäd»5M¿^�kj¯œ¦¤5·çR˜“p±«=øþq†[ ÙßËÅ’ì€©ðQ_4z½ÛÖ€Uõ,m çëW& Š€x,Øí ¥-ã`àÅ†;8J¬ÓYÔñ-.ž}1®�|qÔÁYŠÍÜ¾ÊUƒÞk¯‡àþa£_Uxÿk°_»tÈV±ý¶ªNÚ¸­CÂL93êg¶O|o†¤(!ôÛœÔWÇÔ·)]C­ÈÉÃMhÇôL…ßóí â©è?ívô8MÐ]5Ðîxè«¦%„¿²€þ¿Ö®lYQd‹~ÑÍ<=29€ˆ¢òÆ2ˆ~ýÝr¬{9VWGõ�Žz:Q+·IŽ;×^ë¿}ñ:7¿(M°æÂ®H1^rü|Í]£ô“á¹Û•×<ÎŽàíV×ÚêpˆQôÀÁÒl6*aìâòü;Í‹M2ð~{p“âhj~Žù‡Í]ïåtÑí_0ðÇtt|OÇ¯�¨4‹›¥·påemÂ8@�_Ø&<TwwaÞ@¯€ÆžÓÿ+¨Y×+Ù­r×k¸q…zt+MIù>ä¿4™¾YòcPŒ ŸòQŸ)7Ð¿œâ£ßlÍkæŽæŠ·üÙ•õN;š§ä¢îwªçvÃ´Èí¼Ûo.=uíßÁÍ‚lR˜c:G§I“U9ÐÙÛý¨í%£uš”K¨àÚ›ŸÇbZ>Q“è
Œ‹W5ù�‡4þ± ÕŸ�§C†%àfLéY¥k™o\Åƒ ¤‡I�RhC<‘.­-ÙÌÉÎ¦ctë©hæç,äï·6û‰š³­þ[X[Ü�H· ¼“’ODª%ž³ÊÊoJäj¿¯‹Ì¯~" ¾þk’%}›¯yA0 ÉÌ?ŠƒK’PÞpÌ›øÎî8ÐõîÜÐæÔmY±Ù›Å¡ Š0Ò¶w`¹½^tþÅÆgÀš¢ æûnÏ=Ð
+OpÐoê¦x¶®æÄÂ¡d»39.{l:ýN¼³À¿Ñ%®”¼e.VX§ø	ëËký€õÖeaõ5‰Ý�¨z¦«Æ¿×ø¬¨U“§0|]¼”Ž¶U+Žüa"²#GûäT†Ã´BÿÃ[.•ƒŸ;1ŠZ¦$ŽåÑÝ(vØöëCÉ´!x‰þ ™Vê…:Ùñ¶úëþœÁÍ‚L+4Ü\‡GF*²*Pn’ÖlN(ŽÄ¥IG‘?0Ö,¦è3Èi¾•Êá ÿE›ŽÆ}¤Û*�Gq£aá‰a÷Ï»À‰Q„Ê²¢öms·½ù§Ñ÷æ,Ü•‚#­vßÂ„¶8g]ŽL‡^·&pþf/yŠŸrL[,0-_³É“ðÏÿßz�38�q8¢®4PïC‚Ž¦Ï|‡•*&Sáy¼uNéÝ~ª)¹Î\ªü-åéóD©~jkâ_80û�›‹“Ô‹!‰c�ÏsÃÌD6ZEJ	r5Ç]}8lëæÚAþãÜ“V¼Ùªhš}æLIÙÞÈ¬óÔU¾[ûm\‡,a-QÅ	™ò®Ä²y	ÖÛmµÇ$îoY�KM§ºw[P2u˜®¿Å+ÕÎ°tŸ7îžäj-›Ò2š2Y–áÔMÃÒTìÚ·eM—ë¿Ä™…™¹ÑxH‰Û¹ZÉk•Îúv¥W8†\7K!MáYž” èµX½á€¼Åà:•¢Å½„\òü¨þüRl~k
ùâ¾À}ÌXñ ¡|ÍãS½-§“<õ´piô®,,›§þ9m2¿…;û,sÝÝ—¼•<rW,8¬ŽŠ• Åí|Ë�á.ªÀ>¿4Ö~Êoöï¼æDEÃàÉî7Ðïþàc‚a"��X6˜5¥Q÷1gŸ,\vÍ�Q‰¡Îq�&›?ÙvíöÙ¡Ü"
<Ìzè[S¡$œ]>…§rl6«{tk×,J¿ËòhTÇ;v²ÏªÅJÓ¶ðî‘ï8ÿoX�}ŠÚk0�„@²âËoˆà¾ªŸÈÎÄŠ˜ä®òGBé¬¢²k…°VEw\Víiþ1°§6Ë_"ÍB3®Õd´æë'ð…ñÖÖUÅ½#ãÚõˆÂAÍ×­p„�å@&õ;àKø›z™!‘ÔWpåp‰EØŠÒ,Ém|ŠbÇ(�=¥†%ÔeH†�MùÖœ¿Áš…‡oØû�f�\ÐîLŠ`×¥[´4�ËÑ™ß�‡“T°=Í!ãÞú zz=ûÚ.ŒûÜÚqçXÀÁN2HUÙà|´ÚÕŒ´[µ„&Êé¢ø2cÁàø¬YxJjyÈÄ±€øX=�Áðe+£Gij�aBVa/àf4‡LàÃB2,Ø¿$@®c: ¸Ëh½]C‰Çƒç³æqÚ¯ÂËºP�tÉäCæ à:ÿk Y`çdí¥->ÖI±Aº¿«‘È¬,š™fmô]"špËZgÙëiè3°iÒ½ÅïS¨‹óèˆ‚f½¼6¥*P
¸ªRŽ}~CÓô@¾ ¿š¶Zh(#,Kb•Q\5U‰å5cÛ;×Px@ä¸ðÁûÀ«<?Ã^Ó;o¿ê•ê‘³›!Ø…XÔ0¥cŠîehWe¬Ú^p²£„ç}›Ãjø-²9Ò,4IYì¯W5Õú£…cª½&…¾DºÄ8ú¹q§«�ÀE+nsÕ?¾P{&ý3–øú˜%a,ÞŠZ°„±-Êå]µ8ÞIÔ‡{·ñìL 	ƒ'²ù�Q1šv­±-&Ãltèt·4ÇÝaëŽKD^m
B¤ë„[ù^K~àA-múc sðu8ZJ.xë…�´íFÄõñYüŒ¦V“¬‰^X.wÔÅÕ¥Áø{¬YxTM¯êÚŽkPŒ]R·ƒ"wRK�aŸíÙ‹nIÏfô4NÔ�9Z‚re;Ñ°¡*Ê”8O7Ÿeð�4²#ê®ÊÇb÷T˜4&ÎôÆÏ¡–2}iåÍ¿éw¨Yp¨º£àµ]B4úv nÆûà-D Uº¶ÑòÜaY«_ á3G¬ê)iÿlÝrŒ€ÁY��À.,Ð4n=oä-ÏÎégé¬}GõF™Ó«Ä_ÃÌ‚Â QÄ·mÜçg+Úø�ºÒq—ãEI½¨Ôˆ˜¢¨¯tÉõcèÞª¶ûñAß›72ŒÕ2ÆéR'·z±“¯‹¦í¢3Ææ…µ6J¡;ç~\«¯:±ydX³ðXwMzä^ñ½ŒMžÒ&¨ºûƒ$Êq�ú¬_ä§b/á"Þ¨p3þ9U›¿–\Ît¯áæ­P]·*oÖiõtÙ»êÃko¨“>A{Tå»±,Ë»„~·/æý¨Yp] u…tÍQ
vËóR½b$7"éQõ7æÉl”«G)ÝêæcqhÞÂ»ëWÈnç¦€È'b_“´èX·­bñÅÈ•æQfñ•V\Ù¿ýÎoP³à”Už%°ÎbPf1\œÐuRÕt6g6ØÂ_*t4Â•:#Å—ÚÔìÑdIüî·SÁ
5jžr£óMe˜‘YîŽQ_sTÖé&ØçBàÆó±LÎ�f�–O£Ý:x)Àè²0«u½Ý½?Ã!t¼ã–~FO¶½ö!*ÐÏïm€�#xß ù…ž“©°>	ndybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvF$WvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY“á	³N	}fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJC#tHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ³¾!Vfiber/fiber/v3 v3.2.0 h1:g9+09D320foINPpCnR3ibQ5oBEFHjAWRRfDG1te54u8=
github.com/gofiber/fiber/v3 v3.2.0/go.mod h1:FHOsc2Db7HhHpsE62QAaJlXVV1pNkbZEptZ4jtti7m4=
github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=
github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=
github.com/gofiber/utils/v2 v2.0.4 h1:WwAxUA7L4MW2DjdEHF234lfqvBqd2vYYuBtA9TJq2ec=
github.com/gofiber/utils/v2 v2.0.4/go.mod h1:GGERKU3Vhj5z6hS8YKvxL99A54DjOvTFZ0cjZnG4Lj4=
github“a$³*$]³*CF³Ehamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU=
github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8&jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc“Ña³Aavalyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHId]XA=
github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE“œf$³mf‰x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyOproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E³ég'uED®¶‡xœÍYýrÛÆÿ[zŠ&v@›í™v¦•£ÎÈ"e+£¯’rœŽ›‘AàH"qÈ@Šõ¨“‡è3ôÁò$ýíÞDÊqÚª±Iànooo?{ì÷ÅUÝ†3)a’‰Äˆb.E™%ÓDÆâ¬¼ò.O•–ú@„Â$Ù,•â�“$õÄa!äb"c³ßïóÚñÕQW$Y,ï¤*“Bi± Í±ƒË$³¤˜—“ R‹~ZÞM“¾¥×]¡K�„Ä+—ºÇKÄæó¿ž	™ÍðÛÉ`F4]f1	¬ŒäƒŸ0^$Y_Ë0-’…GW§L5Õ*+$BcdA’²°o¯¯¯DšLI`=±x]&i| f86=‰žªµ"ˆFŒÊì@ˆÍ`¯©lšÌD_Q¿î[ëp‘:Î#UÒØ·½¾ø²?¨Xø¬õXÆ¯èµ§ÁÆÓ0M'°h‡ùÉli‚ŸÌoñƒÒY7Nè¬¡VÁðâ{q(>ApÏÌLf9ëŠ~ªfŠw1#»Í•)ÄD‡õúqbn…ZJ­“XZéæd“ùožÖH½L"),9¯\¾¬<¦ÿì3+c9Ë´°ž·±\à!—O&-g÷»™mq‡Ú¸Ÿa‘ÊY­E˜&¡SÄÃöÖ;öãŠ×'SNxàÞŽÿœ¶êòcETÅÉ–d6¬ó}p¼l[^Ë|}<z7ØbPÏöß{9«èV¢&B`>Ê¡oŠ°hûc½Ž§öóF:ÚßO¹Ò…ð÷÷¼ˆâõ®ðð(³HÅðöþOFe40MÃ/xîI_™,úó¢ÈéYûÙ7É,SzÉÃbÞŸ&©¤€À”	ÍÚDˆ$z$Ù¼ýÎþþ2Ôþk¤ŠCáÅréíïOË,bYýŽø´¿G4v�¶=Šc�ó
’.3sßc�ºÂ£ÿ�dƒ4ki(PœÓüéÅŸ_ˆ¾øš¨nŽƒQÇë€uá !ÎYÓLÅz€g'ZF…‚Yk®_Ž®�n§#JËÿìµ…h±ÝÀf�+(åáv¦Ú‚)
%YMø_¸:»G7Ç—'§o,K™…“Tž.ÆË×J¥¾·ˆ3VHSö½^L¤˜@63²ƒ½ŽÊBõ�B"Ê ÍJ² BÚÂÌÕê{ÙT‹eé,V1¯1(kCÚ‚‘ÄÿxáU¨�ôañ½d*žUŒaÝ=øWp]Sß«“Áí‰éü�öpl±&uN´~‡äS&bÿ^îëÚKáOïÐ#µø^m}R9ûËsqp(Àö�,˜òêrtíu^aü+f÷ôéƒìjÞ¦µ�wà‰ç"Çð=ÉÆ¢U^¶‘l®ÐÞ�Ûô²ò[Œ€Æ'y6¬Ø•›‘Þ]ˆß)„‰eãÕŽGÇbÇítêÍ›¸ÑLË/y“¾ÆŽù»¸ÀJñ1øÕœy(�£éLØÙ-†_Õ#¸¯Ö¤•3ÖÌ6¤ÎXDƒEY’Zý#E'Ø1…'}¨tñ£«ÀâÉÒc¾k”=åPDÕÙð¶,Ô­MØ\CÎôÀáÚÃ�±jˆŠ»®ˆÂ,’)Ñåàà=à×1�úÕÐkäíPH³¿#ßP Z
%äÝã9ñX„·ÒGgäFcÎÆ]ñ²Ãx.T‘L×>Ów…KÄÁøôÍéÅuëýz8:Ç2 2JÂ”Å·=^öJ�z9PÓ¬¥_3/`›Œâ¤NÜ³€u	„ŒruL9fäÆÈÅM™1J7)Æõ¨ßPgÃ.8*Š0š7è6Œ@FèˆÏá1IQO…\Ê…'PŒª›ÕUÕ¨»Ï™Éx< &ÒÉ9 IŠL®,7 j†Ãëo4í3“¨8q ÎK ´y˜ç2c9%œfk‡˜P‘5É\þúË¿1ðÁ1Šé4µ8›E¨·.³×Ñ…Ø rˆT¨é”�Â'€às©eÐÔ«SÕÈÕ¯Uˆd!BN7pGÒ?°%tF*?¶Å€}9«R*Â»]­ÚD-ªmÇ~Ø•U~ozÍm’SšGÏ°h‡(všj†ªËí®Dµ3ôñÛ£S*W¯ªéM©E£,r‚}†X`©£QGi\¿^7&[ÑÌÀUIvZZØ¤¢¦	ÍŠ�¸¶¡ÓÐþûå‰'Øœ§[ëc¯g8ú4ùèºŸbŽ.1ssÛe‰×ìdZ�\¯Òsqà%‘ÙËµZþ£Ïûõ—wàB4ì¡ZAÅ»"�!HÅµºM³©pj1G Ä†=‘Ú5×†r×(éÐ.›•ðêaÁ­<¤¦““Óq%ÎŠLõT48è<5˜©N ®^»ö.KÈµõ÷[¹F‹<Y×J.$|j‘kqøÀM}cD™Ã 2\T2@;8EÀ†Å¦C6mÌv}ÄAu`@ÁØPÞˆ¹Ò´«€ê/ÂüƒE¸?Ú¯O÷T®®Ë2Û‰£”Ö­�§³¿£�=RÇ*®­JFÎ¸(ïhª2v"éäyyÇsÁ[ ²TžPB÷Þ¯…k	FÐØÊ®I“«ÌÈ÷:)øÖÀÖ-Œÿ\"¹1ß[oá+ø$ËÑÇ"½ëu.	* ñ¥IÀk¶‘ ¦lH-Ö®:�}lê’Ó…G-Ki<ôþžºõº<VÎƒò°�e"­ƒñÕè�æïI=�êà±fÎÛÇ-:ÃKg›ËÕåøw±A~ÜÅåÝg˜¸¾r›×»@Dî`7ž¯‡ÿÇ‘\ ÁíÒÖÎž³É€ò_ÅÄÎîlKçíîõÿÀ[ç“Pºî]ê&á'¡‘›Æ�òåY'ÉS€Œ˜áa°¸ŽòÔ¤¤ãÇ|³¤€LÀŒ.3jË9mE¡Ö‰tYVLêò$ŠïÆ—”ÍdŠÅâã'F/]á.)ˆÞd†|_èn	…æË/Æ”þäÄ9›A$æ�Ëî¹1F"'l­ÊvtŠfYßX÷|sVùŸ1rKO0E›ýŽ…o‡Gƒ/ZYã2äÁ­Žçÿntfv«]IsÅÀú–Ç¶ø‰8TF„Ârª
Ï-_à­žáÎ_Yl·š¯…¸Ô‰JQR8•Y§ô@X¸+)Ç	>æs5ñzŠ…$JŠtMèÓYe-×²7M“Ù¼>-éa*ë5NÑsìãŒoB�d¥T–&Ï¬.jÀ¯*B�Z¯CG/JÕüáÅ™�w.‹¹Šq”¦j%c�õÁ"SOOR÷TYÙÅº8}‚£×@•%ÿXêö#(�MWª²Ñò'|''†êqSÇ�E ¤Àn­÷Šn—WD°×·ø›Ò~õç¹¡.ÏXmÂˆßQy­!G¥n¡Už@[E„Ýl†,!¼LjtQ½â®bWÝ<Æ·-�²ó³ÐÐ®è3´#ò0tSY¢e˜&q¥kF^cÈÒØÄÙØGx.—"kÙë…G"™Â×)›râ®0öž[›Ìw°S4)…4Pï9a{ïcmgƒˆì”Ü“¡+°x3x¡\wˆo[ÂŸÄ‚î¸Ÿ,1Éº¨oˆš�w·ÙÑw·BùoÝ-á84Þžréá ÒŸˆóÁƒ}¬j0¼B?–Q‰ªµæ3Þ·ZéVßdùg|Ð£,fþ~»zú´zc†Z[)ŽSe¬},Zö[—Ô–ï}ÛCG€õ¹Gû~LWü°óZâIÖÜy/Ñ/_<£,
a>®ï)6ìx{Ü±»+ðÝ®�†<¼V(ºèUÞEÙ¶¡\ºÙuÙ#¥_°L55gP`8ã_ŽÆ”N]6­’²½anY+Ã¹¬–�5;­7ÒµÍ™­a‡ÿ¸üÐkB—ÞeîÒ‰ð2e²d:eä²½êD#ñ7ÉÃ‹¿í¤‘E ÕÞ•BÅY-%Ô¨è)�ÌÐõ¬¨ÂDZãFv²9Š¨5biµJ{\(z—–,Ÿ}ù"[nXhd›® 0�Ïwø°¶+.¯®O/ÝEôñ´$Ì³©Ï®8*±™NþÁP°ºéE–²%ïÐE—}uÚ´±µ
ØŽno›¸;¹PnbæÃÝ*’#ÙdA	Â_Á,Š¼w?lÐ­ÚÜný³¢°µ­ã¾ikÈ·lßs`rÃ²u»Áî¸äÎÔ½T,››ñ•=–;ö�}'˜éð'14ˆt÷ƒMp­Î€´ß�URI-f¿¤�5zäƒ�P4WÑñODóÂzt“Žç; b~s—Œ(Vié[?�$öZ¥/‹C 6´qaÞ¸�®îÓ·î®ÁòöøÐ€íÛö¸*)^C�[ÎÖˆ­Ò�ýX½©Ò0öM}S@÷·p1?w•à°q“æt´ùQÁxÔõŽÎVÄï=˜>xœUROOAO¼h›X.($æI[R¶œPT!L©á ÑLwÞvG¶3›™Ù¥51xñj4ëWðnÂÁïã'ðäÑøf¶˜lº�Ù™ßûýûóøó£K�a�Jr#¹–j¢1ð$Qá©	Uf×´ƒ:´‘Vc•ŒÄhÃ½C�ëµnR¥­éÀËþ¤#1AZ1É!-`nþ|ÿøè0€A,Ðã†!GIy×¨L‡*«3ÃPÑ�;ÓëíAÂ2Æ¨=˜Û|/RÐ˜£6èèN¦´bÜyz­Šm0Rí vÇ©�öà,Fy©#@É†	rÇ%b‰Á ^3yh`c‹À¼!Ç³£­0Õ¢˜oÈÂ¸:Å+ÈkÞy&¹`Ž(	ÂPgêT(Ð™´bìüv¬‹Ÿhú{Ò‹ã±,±0ÌDÂ‹ß� ê5A‰Ýå•HP¶Ý6lÃzñunõÙ}~=¼~flE¬eÚË�«ízíiµL[2÷R¤ûvñw®S;'*ÎàYH½þÑá 4Õ€h‘½Ò:W7HÔêF—¥¢»Y†¿Ý]õª¬o×5r•a.N÷Ÿn: ©�©pŽ”F™£›�“4Qµgï‹6T9pâ2•êªžLã¬�™Æ²-ž3°ÔKzGÆ”¼è‹‡CšÊ�âŽY˜½ÒÂNa,8OðÌÁýúøG
-d‰;ÏrAuíP²eyKs¨@nõÇ'×sSwÒ´EC:à�¯×‹Æí…b§¹|sÆ·xÓÜ¿U%S|oÞ-‹M‚	„±´ïª÷|0x±Ã¹n?,âZ«ø´øeé-l9AÁqœYNÍ:6~Jópb‹{Kë‹w^ßø]aTÈo„xœë³ïµß #ÅTf¸Y_*ž )Þ™²¡xÅYínÛÈýM=Å„€w©D¦œ`·X¨ð�$n±)Çµ“…a$9²¸¦Hf†’­ÍØ‡Ø'ì“ôœ;CŠò×nŠÍ‹ÎÜ¹çž{gR'éer¡Õ"ÉËÁ _Ô•iT4B]¦U–—ãŸlU†˜-þÕJÝŒçMSóÙ6-›|¡Ã.òf¾œÆiµËëY>^d%fƒñXe‰^Tå©6«<åÖµU‹ƒ£SeýH³®µUM¥š¹Î�Ÿ®òrVÅƒUÒøõVí«³sè°Lõe´b�V
ÛÝ‘=Ž’…Vø·™ätR%?Ì*£`§¯LRf[ó¦2R=Ë¯eb:‡ë”-–˜ƒý¿„aqlÒ:©�/þ7¼É÷Ÿ«jó�/üŽßö{ñyóÏ²:ÿ¼Ì³vÂ<)î‰p¯œ&Oí¬:1v#HÞ8‡œr#±8mÓ¼…÷r›V+mÖjjª+‹ ÐIQ¨Ë²º*Û(¸ØÐ+É²©v�¾Èm£�•¸z:Sâ«WE•^Zµ,›¼`8•[`ÖßZ€¨´*}Ý¨Üª4)S]:‹³e™ªÈ¨§¯éÜÙÃ¬‡ê®²Ñ�1P`s$<%ÒâÎ€‘v|)»JÕd_!~Àû-eþ; š4©›¥ÑÏ ˜Eî‘¾Š0#ö(ƒ;õu]TF¸vá™÷Oàÿ•øñ4«¤ˆžï=ez`)ŒÎ†ÛsaY¡ßãsµl¢?Ý�‰ÄqšÄïÊc­MDE5ž”3˜ƒ#õS•—ðþ´ª
qLä3õÄ�ÒÈ 0v•|¼áà<>Fþ6³(<£êçŠ	´cUÒàïd‡(¤áÌœ‘âÆñË,Ã–òxÚRœ‰kSMõQ•iÑîö$'DÒŠóoÄ.è¨�¡�¹u,QŽ†–Á'ûªtD÷{t…jj–ä@ùÂÌµ³òÊvQ‚xQ�pËË%ÃJë¹ù#©k]f‘¼ŽD	ÌF~ÜçÉð�¤ÇN¶•Ø¶Ð¥“3¤i ’d 'k¸TíXÀ¸QfY‚âæÀ=Ø#Q•Aä¼A`u¡Ie7>C;‡ªÏKmr$f¢JøDJTÌÅ)ÓÌ1©�‚ÆŽ˜’H¹Òç’s×ýðvYÚ#5È2ûÊªÜ�y#íÃ™Ù)Í+Ûx^8XN Ì¤B=ß
4§‰Õ9êJ|Z{²¤LÆãp”å„xgþX‹õbû  F-zL�¾†w#Š~ŽÁ+c~GTB)º!0ß` BŒ=òz »cbSÁÍã‹›ÈÎV›TÄcóæ`¤>Ò:î{º¡ð<Ž¼îEvàÄ.é ¥xÊz‘ÜÓ{â¼×%,iÕ¿ý¡fv[õîèð_ž„ÊD2Ð–¤M¾ÒÅZ5¥8Ž¥ïJ­NŽ_:¹Ÿ¢Ñüª�åH.€”…L­žÇ/¾‹_üðËz¾ÆûNÑG¼ÄþœBµ‰ªŠ‰BÅ­ºŸ"Aî3Ù…G'éÜŒpÜDÅ-•U£’È#™å3¯¹9É¤.’i»WhC¼¿Åq‡ú"I×8¬Ûh'¢FŽTuID8¯�…î—˜8;GZ:0>ÁRÝ,KT�,;o"ä¶®J»¥¡çxÂÑW¿^íëT’­Ò·IÝéGèÆÎH½VRA¼ZÁ-þd·ä�N1 :¶`äÙÖô0ý,V‹.Uœô§ÀHÒ<Sûû*]¸¥¾KÛÖ›áÞ±ìlòÃ9DÐi�pæÙø}uX]¡”¶ï'!OõK0ˆ3,Tˆz¸‚ÊEÇa 6u^ºïéíg«gB í®âí÷èh=h#qãHä¬çf²ƒ=Ð³dY4œùÄÄ?&í@$eR*n$«_Wå,¿�¢x
«&ÒËâAšŠ#È“±zÈlØ&Û+9MÁ·cp§‡ønsÝ	u”u�"ÿ´Zš”Bö¡ŒyÍ‘c­ÎFyËmç,ƒø¢;äþ¬èhŠËê )wŠ¡Ë<µd£ÚfjŒ¤�r+úZ¢ÖfW^„ÜS0!zÔâª$ºœ¬*¿EåZÖ¾há´Ñ%Ù(|w© _íœF�q%ÂpL¸=`>}EÉ"Û¡Ÿ&Ã±iXw.RDÊNñ"_‘ì–<£tÄÖ£�MGa;v`3nÏÂÞ§è«_=ÿL{Ô 	=ísÐ4ýïIH=ÊB~Æ#4ägÜæ!! &”'¢_~yŒ” {$Ô‡“C¦òV‡óXÒa¸ì‰À)9¾‘ß"=Ìüÿ°Ùÿ€ƒœUÂÁ 2?À‹¿Ï8·˜ò+y§�
îjü-ºM;ÒÍ5Ï³Sâø	]8ÇÛ®›Y´XKôëMs¡ªrÓ…Ådœß§áÚ}ÜŒ}úF†«èXg†RztîìrA]øÈÁf¯™ƒ¶:‘“„¦XÇuÐAº;�ãn'´7lg<r—è!rè„[h¦ –GS6X9£[íNâ· Ùñ�™Là´Èu)Uë6ðñkyÿâ�³õB=ùþè‹“×´ÊÖL�O_BÞ/±øLÂñ^8
‘š“ç£p¡Ñðá1ÄïG1ëŽ ¼¼H6œœ�ß| í¶è]üÙÑiãÌà3lŠ“]‘§	­u7Y(.îº*ÆQþD'h1#jÃçPVï4àˆŒ­¡ÅÉŠsÇøæÃÈÊjfžñ^âXõãû÷Ç]/¶‰Ê��Hä$®ßíí�¿ßÛÛÌY’ô5ºF\¡¼+¥^Ðx
ÁÍ„_ÖZr¯/QåO�Û™¥2�ÛˆÎ™¸o‘‹«†�ÉÌT�à?Þª7r¢t_…ðŒfEL¦85 §ìÈÄG•š'&ÃE!>ºuöà+Ñ½H¬Íg<¹^±­\. BQŽQÉ}‘ÝÖ¸×#u¸s$ò
6pRÔ¶§#}£þ£ƒÃ_Z�@Y"pH `ÑÉwKÐK5,l‘ëUàp8!�K¦¢.‹�¸ä||q²Z„C…êÐjtGüµëH¶¥g	¯U3×ÿõ·Õ�·i6-! æNz-ƒðhïb ¿/s‰˜$—–òáf#wimF›x±ŒOq©@„Co,dìCÉ«ŽúÂ.}­»T3±(à®Ó˜L1ŒGŸûîPŠ}ëPÁ¨\ÃHZÑŸghÖ‘SàæaÊ%€“¨¿�¾;Ú
æ(/:Èµ¢°"N/ÞR¿<Zšb„T!™ø�àBAH„—!CuëÀ#lRwCáIjûHÄ¦{CZÊ±ÇH\ìÃ•zÎÔ1˜òÛc–Â-~ûÍ�€ÎZn;Ç…”›Âƒ@nâ5ºbÏs$Ìø-.qç¸Têz„¹Ð3ÊB'ê÷ˆúû;Dý ÃŠ_ÿ½ºLˆ¼ÃG™¶¯o‹»;”ë.|}!„tóœ¸Ò¸/õ‰›„nzˆªüE
©zÚ[¼ÕÖò?Bœ²í¢…•U7í âP¹pöÃ‚šr€ë]zN*ÅðçPô�SáîÅ¦§˜-Û�7=vÚöÐÒŸÎÞR&àú�w=n¨p,ÛZ{û�·›tóœˆÛä|�¢ñþ¡Kxl“sx óÿ’4ñ3°•³¶\%°¿xÍX]sÓF}¶~ÅV3eä`dhfê6`˜&ÃC&²´±ÕÈ+#­pRà¿÷œ»+YvH™>4I¼wï=÷Ü³w½NÒ‹d¡Õ*ÉMä«uYYƒ0-�Õ—6Ä¿Ú¤e–›Åø¯º4(ÊÿmÇKk×ü¿¾2)ÿÚ|¥Ã ÿ,r»læqZ®Æ‹²Ê‹"oô¼.Ó£Ã �Õ±N
nxÑÌá€�µZ5z�.á�z§ç3Ù êf^§U¾¶yiê8°Wk½³»¶U“Zõ9¬%?t(>~wÔ Š`�¹6Ö“õéÁ¦žÊÇ3·ëó×àkàL¶3jk8<èœ�§	ôã4ˆÅ@çl^–…BTá¼€Óu8R¡­S'©xÍÏnf’JxÐ6�;—ÅãÎß²Ö™¢I:×ÃêH×5–×Ê.µÚä•VçeµJ,ÿ¨ÊCªô'¼U»yÞ	‘t€Iê“<	‰pø!L%\à¢lç%‚Q¹Ê­^­í—>OlâL%æŠÿxSÆwWž çµMVk•ûäq·’\�q˜sQÏ\ÞçúXl0Ç°7KÄÚ&´Ö&S¶l	2×>âk·!wS{!1£àØq3�.W#ÕÏ!>•Ú¼—±s]Õß–øk¥P7)”cv]4‹ß•@§U¦ÏÐÞÇüJoú…‘"¥…‘(£7Š5œ7&U»Ë¢¡:èïB%TÚ6•Q÷zÃmKaê^èèæ‚Ž‚�T‰× •T¨n©ñï.©¢,×±š&&ÕË^J,ÆœÑrÇ£!E\æ•…uD…²jm]¨ÔäP‘1b;‘‘è×‡êÀ�Í46fÃ` ´°Ôíˆg80‚šH~†WëB;%¤I­Õpfü¼4:N0?XÆi�âzZØ‡Ï&ã–{ÃS¿¸‹WVõ¸|J*Õ¬U’Á•Cµ‡·~®L—:½x]å‹ÜLSUê€z{2¥¾ÅkŸ+¨˜&[¦¯�gj™˜¬€ êUs©
Tt�ã‘š˜©—H©lìD=Úƒ	‹�öìYs¬fùßz>zøËcL¼«P´;3nÂUm(7§Ì­Ì¼89yãªDgÆŸ�õåº(+]�+¿ú¶ÌïÚŒÀb‡B½†škñ§©kè C¤Ü@&+¡FxìAŽ6Ø4R&/�Èü\–ýtÈÏ=L™°VÿÉ¯{­Æ“,4?!!pIï•EOÖ]=àÎª‰_BÝIIn•S÷÷Dp*óù+î·ø­áe E^gT+Š"³P;jyë^L1‹Zè½VÓMŠ4ˆÞËnwH—;%�øÂ)7Ñ0~kòË#\¹y$Ž³<XÈ „íÄgr+šãÆtê7^ã2£÷Èjö¦Y­#ž·2-tbšõªÎê¨ƒdäZ`fy<e¤F´ìšVlÏÆÍë¯kŠ…1ñL[†ó2Ç=~øÛ´;õÓEŽªw‚"¸<Í²èÉ^¥wv¾)ÍÂ±µŠ¤d	†ä–˜?¦Ud¬ì‹Öû‘ZÕ‹ŽèþFèY âs�â[ñß~ ì¥ Uú£ê.G¯Q˜òÇ¢vØ •«¤‚ú‘¸|»†¸ÊP¢½Úcšmnp[êoPor›.yDÜ!U*ªû�–iïÂE–/ôkyÇN¢«sMÜ	C9c0à†½õê¾
'!~w;h�8È^™ó#‹ã6XâÔènQÇk7r[ï–pWj™ÔðÀ5Y®À;gXÝé©|å÷:MGÊÏ!z	ˆANv‘à�Ÿ»h&ÝÝ²òÀù-Ú¾ðAÜù8¹í.žUe’�Vñ@t9Æu®l&’}…oÜ·˜q†�È"G‡ËvŠÜ—¶ê¶û§;$’®˜r
�’ÎÖA9RlRqø•ˆˆMz¡þäyr³oM¿)�¹D‰]f€ý°Ï�´ÀºF¼®äÅ¥³N¤ðŽ¶eÍº.m#C�û£7Qúï�&‰šxMœ—
šéOË5n#qÊ+ûq{Í¹vKn¾ãÝÙo¹äH%&o’Nþ]eKêvJ	¯ˆ  u;]]u@ž©/_nž¹�Ju)>#Y;Ã=ˆ3Ï³]CR'›È
ÜXáB¾›/ž^»Ç îÄ„°Ût÷;9r�þW0K½Â]„ø�ð¶­/£ÛigzyîÁzç4ËµÜÞëyK†Ão>5Ú—žMðÂp®Û	¯}¾=l^£+ÇcöæœÍ¸ žBâ]Ùø=&SZÈÉ‡�<"þ5©eÿýC4èxß8jrˆ\»�©€¦{ší:LÞ¶í¿æ€¬+1Ú!Én_µ¸wªû.‹ø½¬§ý¦©%Æ¾œùvªwÓ9Èå×1€e…v„ýZ&U½U™^¿!-üm×þÛ„—W/*�kèÄÂŸ³×¯ØU\ï':ºûßAÅoð½‹=�ÂS´‹¢ÔgjÃÇ�ëÖ&êçOüŽ¥ªvkâ:ÑÿÓ³ù•ÕÿC`Ú.pûÎ<ÁËÙ�ºËë˜‘!ÿ £Ë[[îq�qxáøð'ƒ5�´, plus a
// parallel set of SSE subscribers (see realtime_sse.go) so the bundled
// SPA â€” which opens EventSource against /blocks, /transactions, etc.
// â€” can subscribe to the same broadcast feed without speaking
// WebSocket.
type RealtimeHub struc‘Ì:	sse     *sseRegistry³	sse:     newSSERegistry(³©
// channel+chain â€” both WebSocket (`HandleRealtime`) and SSE
// (`HandleSSE`).
//
// WebSocket clients receive the JSON-encoded RealtimeMessage as a TEXT
// frame. SSE clients receive the same payload wrapped in the SSE
// `event: <type>\ndata: <json>f\n\n` framing so the EventSource
// onmessage / addEventListener('<type>', ...) handlers fire cleanly.“€“åõ4WebSocket subscribers (existing path).
	h.mu.RLock()“ž+“b•	h.mu.RUnlock()

	// SSE subscribers â€” wrap in SSE framing once, fan out cheaply.
	//
	// Two shapes, built up-front so fanout() is allocation-free:
	//   - perChannel: `event: <type>\ndata: <RealtimeMessage JSON>\n\n`
	//     for the per-channel /blocks /transactions /etc. endpoints.
	//   - wildcard:   `data: {"event":<type>,"chain":<slug>,"data":<RealtimeMessage.Data JSON>}\n\n`
	//     for the multiplexed /v1/base/realtime endpoint the SPA opens.
	//     No `event:` line â€” the SPA uses `onmessage` and routes off the
	//     envelope's `event` field.
	if h.sse != nil {
		perChannel := append([]byte("event: "+eventType+"\ndata: "), encoded...)
		perChannel = append(perChannel, '\n', '\n')

		envBody, err := json.Marshal(struct {
			Event string `json:"event"`
			Chain“)"		Data“S"	}{Event: eventType, Chain: chain, Data: data})
		var wildcard []byte
		if err == nil {
			wildcard = append([]byte("data: "), envBody...)
			wildcard = append(wildcard, '\n', '\n')
		}
		h.sse.fanout(eventType, chain, perChannel, wildcard)
	}
}

func (h“à³àXó\wº¤xJ*µÕpackage main

import (
	"fmt"
	"net/http"
	"strings"
	"sync"
	"time"
)

// SSE endpoints â€” Server-Sent Events at the URLs the bundled Vite SPA
// expects (/blocks, /transactions, /token-transfers, /internal-txs,
// /tokens, /gas-tracker, /stats, /validators).
//
// Why this exists (2026-05-21):
//
// The SPA opens EventSource against `${window.location.origin}/<channel>`
// expecting `text/event-stream`. The Go backend previously had no
// handler for those paths, so every request fell through to the SPA's
// own static fallback (`handleSPA` â†’ index.html), returning
// `text/html`. Browser console then logged:
//
//     EventSource's response has a MIME type ("text/html") that is not
//     "text/event-stream". Aborting the connection.
//
// â€¦for every channel, on every page load. PR #4 silenced the HEAD
// pre-flight (so the SPA cleanly disabled itself). This file goes the
// other direction: make the SSE endpoints REAL, so the SPA opens them,
// gets a proper `text/event-stream` reply, stays connected, and
// receives broadcasts.
//
// Wire shape:
//   - GET  /blocks                      â†’ SSE for hub channel "blocks"
//   - GET  /transactions                â†’ SSE for hub channel "transactions"
//   - GET  /token-transfers             â†’ SSE for hub channel "token_transfers"
//   - GET  /internal-txs                â†’ SSE for hub channel "internal_transactions"
//   - GET  /tokens, /gas-tracker, /validators â†’ SSE that stays open with
//     keep-alive pings; no broadcasters yet (placeholder so the SPA
//     channel doesn't show as broken)
//   - GET  /stats                       â†’ SSE that emits the hub.Stats()
//     map every 5s â€” cheap server-side state, useful liveness signal
//   - HEAD <any>                        â†’ 200 + the same headers so the
//     SPA pre-flight (`fetch(url, {method: HEAD}).then(r => r.ok)`)
//     succeeds before opening EventSource.
//
// Each connected client gets its own bounded channel. Slow consumers
// drop messages rather than back-pressure the hub; the SPA refreshes
// stale state via REST on reconnect.

// sseClient is a single SSE subscriber. One per connected browser per
// channel (the SPA opens one EventSource per channel it wants to follow).
type sseClient struct {
	channel string        // hub event-type to receive ("blocks" / "transactions" / ...)
	chain   string        // optional chain filter; "" = unscoped
	events  chan []byte   // buffered outbound queue; full â†’ drop
	done    chan struct{} // closed when the client disconnects
}

// sseRegistry holds the live SSE clients. Kept separate from the
// WebSocket client map so the existing readPump/writePump paths don't
// need to learn a new client shape.
type sseRegistry struct {
	mu      sync.RWMutex
	clients map[*sseClient]struct{}
}

func newSSERegistry() *sseRegistry {
	return &sseRegistry{clients: make(map[*sseClient]struct{})}
}

func (r *sseRegistry) attach(c *sseClient) {
	r.mu.Lock()
	r.clients[c] = struct{}{}
	r.mu.Unlock()
}

func (r *sseRegistry) detach(c *sseClient) {
	r.mu.Lock()
	delete(r.clients, c)
	r.mu.Unlock()
	close(c.done)
}

// fanout pushes a pre-encoded message body to every SSE client whose
// channel filter matches. Called from RealtimeHub.Broadcast.
//
// Match policy mirrors the WebSocket subs lookup:
//   - exact channel match (client.channel == eventType)
//   - chain-scoped match (channel + ":" + chain) is checked too so a
//     subscriber that filtered on "blocks:cchain" sees only cchain
//     events even if the hub broadcasts blocks across multiple chains
//   - wildcard (`*`) â€” clients attached by HandleMultiplexedSSE receive
//     every event, but in a different envelope: SSE `data:` carries a
//     JSON `{event, chain, data}` object the SPA can route off of. The
//     wildcard `body` is built here; per-channel clients keep using the
//     prebuilt per-channel framing from Broadcast for cheap reuse.
func (r *sseRegistry) fanout(eventType, chain string, body []byte, wildcardEnvelope []byte) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	for c := range r.clients {
		var payload []byte
		switch {
		case c.channel == "*":
			payload = wildcardEnvelope
		case c.channel == eventType:
			payload = body
		default:
			continue
		}
		if c.chain != "" && c.chain != chain {
			continue
		}
		if payload == nil {
			continue
		}
		select {
		case c.events <- payload:
		default:
			// Slow consumer â€” drop this event rather than block.
		}
	}
}

// HandleSSE returns an http.HandlerFunc bound to a specific channel
// name. main.go mounts one per path.
func (h *RealtimeHub) HandleSSE(channel string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// HEAD pre-flight: GCP HTTP/2 LB + Traefik can 502 on HEAD
		// responses that carry `Connection: keep-alive` together with
		// a streaming content-type. Keep the HEAD response minimal â€”
		// the SPA only checks `r.ok` (status) anyway.
		if r.Method == http.MethodHead {
			w.Header().Set("Content-Type", "text/event-stream")
			w.WriteHeader(http.StatusOK)
			return
		}

		// SSE response headers for the actual GET stream.
		w.Header().Set("Content-Type", "text/event-stream")
		w.Header().Set("Cache-Control", "no-cache")
		w.Header().Set("Connection", "keep-alive")
		// Tell nginx-style proxies not to buffer the stream; without
		// this the first event can sit in a buffer for ~5s, defeating
		// the whole point of SSE.
		w.Header().Set("X-Accel-Buffering", "no")

		flusher, ok := w.(http.Flusher)
		if !ok {
			// Should be impossible on Go's net/http; defensive.
			http.Error(w, "streaming unsupported", http.StatusInternalServerError)
			return
		}

		// Subscribe to the registry. `chain` query param is optional;
		// blank = no chain filter, see fanout()'s match policy.
		client := &sseClient{
			channel: channel,
			chain:   strings.TrimSpace(r.URL.Query().Get("chain")),
			events:  make(chan []byte, 64),
			done:    make(chan struct{}),
		}
		h.sse.attach(client)
		defer h.sse.detach(client)

		// Initial "connected" frame so the SPA's onopen handler fires
		// with a recognizable payload (matches the WebSocket "connected"
		// shape).
		writeSSE(w, flusher, "connected", []byte(`{}`))

		// Keep-alive ticker. Comment lines (`: ping`) don't fire the
		// EventSource onmessage handler but keep the TCP/TLS connection
		// alive through middleboxes that idle-timeout at 30s.
		keepAlive := time.NewTicker(30 * time.Second)
		defer keepAlive.Stop()

		// /stats is a special case â€” server-side periodic poll of the
		// hub's connection-count snapshot. Useful liveness signal even
		// when no hub broadcasts have been wired up yet.
		var statsTicker *time.Ticker
		if channel == "stats" {
			statsTicker = time.NewTicker(5 * time.Second)
			defer statsTicker.Stop()
		}

		ctx := r.Context()
		for {
			select {
			case <-ctx.Done():
				return
			case <-client.done:
				return
			case <-keepAlive.C:
				if _, err := w.Write([]byte(": ping\n\n")); err != nil {
					return
				}
				flusher.Flush()
			case msg := <-client.events:
				// Broadcast messages are already JSON-encoded by the
				// hub; we just prefix with the SSE framing.
				if _, err := w.Write(msg); err != nil {
					return
				}
				flusher.Flush()
			default:
				if statsTicker != nil {
					select {
					case <-statsTicker.C:
						// Synthesize a stats event from the hub.
						stats := fmt.Sprintf(
							`{"connections":%d,"subscriptions":%d}`,
							len(h.clients), len(h.sse.clients),
						)
						writeSSE(w, flusher, "stats", []byte(stats))
						continue
					default:
					}
				}
				time.Sleep(50 * time.Millisecond)
			}
		}
	}
}

// writeSSE emits one SSE event frame:
//
//	event: <name>
//	data: <payload>
//	(blank line)
//
// Per the SSE spec a frame ends with `\n\n`. Browsers tolerate `\r\n`
// too but `\n` is the canonical form Go's net/http uses elsewhere.
func writeSSE(w http.ResponseWriter, flusher http.Flusher, event string, payload []byte) {
	fmt.Fprintf(w, "event: %s\ndata: %s\n\n", event, payload)
	flusher.Flush()
}

// HandleMultiplexedSSE returns an http.HandlerFunc that fans EVERY hub
// broadcast into a single SSE stream, wrapping each event in a JSON
// envelope `{"event":"<channel>","chain":"<slug>","data":<payload>}`.
//
// Wire shape the bundled SPA expects at `/v1/base/realtime` â€” it uses
// `onmessage` (not event-typed listeners) and routes incoming frames
// via a handler map keyed off `msg.event`:
//
//	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);
//	};
//
// So the SPA gets ONE EventSource and receives all event types over it,
// instead of opening N parallel streams. This is the canonical realtime
// channel â€” the per-channel /blocks /transactions /etc. endpoints stay
// for backward compatibility and external consumers, but the SPA uses
// only this one.
func (h *RealtimeHub) HandleMultiplexedSSE() http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// HEAD pre-flight: keep the response brief â€” GCP HTTP/2 load
		// balancers (and Traefik in some configs) 502 when a HEAD
		// response carries `Connection: keep-alive` + a streaming
		// content-type, because they expect HEAD to be a closed-body
		// response, not an opened SSE stream. Return only Content-Type
		// + 200, no streaming-only headers.
		if r.Method == http.MethodHead {
			w.Header().Set("Content-Type", "text/event-stream")
			w.WriteHeader(http.StatusOK)
			return
		}

		w.Header().Set("Content-Type", "text/event-stream")
		w.Header().Set("Cache-Control", "no-cache")
		w.Header().Set("Connection", "keep-alive")
		w.Header().Set("X-Accel-Buffering", "no")

		flusher, ok := w.(http.Flusher)
		if !ok {
			http.Error(w, "streaming unsupported", http.StatusInternalServerError)
			return
		}

		// Subscribe to ALL channels by registering a wildcard client.
		// The empty `channel` means fanout() needs special handling â€” we
		// use a sentinel "*" so we don't accidentally collide with a real
		// channel called "" elsewhere.
		client := &sseClient{
			channel: "*",
			chain:   strings.TrimSpace(r.URL.Query().Get("chain")),
			events:  make(chan []byte, 128),
			done:    make(chan struct{}),
		}
		h.sse.attach(client)
		defer h.sse.detach(client)

		writeSSE(w, flusher, "CONNECT", []byte(`{}`))

		keepAlive := time.NewTicker(30 * time.Second)
		defer keepAlive.Stop()

		ctx := r.Context()
		for {
			select {
			case <-ctx.Done():
				return
			case <-client.done:
				return
			case <-keepAlive.C:
				if _, err := w.Write([]byte(": ping\n\n")); err != nil {
					return
				}
				flusher.Flush()
			case msg := <-client.events:
				if _, err := w.Write(msg); err != nil {
					return
				}
				flusher.Flush()
			}
		}
	}
}
d—¾Áxáëpackage main

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
	"time"

	"github.com/gorilla/websocket"
)

func TestRealtimeHubStats(t *testing.T) {
	hub := NewRealtimeHub()
	stats := hub.Stats()
	if stats["connections"] != 0 {
		t.Fatalf("expected 0 connections, got %d", stats["connections"])
	}
}

func TestRealtimeWebSocket(t *testing.T) {
	hub := NewRealtimeHub()
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	go hub.Run(ctx)

	mux := http.NewServeMux()
	mux.HandleFunc("/v1/explorer/realtime", hub.HandleRealtime)
	ts := httptest.NewServer(mux)
	defer ts.Close()

	// Connect WebSocket
	wsURL := "ws" + strings.TrimPrefix(ts.URL, "http") + "/v1/explorer/realtime"
	conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
	if err != nil {
		t.Fatalf("dial: %v", err)
	}
	defer conn.Close()

	// Read connected message
	var msg RealtimeMessage
	conn.SetReadDeadline(time.Now().Add(5 * time.Second))
	err = conn.ReadJSON(&msg)
	if err != nil {
		t.Fatalf("read connected: %v", err)
	}
	if msg.Type != "connected" {
		t.Fatalf("expected connected, got %s", msg.Type)
	}

	// Subscribe to blocks
	sub := SubscribeRequest{Subscribe: "blocks", Chain: "cchain"}
	conn.WriteJSON(sub)

	// Read subscribed confirmation
	conn.SetReadDeadline(time.Now().Add(5 * time.Second))
	err = conn.ReadJSON(&msg)
	if err != nil {
		t.Fatalf("read subscribed: %v", err)
	}
	if msg.Type != "subscribed" {
		t.Fatalf("expected subscribed, got %s", msg.Type)
	}

	// Broadcast a block
	hub.Broadcast("blocks", "cchain", map[string]any{"number": 1})

	// Read the broadcast
	conn.SetReadDeadline(time.Now().Add(5 * time.Second))
	err = conn.ReadJSON(&msg)
	if err != nil {
		t.Fatalf("read broadcast: %v", err)
	}
	if msg.Type != "blocks" {
		t.Fatalf("expected blocks event, got %s", msg.Type)
	}
	if msg.Chain != "cchain" {
		t.Fatalf("expected chain=cchain, got %s", msg.Chain)
	}

	// Verify stats
	stats := hub.Stats()
	if stats["connections"] != 1 {
		t.Fatalf("expected 1 connection, got %d", stats["connections"])
	}
	if stats["subscriptions"] != 1 {
		t.Fatalf("expected 1 subscription, got %d", stats["subscriptions"])
	}
}

func TestRealtimePing(t *testing.T) {
	hub := NewRealtimeHub()
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	go hub.Run(ctx)

	mux := http.NewServeMux()
	mux.HandleFunc("/v1/explorer/realtime", hub.HandleRealtime)
	ts := httptest.NewServer(mux)
	defer ts.Close()

	wsURL := "ws" + strings.TrimPrefix(ts.URL, "http") + "/v1/explorer/realtime"
	conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
	if err != nil {
		t.Fatalf("dial: %v", err)
	}
	defer conn.Close()

	// Consume connected
	conn.SetReadDeadline(time.Now().Add(5 * time.Second))
	conn.ReadJSON(&RealtimeMessage{})

	// Send ping
	conn.WriteJSON(SubscribeRequest{Subscribe: "ping"})

	var msg RealtimeMessage
	conn.SetReadDeadline(time.Now().Add(5 * time.Second))
	err = conn.ReadJSON(&msg)
	if err != nil {
		t.Fatalf("read pong: %v", err)
	}
	if msg.Type != "pong" {
		t.Fatalf("expected pong, got %s", msg.Type)
	}
}

func TestRealtimeUnscopedSubscription(t *testing.T) {
	hub := NewRealtimeHub()
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	go hub.Run(ctx)

	mux := http.NewServeMux()
	mux.HandleFunc("/v1/explorer/realtime", hub.HandleRealtime)
	ts := httptest.NewServer(mux)
	defer ts.Close()

	wsURL := "ws" + strings.TrimPrefix(ts.URL, "http") + "/v1/explorer/realtime"
	conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
	if err != nil {
		t.Fatalf("dial: %v", err)
	}
	defer conn.Close()

	// Consume connected
	conn.SetReadDeadline(time.Now().Add(5 * time.Second))
	conn.ReadJSON(&RealtimeMessage{})

	// Subscribe to transactions without chain scope (gets all chains)
	conn.WriteJSON(SubscribeRequest{Subscribe: "transactions"})

	conn.SetReadDeadline(time.Now().Add(5 * time.Second))
	conn.ReadJSON(&RealtimeMessage{}) // subscribed

	// Broadcast from any chain should match
	hub.Broadcast("transactions", "zoo", map[string]any{"hash": "0xabc"})

	var msg RealtimeMessage
	conn.SetReadDeadline(time.Now().Add(5 * time.Second))
	err = conn.ReadJSON(&msg)
	if err != nil {
		t.Fatalf("read broadcast: %v", err)
	}
	if msg.Type != "transactions" {
		t.Fatalf("expected transactions, got %s", msg.Type)
	}
	if msg.Chain != "zoo" {
		t.Fatalf("expected chain=zoo, got %s", msg.Chain)
	}
}

func TestRealtimeStatsEndpoint(t *testing.T) {
	hub := NewRealtimeHub()

	mux := http.NewServeMux()
	mux.HandleFunc("GET /v1/explorer/realtime/stats", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(hub.Stats())
	})

	ts := httptest.NewServer(mux)
	defer ts.Close()

	resp, err := ts.Client().Get(ts.URL + "/v1/explorer/realtime/stats")
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != 200 {
		t.Fatalf("expected 200, got %d", resp.StatusCode)
	}

	var stats map[string]int
	json.NewDecoder(resp.Body).Decode(&stats)
	if stats["connections"] != 0 {
		t.Fatalf("expected 0 connections, got %d", stats["connections"])
	}
}
£Þ}xºÑxÕYmoÛFþ,ýŠ	qÉQ9ŠñµE€ªð‰ã\zH|��\q0|ñZ\I¬ù–]Ò¶ø¿÷™Ù¥DÉfì½Í‡ˆ¢vwÞžyff]©é¹škÊUZ‡i^•¦¦p8t1-“´˜?ûÕ–E€³¼æ�¬œóG¡ëg‹º®ø¹´ü¿Ñs}%ßmm°Q^Úe1†øu^Vçó8-ž-UžÅßÃÑpøìí- x¯,féœ=KmI‘ÅþLÓ”¤º¤´HôU<¬—•ÞØIÍ´¦/ÃÁQÖÌ	ÿœl:e­'�ÅÛ€X¤>T®o®,ð¶])ÏX)ºýü
Òëç?P{¦(õ)MÚÕ«ïØqø~��ÞÒÂTÓv1?bÝ6äÆ:6¯](Ï§é‹<¢DÍ#ªDTDWò9¼Ò3Õd5�•e†Ã¼Íð"¿mj¿BêQÙ˜)änùHÞ¶ËÇ��JD"ÒÅEDyRØˆT’"×ë¨Ù×)b”ZªAªÆ™¾Ðý÷Å»·,�iŒ¦Yi\m,¡ïÑ°Žá‡ÛÒñ‰<xPœºè‰É¾ë(°_ÔfI‹2K3²‚œâTe�¥´¶dš¢Np[«Zwä6w„;vD{QŒ¡÷ª®µ)ÈhöÝçò[X^RV^j3UV“Êª…*š\cý�Ëj¡‹xx¡Œ¬nÏØÅ)œ)ñ»ÆÖ{e^Á‹áéÿŽÕø·�ñ�'þs|òe'zþýõ_N;yr¨ç)X²ÓÜn´JÆVÍtÔÚ9Î›Z�­RÇÖ¥Ù°zuÂÚð¼a$gj|øË»¦ÖWÃ�s7X¡:f‹‹ùÉSñŒóPÉöÓø ôÍòôP«Œýý¦9óî;Ð—²s%y
­kÎò‚t^ÕKv‡XgM1¥íáˆœìÕ	Hw£®‚žl�÷^ñ	4?×áíê�"¬ƒÂÖÒ::‡üÛµ×üE’xÝ´Y,¦CmIh§3ç
D$-.T–&„·*ãÐ,I_Á2K3SæØ"�/´W&-M
Ã­dž7;4[fŽ
„ÓÙ¼Ëx#/¦Bò£6ãwªž.Ž$X¼+>B„FL�­³@àñ>+=ƒVW	ããÏAD«-ì >œ_0ŸíîRô�>ƒ�>7©ÑÉ:Õ©ÿDa¾õ‘,Ä½¢ D‡:‚ü˜8oâ·åô< 0mH^},2÷Rçš•ç4AZÅµÇ­)'?ñ/ì 6upDÊP‰ÿLš ƒ9òUež4°Î›í‰’ÕdîèÉJH‹/Å~Í£Õ–s›«E)xd
GQàÆ„.RE�mÇû`\†Ì9ð‡…y‘eT‚rÁ*‚;A~Õ(ƒISeéÉEa>úX%üŽÊQÉgK>cÓÀ(ïÝÑ¸£™¸Øèq’Ú)ûx9¡Æé�)[�­Ö*{ÊÈ@ƒmKl-ìð»Ïñ"ÍZTlÇ—v=-}q‘™ðð•ø=¨«Ü�[Ž9AKàxKác;òŽÜÝŠ„O ŸâkÛÑÊ•¢C�ÃX4/™ZógŸ!¿‘�ýÙíö…’�ŽcG®’H÷K€O7�Ï‡õ�<ì½®3•Y-ií”ÛL‰DEÕã+#¦­½…°³„µPåµgK�6�ZêéÝCÙyä¯ÖçY¿KÜ]—ˆó{ðN’êt»—�n9ªÃ�«´-ÊÌÖÉÚzæ(ŸIÒMr9Š�-EÈo»‚h~Ü&Öž][i°&ã=n�ÐŽbßŽ°Ú†°öW'ÏëŠší‘é)¸ecÙ¶qzÛjºÓý·á`cMËšH«Èm®+|Wœó.»þ©kÐûU	¦;S›º Q(þpüI–IeÞRh;•Fl¼?†:’¾\G´Î=ŸŒ]—EÔI ·àýµ© ù©´ÃýÙÂÐm¶Ë-yÜeUÙÔ\3¥MÚ8!¢�ˆÀÑ+’!Ý¹‰Ûh)³ªÀ¼ØRˆ ‘Û%UUºHB|ÁJŸØ {Óñ‹�êòim,Ïè›Ï¸4Ïàn5—Ç»< ÇÀ yÖãálUÙ0Ð+õ¶TÉktj2ÖdøÂr‰ïßd²™¡€}%&�3ÂJÕ?luú6eÃÎ,mŒö3a‘²þ±ð�ÈTd�¸·¥ñ;q{‰ÎaB�/AK,HŽw<rÌkÊŒµ:eûXäÊØ…ÊB§Í“Ùè§{H­°I÷‹õ(™®Q2‹%¬˜1m{&°¥ë�Ðî0tp“ÉžØ¿ª $o�p6Fc …Q­S¤Ë“¶ø¦¾}dcÏÓ
:»&ÜÉ…Û.¸…‘¹uµ¿m`< ‘�[Pºã¤c`,µ†Ž\0 ²ÚšÉZxíiiïÍ‹ŸÑ•ÂFBÈ&$÷ôÔ—Yyˆã8psÑ-BçÈ³Ç
kÈ‹J¥‚3#IëÚ¡O¬yGDA¸!AVîú#lüÁ¤ùQ¥¦ËÔø¸5«b7€7Ñª¢pÍ)¬Â°ƒmÈ9� i—çObŸÉòý;ÁÇWØ,+—aìDyriÜVÕŒ•:ê‡_rSÄ£_«d×Rè¼s"¢ÜÝgà+“dßy÷çq�å…2çðêïcv  à7€ßM¤ÏæÄ:Nm�îÇÁM¨»;�éÍ‡ïér/ãIoä’Ç½è)K¤,ýëèßýT¸Þ^_‚élUVÿ‚éWÜYèÏôÔÿò¹ÁÕŠ ñ2~rÓ&ÅG¨ãB¨Çì?D;@™‘)'-w7	Hñm_ŒQ~Ÿ/-±õr»Çî€*–€À‘z€±)v%”]FBIÂK¼õe¯¯G¾68cøF`u}�aòÒÇì´Éò.—0}“G˜ÎÌ$w½5þ•vÆ‹Gß©«—KÌ#\]Ø!âíø%Ô‹è‡�Ÿ�F±[>ÁÑ7&‡ìómï=ý &•9Ü&¢\_ŸF.´G¸^kìK•â2€#	—:RåÆò–öÐ—º5«ß¦ÉCð0¸ŒdIbŒS‘‰ ð[Ç}5.­Wös•t·(áèèZ9„Ôo‡n¿¦¨ñµN¾ŠnÝBïœÂî§Á¾	Ÿ2Ârõç·œ‹ÿ¨¬Ñ¡»|‡úˆ²¬Xßúô‚KÖµ÷IÁ=Ñåú�?u‚tÓÙÄ�1Xæß›Ùù õçÄAY¿æ—áü‡ç„Ÿõ"ú$m!÷t2›AØC¬è×ÏÚÌ‡‡^Î8¾÷W4®d@º=2¨µ|+å<-ÝÕCœý„L°¾VùCh´�?´\Îj,Š=þhÉnâ«GW¡*)û¿º]jâêJ’4xªUûš:£:�J‘T1“;legacy `chains: [...]` YAML shape used by tests and the
// minimal config. The full Config struct in config.go is the canonical schema“p²“¨Œ“E<	sup    *ChainSupervisor // nil = registry-only mode (tests)“ZÂttachSupervisor wires a ChainSupervisor so admin/config/mdns adds also
// spawn per-chain indexer + graph goroutines, and removes cancel them“¡AttachSupervisor(s *ChainSupervisor) { r.sup = s }

// Add registers a chain. mDNS-sourced chains never override higher-prioritLy
// (config/env/admin) entries; admin/config/env duplicates return an error“¡³œ“¸.“B.mu.Unlock()“Kd“ï.mu.Unlock()“K(“0“{!.mu.Unlock()
		return nil
	}“©1r.mu.Unlock()““ñ?)
	if r.sup != nil {
		r.sup.start(cfg)
	}“/	t“¹	%.mu.Unlock()“Ú	,r.mu.Unlock()““
)
	if r.sup != nil {
		r.sup.stop(slug)
	}“5
““").mu.Unlock()“á“	“"³8$(	updated := entry.Config
	r.mu.Unlock()
““u@
	if r.sup != nil {
		r.sup.stop(slug)
		r.sup.start(updated)
	}“�““!Õ³÷>d using the legacy `chains: [...]`
// shape. Full Config (with brand, networks, etc.) loads via LoadC“n
“O„“Ôv“K\c.WS = os.Expand(c.WS³˜i³/a³I2“�&“¶N“ð[“‘5³�B“ÄÂ“‡Ø³`p“ÑÁ“‘Z“í-¿—‘ô½±xÅZûOÛÈþ9ù+¦–öÊì¦eY¤Fâ‡òèc/¥ÂÞGUQOÀ·Ž�µ·â¿ß93¶Ç±'$öJ­pÆ3gÎœÇwã©?úæ_I1ñÃ¸Û'Ó$Í…Ûí8—w¹Ì<Èx”a|ÕÿO–Ä4Ë¼�çSó™° §Á„×Mýüº?#I4L¯AÇént»ãY<CœÊ«0ËÓ»7Að&Žðìæâg=ÕnˆïÝN*»âXÎ÷¯Ád±À•ŽLù]êa¹Ë¯÷“x^aQç,š]„ÎhDëœÆŽý‰ä±ý—<›OOöiL8t¨A¿%#?ºN²|ðzç·Í¾¼Íû—£þ~?�ŽxþðnªˆÈ›	œ%³t„!gÄ›ÓØýF·Žñ÷bWÄaDçèäÞ[?÷#Ãx�`Nêí'³8w7hæ+sÞØuäíTŽràŸ¢'®’\ü8½j�&Åï3Uê±jøóæ�B{±m•Ñ<%4½]†íêtxO ¡Çñ�…íõ$ŠTZtöÞˆ³£ów`„ÕUèêÖ1$½Û”t%@È<IÅÿCÅ¢ s:Zx¤(Ü¢éí=¯/³‘?•O±?ùƒÈSÿF¦™l´Éóc˜epú)ÅÉžÚÊØ1!Ç‰b�¢ÄØÆÿ©œ$7ruÞ;V´ðÜ0†ž(}Ñ&pzX;Ù‹ÔÓœ¨Ó2®”þXY‰š$òD¤2Ÿ¥1ô4“êt_Ý4ÈpÕMí+Âç2!’€I£à$NbyÄ“qî¬ÌÐØ�2Év\[NpÒ&ÿóiàçÏ ÿ$"ªù¢* Á�5@îÅÀ¤ÙQJè‰}BfÙÚc9_$LC†›/ÔÑøª'’oj·w2wµ¾•à…‰Ãƒšˆ©c`p 56c™UM�¸cð$fL"&HÇ˜´‹	&lêµŒ3dBDbcZ:à,#‰ ³[M[ :V V’>Nò·t²5°¡+ÓÎZUö6ˆ*w±CÈÇƒã³“4LÒ0¿[ýÝN¿/”i‰Œ´Èýo2ÓTŽd€¬F"³°~‘3Yæu“/~;¦Õƒ�	xÙu2‹63 Aš†¶.Ål	9+r0	bŠÎ¥×ñoÃYVˆ‰ókZ°! €_Á¦ÐÇÑQ’�ëÂð-Í$^·[¶^nóõšDJ›N�†kØø�û³(§„luÃ°+ýñ¦!&U‰…8d�§”Ñ&�@�Fä˜¼‹‰‚ Vo?JüàmšLã›µ$a®Óyáàá4¸÷ß$Y>íßIÂù²¿µwË&€-8õÁ›In[€+Nð•FóèAÈ�'÷†r2=ST�1pQ˜x¿'aìbZy1E£Ì»ó'¡Q’yÿ æH¦<î‰Ï_¨&r¿ªiƒ®/9yèdBP<ÂÓ/Â` ^ïüºóšÇ€êñ°œy.™Ã@Àø—6’'&)X€Fø½Úâç_ÕÞ[››øÇƒo®µÇ³«í¿nôÄæÎö6éÔV’U°VÓËX¹aÞ’èÝÈ°o)Ý»d-¨�Õ‹F*À²ªr�zøß'Óøp@Ü³m&\|—g†La_a'. S7cÃ`©‚Ø®šµ€Š
6}ä½‘\¯¨^‡Tì/d€·f$2ðTþI>GOÀèËù©üs†G×yw8Ä*§ó
•ö4JR™öyiŸeIÁå3Ž6o’%i Ù§SÏ8+ò/ì¨¡gŽò:`ð…ýÛÔ†W†®Ô–f·sã§â2	îÐ!™~F§ÅØ?¾ëv¨B'9�è�€‹¹·‡ižúíþ��#=}Fs U¾óÅsÇQâç;ÛËë}ž½ûJsu)Ô¨XT­º(º¾X£�‚LK»�©æ’ô×ïÈƒ¢]Ðs[œ�SæBuDE)¾´mâŒ`.Å¿û¯À�%qòéìA“àîIo6ËôŒõâjö7 là½—>ã�Q°ŸÄTg½¤(JöæO§Q8òó0‰UckV²0J×¬fmßlmªDø‰ŒZý%³ñô	ˆõ{6šZÈ´ÒÕm!]¸øA `‹“ßƒ™:¢,P‚:Wâ·øãÿQú‹5ÝÛ>yVåÜÛ›¯mÎM¯¨ïò°¹:¼@¹ÎRÉ†¬˜Õ/“w]dŒ°Â4~þð£™t•kQÊÁ GJ›£ÿTÈæñµc[Ío|{ŒùÕè®Öéh†¡çêD|Z«QG8�hÀ§WG³˜zÕùrœêë‚b%°j·�¢Žx68ÓZyn“RqI�Ùòy8†b\C×–ÇE†ÔesvÒè.&,$H´–¡Õf—ë7J£¥¶%4C~ÔlŒÞÅ}ºˆy(Ôµ[�Iˆ!¦‘„51f¹AlonÛ$�WíÀÚ¹Â±Çˆ|YÞ°
0›)K;:›³W•ßrŒ~ù!ê{–fÅk«Ì8%ØCn÷ûÙ§ãçJÀj¥ò=!%2kçS‹!½wÌFë6Ry
é—8m×nx|d}�Â•úÒ¸þøÄ©r-&Pç8¤ÿPFSÜÀP¢Þ.Ó÷Ðz/#ê²5­+Ñ:rŽ?þóÃÙððxxñ÷Ã]¼Úúuû7Êupí]â®t…Xþ4Ñ ™A|#ŠWx@KF'¤¤Y¼ü€{NëÛ[L{»Võ3M,[Jv/I";]ºÂaÊôÐà—Ö¶‡ì�Q¥¾OÀƒ¼J¹`³mRE&®ï&³[ñK™¹‰L¦hñR;Âñ�H“Ýz{ÕUéµ¦2R}'NeB·j×Þ´§†r
g´çÇÙ-5·ðJ—ÂoqÅÎåµ°æ©Pl±—Q?·P!ßX‡¬º�Êù"ýïdÅ÷M–T@j!§ðÞÎ–•¢‚èŠèEˆþ5´Šz‚î¶Ý9«—‡Ù4‰3ÉAô
Sñ3%ëç6†º½›ëŠÓÝX½æ,Û‡ô™µ6<õèÖ(Çs?Ÿe¾3H¾‘KêþÈ Ò¡þ�€Úÿ|í–óç fUÁ¦’º°ÌA/‘L5óö£$“üAÅ(
Q%“qñ0ýàq8ªiÈ… 4›öøËÌRó=ºhÃ‚óÓ#ø‚Sˆ[�Û¯ÐôITÀt<{³Fé�Ðâ“ˆzÓ†Ø2h�¯ª¾®Žˆ£PGL¸èçÔ¡©d;�µ7µÚ	©…aK*ôl#ñ9hfÕI*Öj^ë",M&k-oªÃ8ÌC ü�Ñ>€w;~œ¬×b«D»ÛÁ×3hw/”U+7‰zŽît;‚m•S†Wêé_éhÃûÅ^Ž“³4ú=¶Äº:1…ð°ßÅž×Œ·l1ä­k¼ºñóW˜ïb£ó,tQV¥�/ëo©ÀÐíÌøº¾n¦º;B ´´+c9où¾‹ìLÑ²÷ßu2Ð#Ìj¬w�®ûÞ-1»ªÔ^ÂÕø&c+7^»sÙa‰[Òû1C^„E¶fËf÷œÍ£sì &Të‚å€ãÒë´©7ÑO¿¯>Ñ0µnæ{fQ0C†bô
LZÌ¹ê¤ÊHæ¸³d´Ä,ÊúsË(úƒdƒúÔnO«;ÐÄIÖÓ]©#O@ýCå·E‹ z�&f{J°,„þ ³hÃ%À”!P}¬¥ä£¢à}÷ÿ	�%l”Cx óÿ�VÆU°n³ÅX&²Eªx* Õÿ100755 healthcheck.sh }ðR hõþ÷«Ÿ�<,viÃëO=Ñ<»_xûú#!/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"
"ÖÊ!¸¸xœ¥YmoÇþ,þŠõMx6y”\4må*…ËqZGV%¥i`ÖònInt¼½ìî‰RýÔPôæ—ô™Ù½RJ�¢2 ‘û2;;óÌ33ëZæWr©ÄZêj4šÍ„SöZçÊeK#~úçÄÂšÊO­i¼²b£­®–ba¬ð+%t5­­Áb'^”&¿r9–M­ë…@"	½Xi'ºÄ'æ�.‹©—K!—•q^ç‡B{šÉÍºÆ¢‚½/Þ^¼æc
µ�MéIÐøK3­¤××*s]I{+dUðªK‰SúR8h‰ÃÂŠL|å…ÙTË¬R$¦PtT©r�ÃrSåÊVn"*S)ab³ÒùJxÓàw¾4‡ñâ gÇçoßüýX°m|c+qïú…©z)Æù
¦tÙ­\—i¸ˆ„`»–¥þ§¹‹qÄ�UÎ”×ªˆb`b¿b•¬òJÔÆz(V[µÐ7
Ÿè¶öõWtâ/çoO2õ4çÇ_�|yÎº±•Ù0´F(‰»D·|êDS;o•\‹Ëó(êFÁ¼f5]«Ý˜¶(›­¼¯3YV„`hiëœ‡‚R/_ˆÆ–0�²JÈºÆ5å¼TiÐî·™8={û�ïH&i·6Måq« Ê)‚ÑÍ­0+ü£®ìÄóºÒ·ÊÌd­g
¶ø|öXüô¯Rìp6;xúûlÿÿD#ÈnŸ÷èƒF¹od9Dì›çßœ|ñþ" CR©çesóAÝÔ¥±Ê~ �2™,I§·'o¾µ´¬Œ_I/
½X@w1¿mmMˆ†%J Óá$‚âSQ ÍÂçÅ_2èƒí"Ðyª…¬#§Ì•ß(U‰dC+mS¹�â‰Ÿ5ÎO™)˜¬ÌFtÂØrªH(V&dË”-ƒ¬ÌÒT0í	ø÷*�ôš¬%Æ£½DU¹) Èì{gª‹µ§?•ò3²ëð3ÿj¼.ÛA¸Ÿ>X„§d„ÏK€¹™g8l¶’Õ�FêJFé�jNqúé˜"|o¦Ò9½¬˜:ŸÅâõÅÅiˆá<œÂ ;@\,æhÙÑ¢Ÿ<­&byvúEûíÉÁD8C†¿a)K]„ø#i0,L¢’ÜÔÂ1J�¯�WÖŒÿ°ÿÇ}Cd«”üÌdÓÅÚ¼çÈ¨æ´”M•ÓqÙ`Ÿ‘¼�¿Ûc²r*Ç4r Æxé3±QâJ©:€q!×º$ z"=`Ñˆ.$Ä¸4¦žƒè`X4´ QÉ="tÞiæ`á»�¹
0[ÈJ`¥¨±6êŸ�ÀTÎßóä‘ åG�jùb§µH3µƒ0ñêÕW�«*¹V¤°†ÇZ3|sö&’›1~&yQËZzrör­p8?}Ž0ËK�'VÚîòE&þªjèº"´ãBAO$EPyNQhÈ²„&¦úÔ‹|ETVÌºAa¨`i]t¾ÍF×ÒîÜõˆnû.„Ãûðç#Å‡^ž¯6›ŠÁM%Z²¦§0­·à­)à©šW'Ýg^ç¥wÛ2:Q<E‹ÖÐHsRšÊåÒª¥ô&<˜â•×Ú5LÿvWæpj2ºcÇî&.XE¶É0æBŽŠ®[$Š\KDX›ÜC“¬6ÖÛLKZ°h1é²ûVÆã‚|mokƒ,‹³Ê2¨•+l²„_Š[ò„©"/LŒá�lJy ·òIZÆ0A¢Ûa“ªæP”|mÐž£ø��Ù�_ž}‡LçokuÏ.ð|“{×ŸÐÛŸ ˆÑ^Ëö âv£½/A^Û#mâfƒÄ�[ži“¾úp»Å
—']±±£y´¦±…ËÄKí(R
"Cµ®ýí4 y@C°mÖÔ5Yú”™yÀâ8ˆ¤[Rý‘0="n}�98Ô*–ÑUE²¸tl©‰s$Ç­i,—QÖ…[çÛ¾½ïUœGxàC³ÑC»fç‹åŽ}Rñîý®;áG½�°8;®‚I0´STºíÝ�öˆ'‰ ®ÔøžŒ‰ØŸˆRUtbÖž˜¦£½JÝð¶£Œö¨àþ€ŒE“VV¨×‡[Y(å2FØÑ‘HÛ#
ÑU	¤TXÓjýèˆ´Ÿ|"=v[wÙÝF)ˆ±Gªe-61A™¨Ÿh!ê6A�ý ¶tÓ^©NN¿¶ê%=qð¼¸–¨ÛÙÍi%Ò+qrR•NœØ9z[êŽ!�2Ü1´�ÍÞTqÝÒÂ(Ç) ÙÎµ>—Úò§-MŸFM#Ÿ±�B¨9q|à+œsEk·É»à×÷Ïh’—¶R�DM_ï„*�Ú�
»x>êB <¢Ú”8Æ—ÉnÄ³ÚÕÓ�2¡‰ ÍáU¹ìÂêõ8:É,Iym’°ºµOµ0	S­ŸyjHi‡¡ª�Ñÿt¢º�|à]Êa£w‹TxOFÌ®ïŒBvù…Æ(T1}o”‰cNiK¦,†eAË5S*3¦!·5òq$çr·Ÿºý`×Q]Ò®Z=CS5¥º­?¬ZRÝ
^aq…Dƒ³2ß—™øV…ò/äÜV±‡‹Á4ÀÔ"t}ÃN-Èõ"œ�Â×8"äˆ•©«UŽR$‡YDˆcr%Q[U@Ö’sÄZÙ%®SRˆ¶øFW‘’pù¶°‡ @Y,�x¤ XæíJ/YÝ°“`e”6÷çöînœ¢®ˆ¹ÓH6!öòS(“Ð#eç5öøÅ8éÑßI¯mˆŠ;þ��â?sŒGm
Õ¥·NÄƒ¹¬GoÜzîÙýÚ�Õ
 H)™b°MÜdtˆÈÖÚ*ø´§Äi¡ˆ03	¤È2Ð\|�&3 I“«Ö*ôf€óºD/–ýß¶Ù
à`þ…ãÿÖhÊz4m³ìÀZ¹ë›.CåK×Üxcž‘®‘<]$xÓ5TŸí¶ßv6¦)‹®çõR)×ÅXì)q“xäÏ€¦¿üanÿ?^þÉAÚ^ünôËÓÔ=êò÷ËrŠR®×sÌUþD<Xâ§¡à2DS§p?”�0Ÿg³õT^›BÙMžrwh ¾´9U.¸+ÁA`[¿KZùÉ{ñPÔí%ô,°ÝNl]1(Òõ‡NØ N‰mªk½´’ŠnnwÚ¹;6GWqÝG®çœ�¬å)wÍ«çº¢µôÆ‘}--¸¶¯S¶>MÅÂ(}ûJTqÆÌ7ƒ_ÝlnšÅð„°�Ÿ¸uú,4è2zO.á¤d°:zD¿€ò[r•a¥[à5uÖ”ÉÇ»d˜ÚÂáãy?¤EŠ<�� P„X÷ëØÈ~ôÚ1lêxt¹×Oÿê÷·o¹	Ò5Ÿ½%$¾Æše® Š"}6ÒÒ3T›¡Ú¤<x4Â_5x	 n»ABBÛ1VÙ2Ç\Ìä\Ï²,£–‚vó0:’�é4#›dÏYûåùy‰Ò{l;|Qê9moŸ¼²³`ÖÓhU~ä%«2‡·aîWè‘Íù˜Bù¥fcì½è†“£ÔâÂ ®6W„çé¨ÐÍÐ»´P.GE öõùªµë×d<øêZKªê2þ	®ÇÀåó²ŒÚ“dö8I/ùI#òqðyX
I/Ž_½=;f­r:‰ŠêîÉ4sCÂ*¤éW à‡ëˆWzNobü@SMC·&	to6Š»o†µK\·Äz„:
ü=|w‹…Â°Ãâ1»±F!í®‘)îµ^)…õýnŠSÀ#þ–Šû™Oˆ/ÁOÏQÐQEJ„‰xmj×_âö¾GJ©ìf]O»> a;$è�­iªƒ×‰Úœs¯K‡±6
¦�RÎMTíÄPzk3›*ˆ	ÿ¡Â�”‰Ÿ=úÓþuî%Šˆœ:-®�êr-ÎYa?N¢%R¡x€­ÚXÊ8mÀÃÑ³ê¬ˆQf
*«©ˆë-Ç6êÖ�Í	c«~�¹–>S?4ˆ´4tA$š&ÉN{lÚa»$Ò<98;…>Ñž›¶½Ø°7ƒr\ÒvS{Ã½mFcd4|^âo±÷êâs<¸°š—»Ñ.T üä–fxœë0o0ß°–‰©Ìpó&]ý€Í—HO6sQ¥bÕ ÑÏb•Œ :_‚DO.—qOKîe ð�œ®xn ‘ÿ100644 icon.svg ^pý”`9«ƒ8(í=ëÙü0¸100644 index.html Ì|£}GÊRüHXóªøðyž¹DRe100644 logo.svg ¿ÛÃž—ìÿ6Ñ•£„µ_|nÉ%€h /?ì{x ãÿnn�6?z%.JÉ`½b<_o×�É’ó‘J$“ˆ'ì*x ãÿnn�6Ñ\œt¯)Á^>çy€¯i©¯xåú‘J$Á=×g*x øÿnH�+‘Q
§q¶
x¦ Yÿ<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="12" fill="#0b1220"/><path d="M16 44 L32 16 L48 44 Z" fill="#5cf"/></svg>
2.³xó ÿ<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 64"><rect width="200" height="64" fill="#0b1220"/><text x="20" y="42" fill="#5cf" font-family="ui-sans-serif,system-ui,sans-serif" font-weight="700" font-size="28">Explorer</text></svg>
iQL¸­xµ[ÛrÛH’}¦¾¢š¶A›íîèØ]µ5¶$»5cÉjQ½~p8l�(’� �u�"öi#öuc?a¿¬¿dÏÉªÂ…¤.=³Ó"P¨ÊÊ:™y2+½ˆÆçÑT«y”d[[É|‘¥
¶:Ýqž•úªìâÏ8*£QdôÀ|Mù[gã<N²éà/&Ïø`2—qi>å¯L—ƒYY.øwnøÿETÎ“$ÕüƒLYà{yg®³1•É\w·¶:ŸUwš”³å(çóÁ<*Ël0Í·±tRê8¢ù>]^M’Á´ˆ³�Î¦I†9:ò3‰¯Z35G&Y¬¯tá‡š2/nË—PÐæ…ÝD}1ç‚Ûåª÷W‹4/dEÈuËz~pµbokk0P{3Íp¹ÐÅEb h~™UÎ´Â“í1_*÷¥z¦dëjšù²„2ŒZ¤KÍ™øP«818†ñL˜%*qò£/~ýo&]NoOU”ÅòÂª×?–™ô×¥6%fÎE”q^z\ª>IuªS.eTT YK 'Õ
KËŒÇ¼£¢˜øQç²[€LÜW‚9/óâ\¦¯
=Ï/dœ4Ž²±Ne?óp«¼^è5í YKòm«CÔî'…²XÛêÌ—Jþ#àÂ#ˆwµÕ1e�(êà£÷é©È3ä`ÍéÄmÇ~-Ä–O ¨O„zø³Ý¸Ãž®Ô¦pJÝAÿ7Ë‘hve†XO¢eZ17$³Rá,i (µyVOU�åŠ¦…ž$WÀL‡G­£”ªþy9R‰…‹v|bÔiý6Tf:ÃÔ°ú‹$’�_•e4žùA½¾±©x™CcLƒÿ~äqøÄÈ4ÃåÈŒ‹d<¼Ï^y�#Sªq”¦#xer5Jsü¡/t†sŸ¤ù%¦p�‚�Y~ûÏÿV ‘öA�†QŸ©áð€
t˜P'©ÚUPFš\h5òË…[�¦.žú]A1[7[Cõ‰SÝ>ã	Õoñµ—g“dºÕ±øSÎ5†{Ç7Ël¼Õ¹”ÑöÀ?DIùÆ¶à
8Œc}¹·bÄc%‚MT–]äy©cŠÃn¸5Áä¾Úàî©§«À 
].‹L=^y…7Þ6vüB}<[ØÁ–çÑ¹6[DoÜ®Ú(QµOjmuª5©˜Æa„j¸Àîs˜¸šùr$Y8ÍU4)á«Fy9["ŒŽÓÐzšàð®�n Gé+ü
!HW•vâ)�›	à‰D-j¤�ºgI2h Me¶û -Ë¡)!–¯Ý)fízª­¤`¦š ìÑM™°‰Ï]5sê=Êá,�#q¢Y;n˜ÛC7<»�áx¯Bu6ÓÜ\íô�Jóü\-Ûun&H½S�5*¡P@^Râ„É9›èÞ=Ž-\�_‡2³ypâó/Éi¼…Þžs?pæwi«±ç Ò«§â[‡t{GË+Q7e�mÐmhbÐí«¦#¦Q&ôÛ>´¿×chOàyÏÎ{¾ÓÓh|½i"%ï–ã-‰
>¶~a¨Ëý†³_$ãsD¿Y¿êÐj>d™Yÿ…7·ýÔÎ]Jm¯H¬°QÅáo¾ßÁ­P4ÖhWŸbÈ¯}5YÙa+ ŠÎœ\ÄVÐøÍPÆL²„ÝíÀ¥b¤DñLE)°_o0Q±=€	¾{;_Üµ™) cÛµ^y}ÉD}î+`|ñQ©?R*FÔO?ñ]`{Sç-éÞ:ãòª�0E®Á9¼»ÿ 0[—øG¯Ç¦0�,	D7Œ,‡7äºß°îUâçÃ™«¬‰†ðeJPŽ‰èÐ!C=~ŒM´NeWu»n­çò7)›hï�²…—ÓðU/ )Ü*CK Úë`xX;b?Ï4¡ ù¼“¬B&S<¾Á»JF�sx�ÑÖc‰€òÍ”Š(¢™UoÂ«Š‘Ê�~g¦­Ï;j6É–ð( †Le°)†¨æ:+»@XÛ´·¿xµJÉ}Ê–d1Ù@Àîœ€K�àKt8Gì�Â£�¡ƒ)Ç”Ü„H¾»ùâÛ+WAË9?‰²¿»´±Nu©áóðØ>Æö ä5øðÕF u‘Ä2›ö.Ð±¨!g@Š#H0a‹"‡ûN»º	åÀmO‰‡@pwÞWŸ¡ÚëâzžŠ0�ô\_{œ‡�sRb±´›3Âû›¡º�çfž�P[$µÍ�4ç<g¢ÉQ"�< ìO‘²†'ÈEËIÐýX½OX-_, êGÝêÒ:>îÜòV§\‚uÂ:È±cŽPå¬Âˆmqí|yGôy�$ø‰²Ë÷62	Ñ8—ò¾gÏ&è²ï—ÛxzÓÓ¯\:ôch�Bë>~²jì«ç}•êÌã…Þ‹ø§iíÖ!ÉE&ÙUt_G} ?£Z×à"ÓÐ`Líä‡9ªV¦�Ï­V½«aŠHò·A¾òáàE¹X ä	¸�Ý¿wÔÔ/eFÕ
×°l	Ö�ÅUƒÚ$\½:9´4“�uøË;”AB<�xŽ¡î:	/+=‰—"t§?¿!<É^˜‹â|$üSžPýŽÉË‡¢ ˜‚.dtnÂ£ó8)^¥iàgÁÙýË�?ö~’1ß!çÑZX~d>©9¿ÜQ�. azá!Ž¤Ï¯°D3Þ±à èº%]�"DÖòk–LþÑ¾%î1¶Œ¬¨‹r‚ß%C=åbyoáÂ½47–jÕˆÔáaG„ƒx€:ÜÊ@TZr¯VâÑ	ªUëGVC™EqåvHÃxÔ…ÖB^3P<2*xdzjûÖ±Ô§Â¿Ž£9Ž�íÃëù(OûÊÊÁ¨‹ Ó&�>´|@n„³z ï&‰ÄLg,’ H½5xde­j Xäˆe äÏÕSÅZA8}GFÕá1ò[·Jx‚‘‡¨Qªp¶Ž~pX\ÁFf8‰
£÷—R£<#SÛ8‡;«ÝºNš]ÅÕÅSÓ[vÕô�šˆt"üiñGäwöè,¨>æ¯"»è²zu¸//ì+|¸Ïœ·sz²w�Å¨��ñDÞü|vvr‚b¨ýð¹<k*bGQ�òT¯(_“sps×õ¼�W¤©",e6Ø¤w·áµ2cQ\ß»Võu�$ûí?þ‰ê<íÜõ:ƒ¤Ï§øô‚aŽÓº<¤£õJ‹‚�l6Åç”p]ØÏá¦çäKâK·ËQš˜BiU`‚ÎÅ˜àål²*p^gj1¬–èëW
@"¨]\j_¬<£;—_Ÿ�ÑxÒmdGTG ÕèZÖKD )eº-ÀÝà-bº6Ùä9PB$•Lh¾àˆ¿( E›H¬'n;­w,˜ëêøwÃWv|Ì$„è¬1U‡ª€•¶æãóÕ‰Z56„²{)¬‰ðQŸÑÆò.±ÎV¥¹z¼ßð=N´Ö&YE&®»ó½Ì·{Þ6’¡Á:…§K8“Ue’ô´Øô%kî—v„ }›ÁÕ Ô°æJ·¸ö5|¶ƒ7L>2†Æ¢«@wÛ,ôAr¬pu’ê92^ñ}½VÈåyò¨Ê6Uó)ÇŠWodÄX]Uk³æº,SÇ;I}¼¹ÝÍ~pgÀyÝ¾3ª+=w aÄƒ|s4^Xk´H,“'ÀÛÕnm1j)n38LÂÌq~ô$—ýÑÇ®#$�%LKl$"ö"ÀÆ¾¦ÞyÎ5Þ$4¾Ü–™_±Ö4¢a�c%IUÁ�´®½\È]£bô MIeV¿—©¬#ú�ƒ'Sà6™5ÈÚ%*Š]¹N7P�ÝH3Åvóø¤!¸ÿío¢|q|û±‡N<ª2.äÂ÷È‚®¿š· ¹Ýé>³R>ëþqžÇz·ÈIŠ:>ä5MµsŠ÷W¨{à¿Ï(�Ä£ðÒªSœqwxðî`ïLí½ÿõø,xÚSoNß)»Þç9.PµøðóÁé�*a]»Oä¢é‰zu¼¯2��Ý'�ü³D)ó¤Û‡ÈbƒÇ™SÜ¨&•V¸ŒÛ.à±î=ÁÞbõk	ÁJÀzÅï9<w|í‰`³rÏ¦¸¨È5iÀê² )¦Aõ²A¢·ÚMÞ‹«á¿BpcrJÀÑ$QòwÅ¾äuM¿@­,“•¯‰´¹å©ƒÄZ/`§¯œ‚0&jÊÅ‚f ü{ÍkÍž`Ï¿ë(h7«v´Z	’ê	¶yÎÆ_J±p³ý‹oÇÀ%Ð#3xÚ MÙ·ó—Ù–2˜€W´/qù²lä®ƒ�›‡OµWä¶’¥üîe�«Ê±ÒÕŽ $qîj€ò°î,Å•Á7û@QnßüÛ´¹›Á]‘QØùÀ¨�òŸÿBü&2Z†�{³h8#‘
D�‘p„ÒjVÑÌ«1åSêÁíYµ[¤òÌõñ jÙL¯E*>¢]X»–«�ð"ó£M™óKÿÿ¦ÐM)à³7p¹ÛE�òÑ��““ý“<bûRQ·U„êqí��‰y�ÓJÍNP1<Š2¨Å:”Æƒ>ïÉq÷P^«Ý?x~£½'ÊÕó«Ã0 :7Ã*œÆù?Fv—ÎÁ5r6Ìü>ÓÛf–—0¤×4¤{*Ÿ¨³ü¹J‘_¢ß�	“$
UiŒ %»S£e’¢˜Œk²/¹ÝÆîÒÙ¦�õÌŒdþŒFX«(Q³/�¶¼_”Ûàm¸¤W¯_íýùÍá»wŸÏÞÿùàx¸ûâ'©›ÉŒ#ê (33¹û'©@F–Aø"ó—d/05Ô:¶[¸,PÃeúÞLG0lù·ÿúß”.gŸy×)ûk	[rë}Þ»ãŽOÚx]Eã±íÿ°ª‚BòL&œ$îIL2�ñ^pè� Ær'i„B:M6ú%íì"è®lÅ^ò’–•`($h¤¼•�ƒHeOf=s�Ôg“…‰ÔŠÍ	üœÁB^üÝGq›Ô7±Ð'ß(t§H¹e�ˆí¬Îß˜»åè«©í¬RemßØÔžŒ¦ôåG"™kñ±’ÄJ>{„HBk¶E\™ss�Ý£ÚŽ�¢+atûzA^¡^H1OOµa£KòW–m^¨—/Õ÷òJFŸ¡"…Äÿø‡çø�¶»:-–†hC)Ú
XÜsÂÂ]ŽÙ‚.~Gs6gAòZi�wžQü‰ë¦À~}oÑ:@;ñ£¯·+ËN·!luì-®„°yîî¡Ž­ˆúrç@�äˆC¿x'Ž�ËE7Y�»çµÌôkêz�dnfÿ—v ´¼È3£?Ð¶é
w­~j{¸l6R„¯ó>Ó~‚³y}G€B¼Wp‰�ä}g÷òå÷Ï‰u2÷Bõp³áE­e¶û¨+r;IwDü<Æçë*ïÈîŠ"—…¿|ëzyaº;¿uçðlÆÛAÅU›$V¾?îÞ|ºùâîÿ™–-Íë(öÛƒDWž»pÐ®ôi#$\Ùî ¸%rá³Ëðg»dºº2$+·™Á\»¸IÑÂR§í„¤ZüžØ#IÝõpÁ)Ûåº‘ñ9®¿K4{;Ma•DØtÐº¾q(Ä9CÝV:¨8?ïö»’=uwÅ}WäŽùƒêÆeì;ö0ÙÊ§ü¶4>�~.»×ºÝBZ2º'ï‡gªûÌšîí$2¢­´¯jãËÕÏÞÜò-Ùjò-{+’_ÞmøzuÑß½ž|ð ¥VEMÑòJí—îÌYÌo_T[Éï3ŸU®·Ù×Â¶^´lŠ^–d´ˆoN¿ý¯ißÊrƒJ©x°]\Dt™d­yš¾ë%„Ô2’â6Ë'.ãXi®±�(íöDß3*·èí¾•À‘2¦’NôÈÎÚÕ®R´“‚ÇA}Ò†jíJõ)Äœã€un)åpË¤hw¸g-Õe�›ú�ºn9»‘Q@›
�C;6k8•#ä6¤'ú
´­$s:~†°í3Ó¬‹Ô !*×_Ø¬¶½€oBaXî ¿4·[½ØjÄ¸ïa»<šM­¸VÎ'…Š|¤º¬w}FFy¼²¡üDa</.A�„V¥×"òéÁÑ«ÃãÃã·~OÂw1š{rÚ¶B³ÒŽ„Ží�˜ànI±�Rš¾ZÃPÖð[ÁjèÏ*¶Meó¢!òµ‚ÅÉ½%äÉ¸8–{J#8j¼¤sd�ã�pa@“Iïã³ôYàÙxi]¢§¸ˆ®Á‡ÏìÆ†'¯pðå�¡�3ÖÁóþ¿ö¾øÛ…»Ñx(a}•zÍktYñZ(ÞòS¤{5Vò–¡œ`³ÝÜÎî	t$åu:þ®‡ß›äª�¯ïÁb{Ó�È‘ƒÛ3Á;›·ÿ‚ÂÏÜ§x†îâ<û9¦.®líÕÚÐd�O3&ÌípdÂ™Ê j€ìËÊ'œ‰¸Œ£?Ém®ƒ;Ú<Þ\Rô{È±Ë-šº¡ÅC�ö×Ów!L Ma‰Ì¡nt‘íÛº¼0| uÄFf�0Ï„ýWŒè«2<Ûhð“Dæ?ƒ¦Gep~yÝ'Ë@ŽŠ/ VO–È#"Ü°s~lL¤äÞ¡Õ;uÛà-'*?Ü°ÿUÕQG1Ü†½y³ýèŽÖ?Ëà¼ßtgË“˜‰‰0ðøÞÔX
Y`°Îc@»dÿ¦ä�Ä'Ã‰q‰$§K˜8QÅÑë@½«òÓVÀƒ˜©ÅPóÂ ÕØ‚íÚCwûÊé´ÛÞ)Ž«Ó‚Íìl¬›Èj¯»B)�óò�¶‚àÏwºÊ5;	ø0c×·.oƒ‰dç �×««£po¿fÑÀÈ~»9Šï©ðX¶º4ù,ÆßWV†ß6;€•
ò�õMVgE2·UÙÀ2;¬¶î³�ï=–½÷8huÍVŽÊ^NÙM'à«Ñ¨%âùOO,?$úx2ND~…¨5‹@/q';OÀm!€Í¦'¹…›åú§9·j_+ÞMPàµÉë<Ì«Y�iÝŸx6þK”»¦ý@"ÿOB‰0÷&}ÒêÁPÕ—‰G…¬±†åŠd²Èï9iqòôú®ï°i•=¹(fv§,ÂãÔ±Nî”î=tFnéÔó+6�kì«¬7aãžŒî)ÔL6;š%é!ä³�Ï?Uo¼øã®]™ì§éÈðµ]Úè{wso˜u}äž ê��Z×`½¡`Î²0%’hò©:fcÿmˆü;²óÿ2Î9ÇÊÛ®œñÔ­‘ŸïN"–ê*–ÄfÖGéu`ö•ôšú†CˆÀ1 �£©x<Ž„´Æº®*l	i…2Q3Û^¥ôBÒã¼nmmNå*4ö_óHòI_ª	¥g'ë8mÍ"ÞB×ÑÐêÄŠäª9émXä÷Ä<ÃÙcã)�Ð¡¶”…GL¢†"ˆ(�v�øØÎí…àÙIæájås}»Õ?mªßz£—½½7[ÿ!ý ½êˆËMxœº Eÿ’	’	�ìJ-ÒI#e<ÜB=Gú{cÂ¾í’dw�Ÿ¦õÀ�½ gË³;�Ü·€­“x82r
pFª©B›¶S-rìÄÖgKÁ·40000 ffi 1è«„¥£´~fl¡�¥ŽFÅ%)“â[…ê>¯'“8Ñº“#“�/ÿ~z™“Q¦vAñ±nº÷+¶Àj3vF §³)ºBZÑ€è•D5�|G.¨Æ‹“HJY+NIî‡’5xœëgìgœ+2“ÃyIv��8gŸÈî«¦{f	¥Ù^™X(  þ)ìˆÍCxœÌ 3ÿ’	º�4¬ž'ê	‰×Ã€Ê!6ÐÂœã�¶>‘H¤½`ŸÚîþÙ(z52æv#ºècæ“$@"ä¢Üà§��JáÒ±oëËì`�?3“x8àQ—&A,|”VaŸqSa Fû©DE“QaY�ÒË¿Úœ·oíw7ì×õg\'100644 go.sum Ÿ(8íåº-o,§l8x¯’fÒ100644 main.go ßGª/× Ïsý´÷ÛÜ�®Ìµ³“HJ[­[Èç€\xÇ 8ÿºÇ�4J¯žË³î³Š&^ ÐëBçÀ	‘H¿Makefile :t\CýèíUxô5Œ�‹¦‰fé’ i40000 ffi VøÐ`{ÜÉdÌGÅub8Fæ£100644 ffi_off.go E�H²Uy1ÀƒÐ	²NU4ÿ100644 ffi_on.go …“…Þ?\vHª…Û:È·8.”€À“ ¦òê~ô|Y_½V³unÊ'˜Ó¼¾Q³Z`#ÁTÁ·Êx§Xó# explorer

**Org:** luxfi  Â·  **Ecosystem:** lux  Â·  **Path:** `/Users/a/work/lux/luxfi/explorer`
**Origin:** https://github.com/luxfi/explorer.git

## Discovery

This file (`CLAUDE.md`) is the canonical agent-facing readme; `LLM.md` is a symlink to it. Update either name and both stay in sync.

## Where to look first

- `README.md` â€” human-facing overview (if present)
- `package.json` / `Cargo.toml` / `pyproject.toml` / `go.mod` â€” language & deps
- `.github/workflows/` â€” CI surface
- `docs/` â€” extended docs (if present)

## Sibling repos

See the org-level `LLM.md` at `/Users/a/work/lux/luxfi/LLM.md` for the full inventory of sibling repos and inter-repo dependencies.

## Unified explorer bundle (2026-06-05)

luxfi/explorer + luxfi/graph + luxfi/explore are rolled into ONE Deployment
per env. Architecture:

- **luxfi/explorer (this repo)** â€” the Go binary `cmd: explorer`. Imports
  `github.com/luxfi/indexer` and `github.com/luxfi/graph` as libraries.
  Serves on `:8090`:
    /v1/indexer/{slug}/*          â€” Blockscout-compatible JSON API
    /v1/graph/{slug}/{subgraph}/  â€” GraphQL per-chain per-subgraph
    /v1/explorer/admin/*          â€” Runtime chain registry CRUD
    /v1/explorer/realtime         â€” WebSocket + SSE live feed
    /                             â€” Embedded SPA shim (fallback only)
  Image: `ghcr.io/luxfi/explorer:1.2.18`

- **luxfi/explore** â€” the Next.js SSR app (Blockscout fork). Serves the
  rich SPA at `:3000`. Runs as a **sidecar container in the same Pod**;
  shares the network namespace with the explorer Go binary so it can hit
  `http://localhost:8090` for data if needed (currently it uses the
  public `api-explore.<env>.network` host).
  Image: `ghcr.io/luxfi/explore@sha256:ddb15bc28ff38c7619bc08df9337e02b32ef886c33b9993a9b9cb3c475ad75de`

- **luxfi/graph (formerly graphd)** â€” NOT a separate Deployment. Linked
  into the explorer binary as a library. Per-chain GraphQL engines are
  spawned by the supervisor when `chains[].graph.enabled: true` in the
  chains.yaml ConfigMap.

The legacy `lux-indexer` Deployment (same Go `indexerd` binary, only
indexed EVM) has been **deleted**. Its routes are served by the unified
explorer Deployment.

Per env: 1 Pod, 2 containers, 1 Service exposing `:80 / :8090 / :3000`.

Manifests:
  ~/work/lux/universe/k8s/lux-mainnet/explorer.yaml
  ~/work/lux/universe/k8s/lux-mainnet/explore-ingress.yaml
  ~/work/lux/universe/k8s/lux-testnet/explorer.yaml
  ~/work/lux/universe/k8s/lux-testnet/explore-ingress.yaml
  ~/work/lux/universe/k8s/lux-devnet/explorer.yaml
  ~/work/lux/universe/k8s/lux-devnet/explore-ingress.yaml

Public URLs (all 200 OK, verified 2026-06-05):
  https://explore.lux.network              (Next.js SPA)
  https://api-explore.lux.network/v1/indexer/cchain/blocks
  https://api-explore.lux.network/v1/graph/cchain/amm/graphql
  https://explore.lux-test.network         (Next.js SPA)
  https://api-explore.lux-test.network/v1/indexer/cchain/blocks
  https://api-explore.lux-test.network/v1/graph/cchain/amm/graphql
  https://explore.lux-dev.network          (Next.js SPA)
  https://api-explore.lux-dev.network/v1/indexer/cchain/blocks
  https://api-explore.lux-dev.network/v1/graph/cchain/amm/graphql
*ãLr²®x•X}o·ÿ[úO…f�7é”-E�ºÈ Çv[NàEN;ÀðdêŽ’8óŽ’'[5üÝ÷{Hž¤“ÝŸŽÏûŸÕ"¿KI¥PU¿¯ÊÚXOÃ~o°(ý ãø¯•KùXúøº4õý2SÕd#J�­ßúGýþdB§¦Z¨%)G~%É›z¬åZj�6VZÊBc…W¦"mD!ZXSR¾‚p—†}¿©eËÌyÛäžžú½3áÅ™²>8VÕ2~'ºcºãAŒY¡ìà®ßûåúúê¤("ú«Ø+ïë™ 
£O=tÊûWÑ]Àˆì	Æšµ´Zlòha,M¯N~$YÖ~CïñœË‚�3•Þô{¬¨Š3¹�ö^ZÍ[ÕçŒ1+"
+ôIúcï]@¼¹M¯á-Y[%Æ>îKLonÃkŠFÂŽîs$HU•t$ÈÁ§ZÆ`P­Gµ´ã@Jyã¼)Õï!v]¯$‡\%ò ¹¡à´Í¤,*Gð
b-´3Ô8ðõ+d„[‰ZþH%uáh.7¦*hª›åä“(å$¤ëtq6ù|u:¹F
LZ�	+ÉÔœ3BüD¢®õ†V²
™„�˜QÌ+$•H	©Zf)­ö‡Ê)·X‘6$)úwÿv¦:8@-ˆbž…3Ž4oA]ª
�CªpÖÆêâ,ªÊÿò7Ê
vÌT)£¬í(Ù)¯Ëãs(/œÝqªÊu9¢B,G¤haGäŒ•Ñ\ùÜ¨jG¹Ò¸~¾o%P×$[ç;	É|½~›niˆÜM"Â{b§›rn4Ð°B;ªä>Õ•1ú#4_"ÁºêÕ€ÌÊ$I�³à‰³ó’“ÞkYÊÊSmenÊÙAÃ·�?às´½Ã'Uò‚Ò­ì÷Î+1×¸Ôü™C)r2B’ÚQçöj·‰ü’0ñî¶g œšÆæ)æ]{]€´~ŠÇƒ`d¬²#’ÕzD|#Gñš¢t_$“Òs
WàÊ»ÖŒdñ÷Ü‡RÖzò% ÊýlE½‚Iá¹eµõÉ’Áû¬(…å VmMüsû…=…Oro¨Ž¯±:€Õµ¹—(BŸ?Iá‹"Ô²òÜá�÷Ð–ÊC_qá·ª ÏäT³•X+$4‚nMeç�	‚™JÏ•Ñú¢òÒ®QÜÚG•j€f(6 X$:‡{vÄ›ž-3¼{Ë-ÍúÚä÷pd+Í]dŒ^fýlÎ0Î Tév‡ûlSÃè¹mäpÂ®C”\’kæ!Ä.ÙÞ%ÝY¾»Né2…ˆGÛË³Õ®=àKÑò§›Ûö{ÛG·²[wm^±iK\š¦ò{�KuóšíÉW²ÔàîXî-“õ_'ÁÀÉS¨ÎcîÏ“§VÔ˜ÉŸ'M#!“T˜ˆÐiÞ(íÇª"Æaf�+:«)h¾¡%ðšy†¢4ÑÍãB%9²Z¢pg—˜>0ÐE‰¢ÞTÊ=ˆz¼þÛ(2K¯ïö@ß¡ÜË¼±Ê+‰R°XI”ŸgG„©EP-ü
³¾eÁ¨¯z¿]n]´Û'hÎ‘êæ*ÛªQL¦ðÊ±Š.8@ŽzÊ¼€Äõ½M‰¶º$CDß¥BœebÍHSç.‡0/•Ò#úrA¡VðÐèVæ¡boó”Š±-£äóäÒ,MÐ�˜9¬i<-”»Ç,‡N*íç_é<º(¼VH·ÊEE
fZÒâhÏì¬ä˜,˜åÍU%ì&ÝŒ¨ùK×þ÷2(¹�}ÜÉêÐTþtÃÓmªÑ�¡óïJ=Ü’çh‹§Óú½�YÎ öjÅ³€ÝUƒÝù7Õj(@f<�í¥
ºV¿ÇÎ�Dò*	Kùòù2ØÛ56Hi¬B’³øì‹Õ]EYêë,‚Ô|vÈ"fàA¯�(o�v˜PUÎ—+t¤”m´RË•Æ¸yÅ�$¥Ã›]^¤.†Éãæ¶kê"ö·02Æ¨¶']KO´6—Ò_p™i€¶qmq»,B+y•Eh$á„q;,¢³ÚUÕG¢,n÷ÃÖE|™0ªÞs]B­SpUÚvpÇ0²¢–&—µÌv¾ºs¬œ‡ù¯ùt/éâ;ïÌðÚ{ˆ^„SÆ�NMïÀ?�K
ÔŠ“{¬PÛ)=á‡÷‹™mæq%<:y…‚À!ÌaSÀ"†aI2Ø^¤Í…“Ø�ê•¨šR�þB«MÍ«Ê´‰E%híÄB²ƒ8‡-êêÖ�»SLÐ’Bg:
K_;·qìBÍGÒ­®ùžfï¡/ùÙGìz§q0ÞýëFŒ;þá6=Ç·OoGß¿{þö.nýÜ•NãæÏ3™wytšÇ›£oÏ?ý:ûõä3«ËÞoSL¥Xå0õ(ìíÐ<ø!ë/š*§ÏaèO1é�h�­µÆñÏ¼ô‡w:~OÆeŸ¡W’@xÔï©#Ó7ï©Rš	zVb$¬ÒOÏh‰¥ÏÎ™ábˆŸ9Póß¸czó0Gæ`ôÜï±ÃòÅ2‘nyC0§Aö¥*…Å¢«‡Q©?—ŠÿC|òåóïŠíÄ¯øÁì³��.Øµ{½Q·orÁ!ç!ÃÐ{ëgé±&Àº^Ž•î�‰9vZøø›½DÊ>
ÜÑiø‰æ@îéÂö?Â®ÆÍ›âö	r$ä½ùŠ¨(ì«[ÓÙÖÀ6 Dø—â”ÇWð?ù= î‡¿3xœëgìgœ+²eõã§…wÛþÜÕ‹±“�ÓìZÌn¼eb¡ ØfXá…èAxœ-Ž1‚@Dké­lÆJMDï`”Dc4‘Øó�/l”]²±•;èA<†G°ö$.	Í“É{óÝ´áû3­WaJÙ•sÛ»ÊX0GNæx.ÇÃÇtÍ~áJV99JIçÓ>Pºk‘}Qvññ Ò9*UXrÊh�m4~í5‰ø-9Ö4E‰Ä{¬‹¢mÜ›“‰5Û°ÒQ¶¢züöG?1è…¶"xœ;Ì~ŽuÃ<&=®´Ò¼d…â’Ä¢77ÏàÔ¢²ÌäÔb�x…ÜÄ‚èâ’¢Ì¼ôX¥©P]Ë I0á…²7xœE�ÏJÃ@ÆA<˜h[ÿAA,ŽÕJ#iJ/
­HÁ‚‚Kk²›.6›°»)}÷¼ê³x|ßÀIZíià›ù~3ó}î}•?vWLmGè.‰Œ}ªÜ¤J}HÆ±¤•Žº~,/o¯¯ìfîéTAæL¨lÌ‡pXk.Bu%z
�.¤‰Ò’’ÊH:Öêµ°]É LÆD„‹ºDƒÒDê4ÁCL«ð¶t›ïÂÑób'Ž&D!~8+<ÚVnƒvX*üº ‘w]`b&÷<D¤=��»,Ÿ…™i�dÖ!v&D‚�ÈlkèC�3@ƒù)VŠXT,üÃ1ýÒ�9,m™»â¦y)Y�.ÔðûZàT]È�æ½´_žŸY¯*f1Ox@eÕ5­Õsn¯�ˆ�œ©ÿ°�HüÃ'c@œjãMHd¡s9Ë‹J#*©Ô½vŽù[¥‰VùŽÓeÇ~²?©ŸÚéƒ³+xœU�±NÃ0†) ’‰‘¤Ê©LÔ ±0TH°!×¹&§:v‰íªLÝ:ƒ:ð}�L¼ <Kß‡6löýçïÿüÑþ:z›/‚í÷ïNp±Ø–Õ*øª³p¶¤Î`rÎº‹—Öaµ¿ÓÞª–»ÝpÇpãH¦À3NÊX°9©§X‚Ñ®Ø¥×ó1)…):ua‚¥!­<äyNO‹–Cò‘�Z¡é¦CŠ·pwßKž’þs?¹…“‡|æ¤…Â7ŸúR«ë–eh IeÀXóú¤\ÊW {l~mš%‘£ig<’ÍýÁ§dÔÆy#>pÆß¹Jë�ÞZµOÐXÊˆK•8–\ džçLè"þÿ•«ë?jÑá.‚µà�)xœ;¦Ú¬<a£ÙdG‘�g·	  5¾E¸x8Çþ# Binaries
bin/
explorer
explorer-unified
*.exe
*.test

# Coverage
coverage.out
coverage.html

# Go workspace
go.work
go.work.sum

# Editor
.idea/
.vscode/
*.swp
*.swo

# OS
.DS_Store

# Local
.env
.env.local
*.local

# AI assistant context (not committed)
AGENTS.md
CLAUDE.md
GEMINI.md
QWEN.md

# Runtime
logs/
8Tcì�Ex, Óÿ¸±# Binaries
bin/
/explorer±)	.claude/
=Û¼èxŒsá// Package main is the unified Lux explorer: a single Go binary that embeds
// the SPA, indexes one or more chains via github.com/luxfi/indexer, runs a
// per-chain GraphQL engine via github.com/luxfi/graph, and exposes the
// admin/realtime API and frontend assets on one HTTP listener.
//
// Build: go build -o explorer .
// Run:   explorer --config /etc/explorer/chains.yaml
//
// Routes:
//
//	/                                       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
package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"net/http"
	"os"
	"os/signal"
	"path/filepath"
	"strings"
	"syscall"
	"time"
)

var version = "dev"

func main() {
	var (
		httpAddr   = flag.String("http", "", "HTTP listen address (default :8090 / $HTTP_ADDR)")
		dataDir    = flag.String("data", "", "Data directory (default $DATA_DIR or ~/.explorer/data)")
		configPath = flag.String("config", "", "Path to chains.yaml ($EXPLORER_CONFIG)")
		enableMDNS = flag.Bool("mdns", envBool("EXPLORER_MDNS", false), "Auto-discover chains via mDNS")
		showVer    = flag.Bool("version", false, "Show version and exit")
	)
	flag.Parse()

	if *showVer {
		fmt.Printf("explorer %s (%s)\n", version, fingerprint())
		os.Exit(0)
	}

	if *httpAddr == "" {
		*httpAddr = env("HTTP_ADDR", ":8090")
		if p := os.Getenv("PORT"); p != "" && *httpAddr == ":8090" {
			*httpAddr = ":" + p
		}
	}
	if *dataDir == "" {
		home, _ := os.UserHomeDir()
		*dataDir = env("DATA_DIR", filepath.Join(home, ".explorer", "data"))
	}
	if *configPath == "" {
		*configPath = env("EXPLORER_CONFIG", "")
	}
	if *configPath == "" {
		*configPath = findConfig(*dataDir)
	}

	var cfg Config
	if *configPath != "" {
		c, err := LoadConfig(*configPath)
		if err != nil {
			log.Fatalf("[explorer] config: %v", err)
		}
		cfg = c
	}
	if cfg.DataDir == "" {
		cfg.DataDir = *dataDir
	}
	if cfg.HTTPAddr == "" {
		cfg.HTTPAddr = *httpAddr
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	sigCh := make(chan os.Signal, 1)
	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
	go func() { <-sigCh; log.Println("[explorer] shutdown"); cancel() }()

	registry := NewChainRegistry()
	supervisor := NewChainSupervisor(cfg.DataDir)
	registry.AttachSupervisor(supervisor)
	// Bridge indexer block events into the realtime hub so WebSocket +
	// SSE subscribers see new blocks as they're ingested. Must happen
	// before any chain starts indexing â€” runChain installs the
	// Subscriber.OnBroadcast callback off the hub set here.
	supervisor.AttachRealtime(registry.hub)

	for _, c := range cfg.Chains {
		c.Source = "config"
		if err := registry.Add(c); err != nil {
			log.Printf("[explorer] skip %s from config: %v", c.Slug, err)
		}
	}

	if chains := os.Getenv("CHAINS"); chains != "" {
		registry.LoadFromEnv(chains)
	}

	if *enableMDNS {
		go registry.StartMDNSDiscovery()
	}

	go registry.hub.Run(ctx)
	go supervisor.Wait(ctx)

	frontend, err := NewFrontend(cfg, registry)
	if err != nil {
		log.Fatalf("[explorer] frontend: %v", err)
	}

	mux := http.NewServeMux()

	mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]any{
			"status":  "ok",
			"version": version,
			"chains":  registry.Count(),
		})
	})

	mux.HandleFunc("GET /v1/explorer/admin/chains", registry.HandleList)
	mux.HandleFunc("POST /v1/explorer/admin/chains", registry.HandleAdd)
	mux.HandleFunc("PUT /v1/explorer/admin/chains/{slug}", registry.HandleUpdate)
	mux.HandleFunc("DELETE /v1/explorer/admin/chains/{slug}", registry.HandleRemove)

	mux.HandleFunc("/v1/explorer/realtime", registry.hub.HandleRealtime)
	mux.HandleFunc("GET /v1/explorer/realtime/stats", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(registry.hub.Stats())
	})

	// /v1/base/realtime â€” multiplexed SSE channel the bundled SPA opens.
	// One stream carries every broadcast in a JSON envelope `{event, chain, data}`
	// that the SPA routes off via its handler map. See realtime_sse.go.
	muxRealtimeSSE := registry.hub.HandleMultiplexedSSE()
	mux.HandleFunc("GET /v1/base/realtime", muxRealtimeSSE)
	mux.HandleFunc("HEAD /v1/base/realtime", muxRealtimeSSE)

	// SSE endpoints at the URLs the bundled SPA opens EventSource on.
	// See realtime_sse.go for the channel mapping + the back-story on
	// why each path needs its own handler. Registering HEAD + GET
	// explicitly so the SPA's pre-flight (HEAD-then-EventSource) hits
	// the same SSE handler â€” net/http's ServeMux otherwise returns 405
	// "Method Not Allowed" for HEAD on a GET-only route, which the
	// SPA's `u.ok` check would reject and disable the channel.
	for path, channel := range map[string]string{
		"/blocks":          "blocks",
		"/transactions":    "transactions",
		"/token-transfers": "token_transfers",
		"/internal-txs":    "internal_transactions",
		"/tokens":          "tokens",
		"/gas-tracker":     "gas_tracker",
		"/validators":      "validators",
		"/stats":           "stats",
	} {
		h := registry.hub.HandleSSE(channel)
		mux.HandleFunc("GET "+path, h)
		mux.HandleFunc("HEAD "+path, h)
	}

	supervisor.MountRoutes(mux)
	frontend.Mount(mux)

	log.Printf("[explorer] %s listening %s data=%s chains=%d mdns=%v %s",
		version, cfg.HTTPAddr, cfg.DataDir, registry.Count(), *enableMDNS, fingerprint())

	server := &http.Server{Addr: cfg.HTTPAddr, Handler: withSecurity(mux)}
	go func() {
		if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("[explorer] server: %v", err)
		}
	}()
	<-ctx.Done()
	shutCtx, shutCancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer shutCancel()
	server.Shutdown(shutCtx)
	log.Println("[explorer] stopped")
}

// withSecurity adds the baseline security headers and CORS preflight handler.
func withSecurity(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("X-Content-Type-Options", "nosniff")
		w.Header().Set("X-Frame-Options", "DENY")
		w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
		w.Header().Set("Access-Control-Allow-Origin", "*")
		w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
		w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
		if r.Method == http.MethodOptions {
			w.WriteHeader(http.StatusNoContent)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func env(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

func envBool(key string, fallback bool) bool {
	switch strings.ToLower(os.Getenv(key)) {
	case "true", "1", "yes":
		return true
	case "false", "0", "no":
		return false
	}
	return fallback
}

// findConfig probes for a chains.yaml in the standard locations.
func findConfig(dataDir string) string {
	for _, p := range []string{
		filepath.Join(dataDir, "chains.yaml"),
		"chains.yaml",
		"/etc/explorer/chains.yaml",
	} {
		if _, err := os.Stat(p); err == nil {
			return p
		}
	}
	return ""
}
«$ãóExœC ¼ÿºº°6*)è}ÌŒ÷£9VIuÊæã¶É~100644 go.sum ëÈ�^�~
“ :KÄªÎiu³7ƒ¦
Ôâƒë8xœk�ÇtoÓ.×É]¹6ïvõò LWÆárx! Þÿºº°Æ”(¦‰‰ÿïÓQ"³îêhjÿ³¥ëœÀgÃx øÿÆ%ƒ#°ƒ‘Öä	Dx” kÿºº"40000 .github „=rP·ÙÃù!F¡ÃZõ€=vf‘"]k¤TMJ+“ ñËP¾½³�«±“n6E5ug¡)BLF±–9—,q¤…¦Þ100644 go.sum |Sï…)B’(2—(ŽÆÚ‚Ü¹I³7oˆM´x¦Ïþ)Sý9+ìÉ+«L¡3:²ã€"xC ¼ÿºº°6+œñ–³>¬¨$ãRMƒ–"“â)100644 go.sum 7Ïós¡	��³»žaqplÀ�%³7ƒ•‹žëRx+ Ôÿºº"40000 .github •u©wº	¹Ý:È>>7Ì))±"˜7uã9xC ¼ÿºº"40000 .github Ãf}1W¤Þ /Kå�À§hòa*ú‘"]¹S¯‡pv9Íc[%•(Í]ªó±“'Z
êà‚x ïÿƒ#ƒ#�—3“A±«Ø/ÃÛà¼xÀ{?„ƒ#»›c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M=
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM=
cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI=
cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
cloud.google.com/go/storage v1.36.0 h1:P0mOkAcaJxhCTvAkMhxMfrTKiNcub4YmmPBtlhAyTr8=
cloud.google.com/go/storage v1.36.0/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
“00h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
“0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2 h1:Hr5FTipp7SL07o2FvoVOX9HRiRH3CR3Mj8pxqCcdD5A=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2/go.mod h1:QyVsSSN64v5TGltphKLQ2sQxe4OBQg0J1eKRcVBnfgE=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 h1:MhRfI58HblXzCtWEZCO0feHs8LweePB3s90r7WaR1KU=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0/go.mod h1:okZ+ZURbArNdlJ+ptXoyHNuOETzOl1Oww19rm8I2WLA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2 h1:FwladfywkNirM+FZYLBR2kBz5C8Tg0fw5w5Y7meRXWI=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2/go.mod h1:vv5Ad0RrIoT1lJFdWBZwt4mB1+j+V8DUroixmKDTCdk=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0 h1:wQlqotpyjYPjJz+Noh5bRu7Snmydk8SKC5Z6u1CR20Y=
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0/go.mod h1:FTzydeQVmR24FI0D6XWUOMKckjXehM/jgMn1xC+DA9M=
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18 h1:9vWXHtaepwoAl/UuKzxwgOoJDXPCC3hvgNMfcmdS2Tk=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18/go.mod h1:sKuUZ+MwUTuJbYvZ8pK0x10LvgcJK3Y4rmh63YBekwk=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ib
hJ6rlkpw=
“€00h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
“€/8/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
“½$0h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
“½#8/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
“î#0h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
“î"/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
“)0h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU=
“(/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
“T%0h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
“T$/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi/v“�	0h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
“†/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go5.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
“ß0h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
“ß/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
gi“30h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
“4/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw=
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go? v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
“M0h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
“M/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
github.com/googleaMpis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
“x0h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
“x/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/rpc v1.2.1 h1:yC+LMV5esttgpVvNORL/xX4jvTTEUE30UZhZ5J\F7K9k=
github.com/gorilla/rpc v1.2.1/go.mod h1:uNpOihAlF5xRFLuTYhfR0yfCTm0WTQSQttkMSptRfGk=
‘8A1 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
‘8A8/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
“Î$0h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
“Î#8/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
“1“0h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k=
“1Dhijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
“1*0h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0=
“1)8/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc=
“h0h1:jWamJSHg1RtvcarevSJ/W5lyPJfRB4vBYXDuxyyOup0=
“h8/go.mod h1:rWgoDyQiB5d+GOGYncKrQu6uS7Gnoh0xlm1hCU5rq5E=
““"0h1:NjK1Umwaxb5lnoFB44iGPtGj58y/PLucNcCc2gE8q4I=
““!8/go.mod h1:/iX6TGHL94Q9MkMmvHKKHlyopHn06RjEW7Bz3h6TJEk=
“Â$0h1:PulFMIVVbvxz/LmBSKlj/o+T7nZmPGd2L2YjYVv5V1U=
“Â#8/go.mod h1:U0WdEoFFNw4lS9CRysXtsWOqtkzjIi2Ggv+C8Nsz8+4=
“ó*0h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
“ó)/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOedrLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
“*A0h1:lvIuaX7hO0eO3Rlev+cVnlsoExR3i/JXxu88zt4JHPg=
“*@8/go.mod h1:Wqfu7mjUHj9WDzSSPI5KfBclTTEnLveRUFr/ujWnTgE=
“x&0h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
“x%8/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
“«!;0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
“«%0h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
“«$8/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
“Ý"0h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
“Ý!/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug? v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
“0h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
“8/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
“3!h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I=
github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJ	Z0NfMwE=
“a0h1:Tsk6gXj2uKE19501bD0ajRYdeCHIlTGb6jYyLc+F8hc=
“a8/go.mod h1:K00BcnLzEYMPHwCFq8Tf/450ApmTs9xBVvYOnofJCkc=
“Œ0h1:G69HbSV4R3vKEH9B0CulnRaMdSdf4RalMgP8xKmxHeI=
“Œ/go.mod h1:iAYAxgvrXxcy746+Ovh/eWWDuF9teJLNcCSSOX9RYW0=
github.com/luxfi/atomic v1.0.0 h1:xUV60MuzRvXngaQ1sM0yVC2v4TRoLlUGkkH7M9PS4yw=
github.com/luxfi/atomic v1.0.0/go.mod h1:0G2mTlQ6TXWHICUHrUUPu1/qAiIyR4gSZ2tva9ci/bI=
github.com/luxfi/cache v1.2.1 h1:kAzOS55/hmYeNKR+0HAKv4ma48Y6JjkI8UQeqdZ8bfI=
github.com/luxfi/cache v1.2.1/go.mod h1:co7JTxZZHpKT31Yh01LFp5aZOxmoUg157FhBLQdQHVU=
“à0h1:Yl8ZalMNkqo7cD6R9AjczAajkLOmsjyZ9+DASVYHrvg=
“à8/go.mod h1:oGQ3j6E8c2P0pL0irYtWkrB1hmDUFIE0puXHK4gV5KI=
“!0h1:4tEUHw5MK1bu5UOjfYCt4OKMiH7yykIgmGPRA/BfJTM=
“ 8/go.mod h1:Cc1yxD2pfzrvpO32W2GDwLKff+CylHEvzZh2Ko8RSIU=
“9#0h1:eJyv1fhaC0jMLMw6+QS774cUmp7GK+ouMgvLCqnC7cc=
“9"/go.mod h1:Aj/FR5NpM0cB2P4Nt3+tz9+dV6V+LUW4HuMgSjwq5hw=
github.com/luxfi/constants v1.4.7 h1:e/Qs+DQP3pugle3Zncq6fZCxKgqqtbyD/z7Gm4ZjsYg=
github.com/luxfi/constants v1.4.7/go.mod h1:hOszZ2NDQ8gMZKncfcZ67PXkb5OIbnwAzXC3oFbQwW0=
github.com/luxfi/container v0.0.4 h1:BXhF82WyfqVP5mjlNcr7tP0Fcnvl0Ap1rkiu+rq5XuM=
github.com/luxfi/container v0.0.4/go.mod h1:Z3SpmMF5d4t77MM0nHYXURpn+EMVaeu1fhbd/3BGaek=
“i!0h1:l2LLu7UFyICtJVfraLDLRi+lFGiDXKHSL18M9/m1gsQ=
“i 8/go.mod h1:INjdZtke85k8hX/QAmTMAY8bbZ4gzGZQLqURg3xf6Gk=
“—"0h1:sySH5eS8ihJtyc+yb0LgTignGwJ9H4W9bv+nboCuIm0=
“—!/go.mod h1:o1ccjgVKMYbVBfi+6+sDvx4swGFgGnCGFQ4gL/oJ+kY=
github.com/luxfi/geth v1.16.98 h1:w187TtKuGStf3tm2bshuHVKBv2Frjx0lT54kQ^VXyNHA=
github.com/luxfi/geth v1.16.98/go.mod h1:6kEzSExdk9CPQDPXALt6P3HfQqBq7KF1Jrrv9gBpxbU=
‘{1 h1:cgHiBU8vBO4b39HnVr1UewEAeP3Bx+JZBHlfacTvDZQ=
‘{/go.mod h1:NbKTixz6Miunyh+7ER3G389mzUKV22QIVPjpcTnKZuY=
github.com/luxfi/ids v1.2.13 h1:lEotq0WUpMLcc/5X5MbE3n73jdfNo8IlFvTDfUXZg+9A=
github.com/luxfi/ids v1.2.13/go.mod h1:QWjJvggX/nRkmrz1AGSaF2Y6mnOvz3g7lFOzs3Zd+JM=
‘š1 h1:vfYgM/hnNujjS47B7/VKAxFJHsFhhME0SnLaAAaYuwA=
‘š8/go.mod h1:pz5W+DqKx0La2uyq1mV5Kpi0gXXNhmVpZTZRmhw+fX4=
“ð0h1:xkUKRWvQ4ZwvlUC2e0/RTtHYZOYSMvSQ9W9lbjwBmiI=
“ð8/go.mod h1:myIkufyiQomSQH34K981kbz6cG4WUoerRUh7F4XhlQI=
“0h1:1t9bCCsEqnl9yIKrShlbs80DBKyYTWdnzkVfBqEeO7Q=
“8/go.mod h1:QvbRxauQyE1w4lvbcLSe6c8yeJz2Zj1Bq1rayGgs2tA=
“C!h1:Vz4c0RsZVPdIKPsHPgAJChH/R3p15WHRUz7LkLf+NIQ=
github.com/luxfi/math/big v0.1.0/go.mod h1:BuxSu22RbO93xBLk5Eam5nldFponoJ73xDFz	4uJ3Huk=
‘»1 h1:g2eRr9AXcziPkkcd24M+Qu9ApEpoKKjfI79QSNqv0rQ=
‘»8/go.mod h1:dbp5f3h3aE7CGzwbaWzBM9cwdcekhmSrWhQevgYhhNA=
“q0h1:axPwfq+erOlIue7IJp5g+hMcMtVhYHja9gJAaT3+KNA=
“q8/go.mod h1:fO2giazkg4NDtr72JM/QXJBYebplAMeWC1JoZyNDvKw=
“�0h1:0HEtIjg1J6CWz+IUyP6rsGqNWTcmxjFnSQIhaDuARwY=
“�/go.mod h1:jo35akl3Vtd8LbzDts8VJ0jmSVycrd1/eBi6g6t5hKU=
github.com/luxfi/pq v1.0.3 h1:pFlQm1+5FuKTDUh2y/23bXWkN4I2Rc5iuxJypwDFFMs=
github.com/luxfi/pq v1.0.3/go.mod h1:8bppZcRElfrVt0n3nYCZW3iX1TvhvzNbdjNdK1irgIE=
github.com/luxfi/sampler v1.0.0 h1:k8Sf6otW83w4pQp0jXLA+g3J/joB7w7SqXQsWmNTOV0=
github.com/luxfi/sampler v1.0.0/go.mod h1:f96/ozlj9vFfZj+akLtrHn4VpulQahwB+MQQhpeIekk=
github.com/luxfi/utils v1.1.4 h1:8OY0jXCkpp6OotN1Y++6DATJkvtEwzq2k0p56imJlAk=
github.com/luxfi/utils v1.1.4/go.mod h1:c3yz1RjzrB#+cs5GZm+q1T3/2cCKElO9vxm9yRRtgSEM=
“ð8 v0.7.0 h1:wiOXfctxiNk0EAiaEPURTXemvO8fyaGViIkoQVWs7Kw=
“ð? v0.7.0/go.mod h1:WOBoeqEh0BCApg1wfxoadOLMXZUC9T1ksOABGoEp8os=
“ð0h1:1lLHEmkyC0BucnA/zjQYsMkUVxuEo2vQkEaQGjYfuuc=
“ð8/go.mod h1:Qukh3hDRD0MnxA6z+a28JTnXhN85AiLLgp6TYr4QAMc=
“	-0h1:2e8XJozaERVxGwsRg72coi51L2aiYqE2gukkdLc85ck=
“	,8/go.mod h1:CBAUADJuMNhJpyM6IYaWAoFhtKaqnUcznY2cL7gNugY=
“V	&0h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
“V	%8/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
“‰	#0h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
“‰	"8/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
‘Ù>1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
‘Ù/1 h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
‘Ù//go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
“¹	0h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
“¹	8/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
“ã	"0h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
“ã	!/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSbQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
“@
$0h1:RCJM0R1XOsRs+A3x3UCaf3ZYbByDaLjFeAi+YCQEPhs=
“@
#8/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0=
“q
!0h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI=
“q
 8/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8=
“Ÿ
@0h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
“Ÿ
?/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.44.0 h1:ECKVrDLdh/kDPV1g0gAQ+2+m2KprqZK5O/eJAyAnH2M=
github.com/nats-io/nats.go v1.44.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/7go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
“ì
&0h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
“ì
%8/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
“ 0h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
“/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhY#xTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
“î“b0h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
“î“b8/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
“î“›0h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
“î“›8/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
“î?common v0.68.0 h1:8rQJvQmYltsR2L7h8Zw0Iyj8WYNNmpwikoQTZXwfVeA=
“îFcommon v0.68.0/go.mod h1:4soH+U8yJSROk7OJ//hmTiWKsxapv6zRGgTt3keN8gQ=
“î%0h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
“î$8/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
“ B0h1:9bBMbcwroL46feESdJWjRX0GV+k8o/P9gAg9UX6Vz7U=
“ A8/go.mod h1:iW4cSew5PAb1sMZiTEkVJAIBNrepaB6jTYjeP47WtI0=
“oD0h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
“oC/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/x“Ï
0h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
“À/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/studio-b12/gowebdav v0.11.0 h1:qbQzq4USxY28ZYsGJUfO5jR+xkFtcnwWgitp4Zp1irU=
github.com/studio-b12/gowebdav v0.11.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
“æ&0h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
“æ%8/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
“0h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
“8/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
“E#0h1:9j4uCAJyO3IJHygeOdUJNzm6sX3P1+3xuKTbCITQrvc=
“E"8/go.mod h1:cDOlK/pNYGbGKRNa+NnXDJ1c8R63tWjnTjFKzoSbLv0=
“uF0h1:gQ+ZV4wJke/EBKYciZ2MshEouEHFuinB85dY3f5s1q8=
“uE/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUo^NXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
“È0h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
“È/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0R=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
“Q“0h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
“Q“8/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
“Qcontrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE=
go.opentelemetr“`contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE=
go.opent“Ybcontrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
“Qicontrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
“Q“;	0h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
“Q@ v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
“Q(0h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
“Q'8/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
“Q“Ÿ0h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
“Q“Ÿ/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdM	YcacYXQ=
“ß0h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
“ß/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/g6o.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
“/40h1:4d4PbuBNwaxMXkXI8yiIYjydtMU+04RHeuSxJdgKftM=
“/38/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
“pY1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
“p0h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
“p/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
“¼0h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
“¼/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
“0W0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
“00h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
“0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.154.0 h1:X7QkVKZBskztmpPKWQXgjJRPA2dJYrL6r+sYPRLj050=
google.golang.org/api v0.154.0/go.mod h1:qhSMkM85hgqiokIYsrRyKxrjfBeIhgl4Z2JmeRkYylc=
google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos=
google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/“j0h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
“X#/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.2 h1:JtOSMb9OuaCZKr7h5D/h6iii14sK0hLbplTc6frx4Ss=
gopkg.in/ini.v1 v1.67.2/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
“®(0h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
“®'/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.2 h1:mxsy2FdrB6+qG3NfXefz1AmWv0ehOSDO4jxgxd7h9yo=
modernc.org/ccgo/v4 v4.34.2/go.mod h1:1L7us56+kAKu04p25EATpmBBvhbcqqZ85ibqWVwVgog=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.5 h1:m2OGx9Ser1VvTS4Z9ZJlWs+CBMxutLaTiAWkNz+NB9U=
modernc.org/libc v1.72.5/go.mod h1:np0N7KDJ7eUtMZmOqVZNldrZyG+DHLl2B5pg8Hbar3U=
“	0h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
“	/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
“Z0h1:aH/MMSoayAIhozZ7uJbVTT9QO/VhzBf0J9tymmmuC/U=
“Z/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kpLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
,í<vl‚ž"x óÿØuÚp°+#³©%/*+[¦x& Ùÿ100644 docker.yml ÀÃ'K
‡‘Æ�YÁÑ{h�PÀþünB¯xO °ÿ100644 actionlint.yaml !U¡išQà›â‡Fµ°˜!>F40000 workflows „n8TVÆà/ª_û9StKÝ¦x& Ùÿ100644 docker.yml ›ïhÒÌ½ÿºÿÚgƒÈ±Û@Ò]&ÿ÷é‹¶Mx æÿÇœ°runs-on:“�³aæo¯”±šx¡^îmodule github.com/luxfi/explorer

go 1.26.4

require (
	github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
	github.com/luxfi/graph v1.2.2
	github.com/luxfi/indexer v1.4.5-0.20260526184203-3b72d71cc8fd
	github.com/luxfi/mdns v0.1.1
	github.com/mattn/go-sqlite3 v2.0.3+incompatible
	gopkg.in/yaml.v3 v3.0.1
)

require (
	filippo.io/hpke v0.4.0 // indirect
	github.com/beorn7/perks v1.0.1 // indirect
	github.com/cenkalti/backoff v2.2.1+incompatible // indirect
	github.com/cespare/xxhash/v2 v2.3.0 // indirect
	github.com/cloudflare/circl v1.6.3 // indirect
	github.com/dgraph-io/ristretto/v2 v2.4.0 // indirect
	github.com/dustin/go-humanize v1.0.1 // indirect
	github.com/go-chi/chi/v5 v5.3.0 // indirect
	github.com/go-logr/logr v1.4.3 // indirect
	github.com/go-logr/stdr v1.2.2 // indirect
	github.com/google/flatbuffers v25.12.19+incompatible // indirect
	github.com/google/uuid v1.6.0 // indirect
	github.com/gorilla/mux v1.8.1 // indirect
	github.com/gorilla/rpc v1.2.1 // indirect
	github.com/grandcat/zeroconf v1.0.0 // indirect
	github.com/hablullah/go-hijri v1.0.2 // indirect
	github.com/hablullah/go-juliandays v1.0.0 // indirect
	github.com/hanzoai/ltx v0.5.1 // indirect
	github.com/hanzoai/lz4/v4 v4.1.22 // indirect
	github.com/hanzoai/replicate v0.7.2 // indirect
	github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
	github.com/jalaali/go-jalaali v0.0.0-20250521085720-bf793ab67800 // indirect
	github.com/klauspost/compress v1.18.6 // indirect
	github.com/klauspost/cpuid/v2 v2.3.0 // indirect
	github.com/klauspost/crc32 v1.3.0 // indirect
	github.com/lib/pq v1.12.3 // indirect
	github.com/lmittmann/tint v1.1.3 // indirect
	github.com/luxfi/accel v1.1.9 // indirect
	github.com/luxfi/age v1.5.0 // indirect
	github.com/luxfi/cache v1.2.1 // indirect
	github.com/luxfi/codec v1.1.4 // indirect
	github.com/luxfi/compress v0.0.5 // indirect
	github.com/luxfi/concurrent v0.0.3 // indirect
	github.com/luxfi/crypto v1.19.17 // indirect
	github.com/luxfi/database v1.19.0 // indirect
	github.com/luxfi/ids v1.2.13 // indirect
	github.com/luxfi/log v1.4.3 // indirect
	github.com/luxfi/math v1.4.1 // indirect
	github.com/luxfi/math/big v0.1.0 // indirect
	github.com/luxfi/metric v1.5.8 // indirect
	github.com/luxfi/mock v0.1.1 // indirect
	github.com/luxfi/zap v0.7.0 // indirect
	github.com/luxfi/zapdb v1.10.0 // indirect
	github.com/markusmobius/go-dateparser v1.2.4 // indirect
	github.com/mattn/go-colorable v0.1.15 // indirect
	github.com/mattn/go-isatty v0.0.22 // indirect
	github.com/miekg/dns v1.1.72 // indirect
	github.com/minio/crc64nvme v1.1.1 // indirect
	github.com/minio/md5-simd v1.1.2 // indirect
	github.com/minio/minio-go/v7 v7.2.0 // indirect
	github.com/mr-tron/base58 v1.3.0 // indirect
	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
	github.com/ncruces/go-strftime v1.0.0 // indirect
	github.com/philhofer/fwd v1.2.0 // indirect
	github.com/prometheus/client_golang v1.23.2 // indirect
	github.com/prometheus/client_model v0.6.2 // indirect
	github.com/prometheus/common v0.68.0 // indirect
	github.com/prometheus/procfs v0.20.1 // indirect
	github.com/psanford/sqlite3vfs v0.0.0-20260519004904-f9180fa2acc9 // indirect
	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
	github.com/rs/xid v1.6.0 // indirect
	github.com/tetratelabs/wazero v1.12.0 // indirect
	github.com/tinylib/msgp v1.6.4 // indirect
	github.com/wasilibs/go-re2 v1.11.0 // indirect
	github.com/wasilibs/wazero-helpers v0.0.0-20250123031827-cd30c44769bb // indirect
	github.com/zeebo/xxh3 v1.1.0 // indirect
	go.opentelemetry.io/auto/sdk v1.2.1 // indirect
	go.opentelemetry.io/otel v1.44.0 // indirect
	go.opentelemetry.io/otel/metric v1.44.0 // indirect
	go.opentelemetry.io/otel/trace v1.44.0 // indirect
	go.uber.org/mock v0.6.0 // indirect
	go.yaml.in/yaml/v3 v3.0.4 // indirect
	golang.org/x/crypto v0.52.0 // indirect
	golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 // indirect
	golang.org/x/mod v0.36.0 // indirect
	golang.org/x/net v0.55.0 // indirect
	golang.org/x/sync v0.20.0 // indirect
	golang.org/x/sys v0.45.0 // indirect
	golang.org/x/text v0.37.0 // indirect
	golang.org/x/tools v0.45.0 // indirect
	google.golang.org/protobuf v1.36.11 // indirect
	gopkg.in/ini.v1 v1.67.2 // indirect
	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
	modernc.org/libc v1.72.5 // indirect
	modernc.org/mathutil v1.7.1 // indirect
	modernc.org/memory v1.11.0 // indirect
	modernc.org/sqlite v1.51.0 // indirect
)
Szµd·ßx÷�rc2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M=
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM=
cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI=
cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8=
cloud.google.com/go/storage v1.36.0 h1:P0mOkAcaJxhCTvAkMhxMfrTKiNcub4YmmPBtlhAyTr8=
cloud.google.com/go/storage v1.36.0/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8=
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2 h1:Hr5FTipp7SL07o2FvoVOX9HRiRH3CR3Mj8pxqCcdD5A=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.2/go.mod h1:QyVsSSN64v5TGltphKLQ2sQxe4OBQg0J1eKRcVBnfgE=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 h1:MhRfI58HblXzCtWEZCO0feHs8LweePB3s90r7WaR1KU=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0/go.mod h1:okZ+ZURbArNdlJ+ptXoyHNuOETzOl1Oww19rm8I2WLA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2 h1:FwladfywkNirM+FZYLBR2kBz5C8Tg0fw5w5Y7meRXWI=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.2/go.mod h1:vv5Ad0RrIoT1lJFdWBZwt4mB1+j+V8DUroixmKDTCdk=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0 h1:wQlqotpyjYPjJz+Noh5bRu7Snmydk8SKC5Z6u1CR20Y=
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0/go.mod h1:FTzydeQVmR24FI0D6XWUOMKckjXehM/jgMn1xC+DA9M=
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18 h1:9vWXHtaepwoAl/UuKzxwgOoJDXPCC3hvgNMfcmdS2Tk=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.18/go.mod h1:sKuUZ+MwUTuJbYvZ8pK0x10LvgcJK3Y4rmh63YBekwk=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/dgraph-io/ristretto/v2 v2.4.0 h1:I/w09yLjhdcVD2QV192UJcq8dPBaAJb9pOuMyNy0XlU=
github.com/dgraph-io/ristretto/v2 v2.4.0/go.mod h1:0KsrXtXvnv0EqnzyowllbVJB8yBonswa2lTCK2gGo9E=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw=
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/rpc v1.2.1 h1:yC+LMV5esttgpVvNORL/xX4jvTTEUE30UZhZ5JF7K9k=
github.com/gorilla/rpc v1.2.1/go.mod h1:uNpOihAlF5xRFLuTYhfR0yfCTm0WTQSQttkMSptRfGk=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6kE=
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k=
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0=
github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc=
github.com/hanzoai/ltx v0.5.1 h1:jWamJSHg1RtvcarevSJ/W5lyPJfRB4vBYXDuxyyOup0=
github.com/hanzoai/ltx v0.5.1/go.mod h1:rWgoDyQiB5d+GOGYncKrQu6uS7Gnoh0xlm1hCU5rq5E=
github.com/hanzoai/lz4/v4 v4.1.22 h1:NjK1Umwaxb5lnoFB44iGPtGj58y/PLucNcCc2gE8q4I=
github.com/hanzoai/lz4/v4 v4.1.22/go.mod h1:/iX6TGHL94Q9MkMmvHKKHlyopHn06RjEW7Bz3h6TJEk=
github.com/hanzoai/replicate v0.7.2 h1:PulFMIVVbvxz/LmBSKlj/o+T7nZmPGd2L2YjYVv5V1U=
github.com/hanzoai/replicate v0.7.2/go.mod h1:U0WdEoFFNw4lS9CRysXtsWOqtkzjIi2Ggv+C8Nsz8+4=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/jalaali/go-jalaali v0.0.0-20250521085720-bf793ab67800 h1:lvIuaX7hO0eO3Rlev+cVnlsoExR3i/JXxu88zt4JHPg=
github.com/jalaali/go-jalaali v0.0.0-20250521085720-bf793ab67800/go.mod h1:Wqfu7mjUHj9WDzSSPI5KfBclTTEnLveRUFr/ujWnTgE=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I=
github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/luxfi/accel v1.1.9 h1:Tsk6gXj2uKE19501bD0ajRYdeCHIlTGb6jYyLc+F8hc=
github.com/luxfi/accel v1.1.9/go.mod h1:K00BcnLzEYMPHwCFq8Tf/450ApmTs9xBVvYOnofJCkc=
github.com/luxfi/age v1.5.0 h1:G69HbSV4R3vKEH9B0CulnRaMdSdf4RalMgP8xKmxHeI=
github.com/luxfi/age v1.5.0/go.mod h1:iAYAxgvrXxcy746+Ovh/eWWDuF9teJLNcCSSOX9RYW0=
github.com/luxfi/atomic v1.0.0 h1:xUV60MuzRvXngaQ1sM0yVC2v4TRoLlUGkkH7M9PS4yw=
github.com/luxfi/atomic v1.0.0/go.mod h1:0G2mTlQ6TXWHICUHrUUPu1/qAiIyR4gSZ2tva9ci/bI=
github.com/luxfi/cache v1.2.1 h1:kAzOS55/hmYeNKR+0HAKv4ma48Y6JjkI8UQeqdZ8bfI=
github.com/luxfi/cache v1.2.1/go.mod h1:co7JTxZZHpKT31Yh01LFp5aZOxmoUg157FhBLQdQHVU=
github.com/luxfi/codec v1.1.4 h1:Yl8ZalMNkqo7cD6R9AjczAajkLOmsjyZ9+DASVYHrvg=
github.com/luxfi/codec v1.1.4/go.mod h1:oGQ3j6E8c2P0pL0irYtWkrB1hmDUFIE0puXHK4gV5KI=
github.com/luxfi/compress v0.0.5 h1:4tEUHw5MK1bu5UOjfYCt4OKMiH7yykIgmGPRA/BfJTM=
github.com/luxfi/compress v0.0.5/go.mod h1:Cc1yxD2pfzrvpO32W2GDwLKff+CylHEvzZh2Ko8RSIU=
github.com/luxfi/concurrent v0.0.3 h1:eJyv1fhaC0jMLMw6+QS774cUmp7GK+ouMgvLCqnC7cc=
github.com/luxfi/concurrent v0.0.3/go.mod h1:Aj/FR5NpM0cB2P4Nt3+tz9+dV6V+LUW4HuMgSjwq5hw=
github.com/luxfi/constants v1.4.7 h1:e/Qs+DQP3pugle3Zncq6fZCxKgqqtbyD/z7Gm4ZjsYg=
github.com/luxfi/constants v1.4.7/go.mod h1:hOszZ2NDQ8gMZKncfcZ67PXkb5OIbnwAzXC3oFbQwW0=
github.com/luxfi/container v0.0.4 h1:BXhF82WyfqVP5mjlNcr7tP0Fcnvl0Ap1rkiu+rq5XuM=
github.com/luxfi/container v0.0.4/go.mod h1:Z3SpmMF5d4t77MM0nHYXURpn+EMVaeu1fhbd/3BGaek=
github.com/luxfi/crypto v1.19.17 h1:l2LLu7UFyICtJVfraLDLRi+lFGiDXKHSL18M9/m1gsQ=
github.com/luxfi/crypto v1.19.17/go.mod h1:INjdZtke85k8hX/QAmTMAY8bbZ4gzGZQLqURg3xf6Gk=
github.com/luxfi/database v1.19.0 h1:sySH5eS8ihJtyc+yb0LgTignGwJ9H4W9bv+nboCuIm0=
github.com/luxfi/database v1.19.0/go.mod h1:o1ccjgVKMYbVBfi+6+sDvx4swGFgGnCGFQ4gL/oJ+kY=
github.com/luxfi/geth v1.16.98 h1:w187TtKuGStf3tm2bshuHVKBv2Frjx0lT54kQVXyNHA=
github.com/luxfi/geth v1.16.98/go.mod h1:6kEzSExdk9CPQDPXALt6P3HfQqBq7KF1Jrrv9gBpxbU=
github.com/luxfi/graph v1.2.2 h1:APDihqCknjMeo06M3Xi9WAuyIMYOWMwsGmvKw9JElEU=
github.com/luxfi/graph v1.2.2/go.mod h1:/wuzuD5jiziLSecONTi3GOeuzLhAsflOqMerEawuFgw=
github.com/luxfi/ids v1.2.13 h1:lEotq0WUpMLcc/5X5MbE3n73jdfNo8IlFvTDfUXg+9A=
github.com/luxfi/ids v1.2.13/go.mod h1:QWjJvggX/nRkmrz1AGSaF2Y6mnOvz3g7lFOzs3Zd+JM=
github.com/luxfi/indexer v1.4.5-0.20260526184203-3b72d71cc8fd h1:RBI4m3Y+MmIsb1JO7JzBi/HffCVavhzt814qQbToGFk=
github.com/luxfi/indexer v1.4.5-0.20260526184203-3b72d71cc8fd/go.mod h1:h5U3td/esXhbkDojcpvFQtSCQ9USrYMnnmtNk56ei+Y=
github.com/luxfi/log v1.4.3 h1:xkUKRWvQ4ZwvlUC2e0/RTtHYZOYSMvSQ9W9lbjwBmiI=
github.com/luxfi/log v1.4.3/go.mod h1:myIkufyiQomSQH34K981kbz6cG4WUoerRUh7F4XhlQI=
github.com/luxfi/math v1.4.1 h1:1t9bCCsEqnl9yIKrShlbs80DBKyYTWdnzkVfBqEeO7Q=
github.com/luxfi/math v1.4.1/go.mod h1:QvbRxauQyE1w4lvbcLSe6c8yeJz2Zj1Bq1rayGgs2tA=
github.com/luxfi/math/big v0.1.0 h1:Vz4c0RsZVPdIKPsHPgAJChH/R3p15WHRUz7LkLf+NIQ=
github.com/luxfi/math/big v0.1.0/go.mod h1:BuxSu22RbO93xBLk5Eam5nldFponoJ73xDFz4uJ3Huk=
github.com/luxfi/mdns v0.1.1 h1:g2eRr9AXcziPkkcd24M+Qu9ApEpoKKjfI79QSNqv0rQ=
github.com/luxfi/mdns v0.1.1/go.mod h1:dbp5f3h3aE7CGzwbaWzBM9cwdcekhmSrWhQevgYhhNA=
github.com/luxfi/metric v1.5.8 h1:axPwfq+erOlIue7IJp5g+hMcMtVhYHja9gJAaT3+KNA=
github.com/luxfi/metric v1.5.8/go.mod h1:fO2giazkg4NDtr72JM/QXJBYebplAMeWC1JoZyNDvKw=
github.com/luxfi/mock v0.1.1 h1:0HEtIjg1J6CWz+IUyP6rsGqNWTcmxjFnSQIhaDuARwY=
github.com/luxfi/mock v0.1.1/go.mod h1:jo35akl3Vtd8LbzDts8VJ0jmSVycrd1/eBi6g6t5hKU=
github.com/luxfi/pq v1.0.3 h1:pFlQm1+5FuKTDUh2y/23bXWkN4I2Rc5iuxJypwDFFMs=
github.com/luxfi/pq v1.0.3/go.mod h1:8bppZcRElfrVt0n3nYCZW3iX1TvhvzNbdjNdK1irgIE=
github.com/luxfi/sampler v1.0.0 h1:k8Sf6otW83w4pQp0jXLA+g3J/joB7w7SqXQsWmNTOV0=
github.com/luxfi/sampler v1.0.0/go.mod h1:f96/ozlj9vFfZj+akLtrHn4VpulQahwB+MQQhpeIekk=
github.com/luxfi/utils v1.1.4 h1:8OY0jXCkpp6OotN1Y++6DATJkvtEwzq2k0p56imJlAk=
github.com/luxfi/utils v1.1.4/go.mod h1:c3yz1RjzrB+cs5GZm+q1T3/2cCKElO9vxm9yRRtgSEM=
github.com/luxfi/zap v0.7.0 h1:wiOXfctxiNk0EAiaEPURTXemvO8fyaGViIkoQVWs7Kw=
github.com/luxfi/zap v0.7.0/go.mod h1:WOBoeqEh0BCApg1wfxoadOLMXZUC9T1ksOABGoEp8os=
github.com/luxfi/zapdb v1.10.0 h1:1lLHEmkyC0BucnA/zjQYsMkUVxuEo2vQkEaQGjYfuuc=
github.com/luxfi/zapdb v1.10.0/go.mod h1:Qukh3hDRD0MnxA6z+a28JTnXhN85AiLLgp6TYr4QAMc=
github.com/markusmobius/go-dateparser v1.2.4 h1:2e8XJozaERVxGwsRg72coi51L2aiYqE2gukkdLc85ck=
github.com/markusmobius/go-dateparser v1.2.4/go.mod h1:CBAUADJuMNhJpyM6IYaWAoFhtKaqnUcznY2cL7gNugY=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.2.0 h1:RCJM0R1XOsRs+A3x3UCaf3ZYbByDaLjFeAi+YCQEPhs=
github.com/minio/minio-go/v7 v7.2.0/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0=
github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI=
github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.44.0 h1:ECKVrDLdh/kDPV1g0gAQ+2+m2KprqZK5O/eJAyAnH2M=
github.com/nats-io/nats.go v1.44.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.6 h1:JFZT4XbOU7l77xGSpOdW+pwIMqP044IyjXX6FGyEKFo=
github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.68.0 h1:8rQJvQmYltsR2L7h8Zw0Iyj8WYNNmpwikoQTZXwfVeA=
github.com/prometheus/common v0.68.0/go.mod h1:4soH+U8yJSROk7OJ//hmTiWKsxapv6zRGgTt3keN8gQ=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/psanford/sqlite3vfs v0.0.0-20260519004904-f9180fa2acc9 h1:9bBMbcwroL46feESdJWjRX0GV+k8o/P9gAg9UX6Vz7U=
github.com/psanford/sqlite3vfs v0.0.0-20260519004904-f9180fa2acc9/go.mod h1:iW4cSew5PAb1sMZiTEkVJAIBNrepaB6jTYjeP47WtI0=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/studio-b12/gowebdav v0.11.0 h1:qbQzq4USxY28ZYsGJUfO5jR+xkFtcnwWgitp4Zp1irU=
github.com/studio-b12/gowebdav v0.11.0/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/wasilibs/go-re2 v1.11.0 h1:9j4uCAJyO3IJHygeOdUJNzm6sX3P1+3xuKTbCITQrvc=
github.com/wasilibs/go-re2 v1.11.0/go.mod h1:cDOlK/pNYGbGKRNa+NnXDJ1c8R63tWjnTjFKzoSbLv0=
github.com/wasilibs/wazero-helpers v0.0.0-20250123031827-cd30c44769bb h1:gQ+ZV4wJke/EBKYciZ2MshEouEHFuinB85dY3f5s1q8=
github.com/wasilibs/wazero-helpers v0.0.0-20250123031827-cd30c44769bb/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 h1:4d4PbuBNwaxMXkXI8yiIYjydtMU+04RHeuSxJdgKftM=
golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/api v0.154.0 h1:X7QkVKZBskztmpPKWQXgjJRPA2dJYrL6r+sYPRLj050=
google.golang.org/api v0.154.0/go.mod h1:qhSMkM85hgqiokIYsrRyKxrjfBeIhgl4Z2JmeRkYylc=
google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos=
google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.2 h1:JtOSMb9OuaCZKr7h5D/h6iii14sK0hLbplTc6frx4Ss=
gopkg.in/ini.v1 v1.67.2/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
modernc.org/ccgo/v4 v4.34.2 h1:mxsy2FdrB6+qG3NfXefz1AmWv0ehOSDO4jxgxd7h9yo=
modernc.org/ccgo/v4 v4.34.2/go.mod h1:1L7us56+kAKu04p25EATpmBBvhbcqqZ85ibqWVwVgog=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc=
modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.5 h1:m2OGx9Ser1VvTS4Z9ZJlWs+CBMxutLaTiAWkNz+NB9U=
modernc.org/libc v1.72.5/go.mod h1:np0N7KDJ7eUtMZmOqVZNldrZyG+DHLl2B5pg8Hbar3U=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.51.0 h1:aH/MMSoayAIhozZ7uJbVTT9QO/VhzBf0J9tymmmuC/U=
modernc.org/sqlite v1.51.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
ÂËtïƒ»&xO °ÿºº°@Ò[$rQ”³j¦Ù“ÄU!ÆÐ“T8%Mµ¦•ÁÑïw8-’H³ †ï³ �O[>LbgXá)�	i}b Ïd¸»ûx»Dè# chains.example.yaml â€” full per-network customization for the unified Lux
# explorer. Mount as /etc/explorer/chains.yaml or pass --config=chains.yaml.
#
# Every chain gets its own SQLite file under {data_dir}/{slug}/ and its own
# /v1/indexer/{slug}/* + /v1/graph/{slug}/{subgraph}/* routes. Branding is
# per-chain; the SPA reads it at runtime via /envs.js so a deploy can
# rebrand by remounting this file + assets â€” no image rebuild.

data_dir: /data
http_addr: :8090

# Default branding the SPA uses when the request hostname does not match a
# chain-specific brand below. Per-chain `brand:` overrides win.
brand_default:
  name: Lux Explorer
  coin: LUX
  accent_color: "#5cf"
  icon_url: /icon.svg
  logo_url: /logo.svg

# Cross-explorer link list rendered in the SPA's network switcher.
networks:
  - label: Mainnet
    domain: explorer.lux.network
    chain_id: 96369
  - label: Testnet
    domain: testnet.explorer.lux.network
    chain_id: 96368
  - label: Devnet
    domain: devnet.explorer.lux.network
    chain_id: 96370

chains:
  # â”€â”€ Lux native chains â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

  - slug: cchain
    name: Lux C-Chain
    chain_id: 96369
    type: evm
    rpc: ${LUX_CCHAIN_RPC:-https://api.lux.network/ext/bc/C/rpc}
    coin: LUX
    enabled: true
    default: true
    indexer: { poll_interval: 30s }
    graph:
      enabled: true
      subgraphs:
        - { name: amm,        schema: amm,        enabled: true }
        - { name: securities, schema: securities, enabled: false }
    brand:
      name: Lux C-Chain
      coin: LUX
      accent_color: "#5cf"

  - slug: pchain
    name: P-Chain
    type: linear
    rpc: ${LUX_RPC:-https://api.lux.network}/ext/bc/P
    coin: LUX
    enabled: true

  - slug: xchain
    name: X-Chain
    type: dag
    rpc: ${LUX_RPC:-https://api.lux.network}/ext/bc/X
    coin: LUX
    enabled: true

  - slug: achain
    name: A-Chain
    type: dag
    rpc: ${LUX_RPC:-https://api.lux.network}/ext/bc/A
    coin: LUX
    enabled: true

  - slug: bchain
    name: B-Chain
    type: dag
    rpc: ${LUX_RPC:-https://api.lux.network}/ext/bc/B
    coin: LUX
    enabled: true

  - slug: qchain
    name: Q-Chain
    type: dag
    rpc: ${LUX_RPC:-https://api.lux.network}/ext/bc/Q
    coin: LUX
    enabled: true

  - slug: tchain
    name: T-Chain
    type: dag
    rpc: ${LUX_RPC:-https://api.lux.network}/ext/bc/T
    coin: LUX
    enabled: true

  - slug: zchain
    name: Z-Chain
    type: dag
    rpc: ${LUX_RPC:-https://api.lux.network}/ext/bc/Z
    coin: LUX
    enabled: true

  - slug: kchain
    name: K-Chain
    type: dag
    rpc: ${LUX_RPC:-https://api.lux.network}/ext/bc/K
    coin: LUX
    enabled: true

  # â”€â”€ Lux subnet chains (EVM) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

  - slug: zoo
    name: Zoo
    chain_id: 200200
    type: evm
    rpc: ${ZOO_RPC:-https://api.lux.network/ext/bc/Zoo/rpc}
    coin: ZOO
    enabled: true
    indexer: { poll_interval: 30s }
    graph:
      enabled: true
      subgraphs:
        - { name: amm, schema: amm, enabled: true }
    brand:
      name: Zoo
      coin: ZOO
      accent_color: "#7c3aed"

  - slug: hanzo
    name: Hanzo
    chain_id: 36963
    type: evm
    rpc: ${HANZO_RPC:-https://api.lux.network/ext/bc/Hanzo/rpc}
    coin: HANZO
    enabled: true
    indexer: { poll_interval: 30s }
    graph:
      enabled: true
      subgraphs:
        - { name: amm, schema: amm, enabled: true }
    brand:
      name: Hanzo
      coin: HANZO
      accent_color: "#0b1220"

  - slug: spc
    name: SPC
    chain_id: 36911
    type: evm
    rpc: ${SPC_RPC:-https://api.lux.network/ext/bc/SPC/rpc}
    coin: SPC
    enabled: true
    indexer: { poll_interval: 30s }

  - slug: pars
    name: Pars
    chain_id: 494949
    type: evm
    rpc: ${PARS_RPC:-https://api.lux.network/ext/bc/Pars/rpc}
    coin: PARS
    enabled: true
    indexer: { poll_interval: 30s }

  - slug: dex
    name: Lux DEX
    type: evm
    rpc: ${LUX_RPC:-https://api.lux.network}/ext/bc/D/rpc
    coin: LUX
    enabled: true
    indexer: { poll_interval: 15s }
    graph:
      enabled: true
      subgraphs:
        - { name: amm,  schema: amm,  enabled: true }
        - { name: clob, schema: all,  enabled: true }

  # â”€â”€ External chains (bridge / MPC counterparties) â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€

  - slug: ethereum
    name: Ethereum
    chain_id: 1
    type: evm
    rpc: ${ETH_RPC:-https://eth.llamarpc.com}
    coin: ETH
    enabled: false
    graph:
      enabled: false
      subgraphs:
        - { name: uniswap-v3, schema: uniswap-v3, enabled: true }

  - slug: arbitrum
    name: Arbitrum One
    chain_id: 42161
    type: evm
    rpc: ${ARB_RPC:-https://arb1.arbitrum.io/rpc}
    coin: ETH
    enabled: false

  - slug: optimism
    name: Optimism
    chain_id: 10
    type: evm
    rpc: ${OP_RPC:-https://mainnet.optimism.io}
    coin: ETH
    enabled: false

  - slug: base
    name: Base
    chain_id: 8453
    type: evm
    rpc: ${BASE_RPC:-https://mainnet.base.org}
    coin: ETH
    enabled: false

  - slug: polygon
    name: Polygon
    chain_id: 137
    type: evm
    rpc: ${POLYGON_RPC:-https://polygon-rpc.com}
    coin: MATIC
    enabled: false

  - slug: bsc
    name: BNB Chain
    chain_id: 56
    type: evm
    rpc: ${BSC_RPC:-https://bsc-dataseed.binance.org}
    coin: BNB
    enabled: false

  - slug: avalanche
    name: Avalanche C-Chain
    chain_id: 43114
    type: evm
    rpc: ${AVAX_RPC:-https://api.avax.network/ext/bc/C/rpc}
    coin: AVAX
    enabled: false

  - slug: solana
    name: Solana
    type: solana
    rpc: ${SOL_RPC:-https://api.mainnet-beta.solana.com}
    coin: SOL
    enabled: false

  - slug: bitcoin
    name: Bitcoin
    type: bitcoin
    rpc: ${BTC_RPC:-}
    coin: BTC
    enabled: false

  - slug: cosmos
    name: Cosmos Hub
    type: cosmos
    rpc: ${ATOM_RPC:-https://cosmos-rpc.publicnode.com}
    coin: ATOM
    enabled: false
_Î—l²£x2Íípackage main

import (
	"fmt"
	"os"
	"regexp"

	"gopkg.in/yaml.v3"
)

// Config is the top-level explorer configuration loaded from chains.yaml.
type Config struct {
	DataDir      string        `yaml:"data_dir"`
	HTTPAddr     string        `yaml:"http_addr"`
	StaticDir    string        `yaml:"static_dir"` // overlay dir for SPA; empty = embedded only
	BrandDefault Brand         `yaml:"brand_default"`
	Networks     []Network     `yaml:"networks"`
	Chains       []ChainConfig `yaml:"chains"`
}

// ChainConfig defines a single chain plus per-chain customization. The
// admin/registry/mdns layer also uses this shape; fields beyond Slug/Name/
// ChainID/RPC/Type/Default are optional and apply when the chain is loaded
// from a config file.
type ChainConfig struct {
	Slug       string `json:"slug"     yaml:"slug"`
	Name       string `json:"name"     yaml:"name"`
	ChainID    int64  `json:"chain_id" yaml:"chain_id"`
	Type       string `json:"type"     yaml:"type"` // evm, dag, linear, solana, bitcoin, cosmos
	RPC        string `json:"rpc"      yaml:"rpc"`
	WS         string `json:"ws"       yaml:"ws"`
	CoinSymbol string `json:"coin"     yaml:"coin"`
	Enabled    bool   `json:"enabled"  yaml:"enabled"`
	Default    bool   `json:"default"  yaml:"default"`
	Source     string `json:"source"   yaml:"-"` // config, env, mdns, admin

	Indexer IndexerSettings `json:"indexer,omitempty" yaml:"indexer,omitempty"`
	Graph   GraphSettings   `json:"graph,omitempty"   yaml:"graph,omitempty"`
	Brand   *Brand          `json:"brand,omitempty"   yaml:"brand,omitempty"`
	Tokens  TokensFeatures  `json:"tokens,omitempty"  yaml:"tokens,omitempty"`
}

// IndexerSettings overrides indexer behaviour for a single chain.
type IndexerSettings struct {
	PollInterval string `yaml:"poll_interval" json:"poll_interval,omitempty"` // e.g. "30s"
	StartBlock   uint64 `yaml:"start_block"   json:"start_block,omitempty"`
}

// GraphSettings configures per-chain GraphQL subgraphs.
type GraphSettings struct {
	Enabled   bool       `yaml:"enabled"   json:"enabled"`
	Subgraphs []Subgraph `yaml:"subgraphs" json:"subgraphs,omitempty"`
}

// Subgraph mounts a single named GraphQL schema under
// /v1/graph/{chain-slug}/{subgraph-name}/. Schema is either a built-in name
// understood by github.com/luxfi/graph/engine.LoadBuiltin (amm, uniswap-v2,
// uniswap-v3, uniswap-v4, securities, fhe, etc.) or a path to a .graphql file.
type Subgraph struct {
	Name    string `yaml:"name"    json:"name"`
	Schema  string `yaml:"schema"  json:"schema"`
	Enabled bool   `yaml:"enabled" json:"enabled"`
}

// Brand is the per-chain (or default) UI branding shown by the SPA. Icon/Logo
// files are read from disk on every request so a deploy can swap them without
// rebuilding the binary.
type Brand struct {
	Name        string `yaml:"name"         json:"name,omitempty"`
	Coin        string `yaml:"coin"         json:"coin,omitempty"`
	AccentColor string `yaml:"accent_color" json:"accentColor,omitempty"`
	IconFile    string `yaml:"icon_file"    json:"-"`
	LogoFile    string `yaml:"logo_file"    json:"-"`
	IconURL     string `yaml:"icon_url"     json:"iconUrl,omitempty"`
	LogoURL     string `yaml:"logo_url"     json:"logoUrl,omitempty"`
}

// TokensFeatures controls which tokens the SPA highlights or hides.
type TokensFeatures struct {
	Featured  []string `yaml:"featured"   json:"featured,omitempty"`
	AllowList []string `yaml:"allow_list" json:"allowList,omitempty"`
	BlockList []string `yaml:"block_list" json:"blockList,omitempty"`
}

// Network is a peer explorer the SPA can link to in its network switcher.
type Network struct {
	Label   string `yaml:"label"    json:"label"`
	Domain  string `yaml:"domain"   json:"domain"`
	ChainID int64  `yaml:"chain_id" json:"chainId"`
}

// slugPattern restricts chain slugs to lowercase alphanumeric + hyphen so they
// remain safe in URL routes (/v1/indexer/{slug}/) and filesystem paths.
var slugPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,63}$`)

// LoadConfig reads chains.yaml, expands $ENV_VAR in RPC/WS, and validates slugs.
func LoadConfig(path string) (Config, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return Config{}, fmt.Errorf("read %s: %w", path, err)
	}
	var cfg Config
	if err := yaml.Unmarshal(data, &cfg); err != nil {
		return Config{}, fmt.Errorf("parse %s: %w", path, err)
	}
	for i := range cfg.Chains {
		cfg.Chains[i].RPC = os.Expand(cfg.Chains[i].RPC, os.Getenv)
		cfg.Chains[i].WS = os.Expand(cfg.Chains[i].WS, os.Getenv)
		if !slugPattern.MatchString(cfg.Chains[i].Slug) {
			return Config{}, fmt.Errorf("chain[%d]: invalid slug %q", i, cfg.Chains[i].Slug)
		}
	}
	return cfg, nil
}
†=
æ¾ûx¾7AÈpackage main

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"time"

	_ "github.com/mattn/go-sqlite3"

	"github.com/luxfi/graph/engine"
	graphidx "github.com/luxfi/graph/indexer"
	graphstor "github.com/luxfi/graph/storage"

	"github.com/luxfi/indexer/evm"
	"github.com/luxfi/indexer/explorer"
	idxstor "github.com/luxfi/indexer/storage"
)

// ChainSupervisor owns the per-chain indexer + graph goroutines plus the
// route dispatcher that maps /v1/indexer/{slug}/* and /v1/graph/{slug}/*
// requests to the correct handler. Routes are mutable at runtime: adding a
// chain spawns its workers, removing a chain cancels them.
type ChainSupervisor struct {
	dataDir string
	mu      sync.Mutex
	state   map[string]*chainState

	indexerRoutes sync.Map // map[slug]http.Handler
	graphRoutes   sync.Map // map[slug + "/" + subgraph]http.Handler
	defaultSlug   string   // serves /v1/indexer/* (no slug prefix)

	// realtimeHub is the explorer's RealtimeHub. When set (via
	// AttachRealtime), the supervisor wires each per-chain EVM indexer's
	// Subscriber.OnBroadcast callback so block events flow into the hub
	// â†’ out to WebSocket + SSE subscribers. Nil = no live broadcast.
	realtimeHub *RealtimeHub
}

type chainState struct {
	cfg    ChainConfig
	cancel context.CancelFunc
	wg     sync.WaitGroup
}

// NewChainSupervisor creates a supervisor rooted at dataDir.
func NewChainSupervisor(dataDir string) *ChainSupervisor {
	return &ChainSupervisor{
		dataDir: dataDir,
		state:   make(map[string]*chainState),
	}
}

// AttachRealtime wires per-chain block events into the explorer's
// RealtimeHub. Called once from main.go after both the supervisor and
// the registry's hub exist. Subsequent chain spawns (runChain below)
// install the Subscriber.OnBroadcast callback automatically.
func (s *ChainSupervisor) AttachRealtime(h *RealtimeHub) {
	s.realtimeHub = h
}

// MountRoutes installs the /v1/indexer and /v1/graph dispatchers on mux. The
// dispatchers look up per-chain handlers from the supervisor's route maps so
// chains added at runtime become reachable without re-mounting.
func (s *ChainSupervisor) MountRoutes(mux *http.ServeMux) {
	mux.Handle("/v1/indexer/", http.HandlerFunc(s.dispatchIndexer))
	mux.Handle("/v1/explorer/", http.HandlerFunc(s.dispatchExplorerLegacy))
	mux.Handle("/v1/graph/", http.HandlerFunc(s.dispatchGraph))
}

// SetDefaultSlug picks which chain serves the unprefixed /v1/indexer/* route.
func (s *ChainSupervisor) SetDefaultSlug(slug string) {
	s.mu.Lock()
	s.defaultSlug = slug
	s.mu.Unlock()
}

// start spawns workers for cfg. Idempotent: starting an already-running slug
// is a no-op.
func (s *ChainSupervisor) start(cfg ChainConfig) {
	s.mu.Lock()
	if _, ok := s.state[cfg.Slug]; ok {
		s.mu.Unlock()
		return
	}
	ctx, cancel := context.WithCancel(context.Background())
	st := &chainState{cfg: cfg, cancel: cancel}
	s.state[cfg.Slug] = st
	if cfg.Default && s.defaultSlug == "" {
		s.defaultSlug = cfg.Slug
	}
	s.mu.Unlock()

	st.wg.Add(1)
	go func() {
		defer st.wg.Done()
		s.runChain(ctx, cfg)
	}()

	if cfg.Graph.Enabled {
		for _, sg := range cfg.Graph.Subgraphs {
			if !sg.Enabled {
				continue
			}
			sg := sg
			st.wg.Add(1)
			go func() {
				defer st.wg.Done()
				s.runSubgraph(ctx, cfg, sg)
			}()
		}
	}
}

// stop cancels and waits for all workers belonging to slug.
func (s *ChainSupervisor) stop(slug string) {
	s.mu.Lock()
	st, ok := s.state[slug]
	if !ok {
		s.mu.Unlock()
		return
	}
	delete(s.state, slug)
	if s.defaultSlug == slug {
		s.defaultSlug = ""
	}
	s.mu.Unlock()

	st.cancel()
	st.wg.Wait()

	s.indexerRoutes.Delete(slug)
	s.graphRoutes.Range(func(k, _ any) bool {
		if key, ok := k.(string); ok && strings.HasPrefix(key, slug+"/") {
			s.graphRoutes.Delete(k)
		}
		return true
	})
	log.Printf("[supervisor] stopped %s", slug)
}

// Wait blocks until ctx is done, then cancels every chain and waits for
// goroutines to drain.
func (s *ChainSupervisor) Wait(ctx context.Context) {
	<-ctx.Done()
	s.mu.Lock()
	slugs := make([]string, 0, len(s.state))
	for k := range s.state {
		slugs = append(slugs, k)
	}
	s.mu.Unlock()
	for _, slug := range slugs {
		s.stop(slug)
	}
}

// runChain runs one chain's indexer to completion or context cancel and then
// mounts the standalone explorer API once the SQLite schema is ready.
func (s *ChainSupervisor) runChain(ctx context.Context, cfg ChainConfig) {
	chainDir := filepath.Join(s.dataDir, cfg.Slug)
	if err := os.MkdirAll(chainDir, 0755); err != nil {
		log.Printf("[%s] mkdir: %v", cfg.Slug, err)
		return
	}
	store, err := idxstor.NewUnified(idxstor.DefaultUnifiedConfig(chainDir))
	if err != nil {
		log.Printf("[%s] storage: %v", cfg.Slug, err)
		return
	}
	defer store.Close()
	if err := store.Init(ctx); err != nil {
		log.Printf("[%s] storage init: %v", cfg.Slug, err)
		return
	}
	dbPath := filepath.Join(chainDir, "query", "indexer.db")
	log.Printf("[%s] indexing %s (%s) -> %s", cfg.Slug, cfg.Name, cfg.CoinSymbol, dbPath)

	go s.mountIndexerAPI(ctx, cfg, dbPath)

	switch cfg.Type {
	case "", "evm":
		poll := 30 * time.Second
		if cfg.Indexer.PollInterval != "" {
			if d, err := time.ParseDuration(cfg.Indexer.PollInterval); err == nil {
				poll = d
			}
		}
		idx, err := evm.NewIndexer(evm.Config{
			ChainName:    cfg.Name,
			ChainID:      cfg.ChainID,
			RPCEndpoint:  cfg.RPC,
			HTTPPort:     0,
			PollInterval: poll,
			StartBlock:   cfg.Indexer.StartBlock,
		}, store)
		if err != nil {
			log.Printf("[%s] evm indexer: %v", cfg.Slug, err)
			return
		}
		// Live broadcast bridge â€” when the supervisor has been given a
		// RealtimeHub (see AttachRealtime), every block the indexer
		// commits is re-published into the hub on the chain's slug. The
		// hub then fans out to WebSocket and SSE subscribers (see
		// realtime.go + realtime_sse.go). slug captured by value so the
		// closure doesn't see a mutated `cfg` if the supervisor loops.
		if s.realtimeHub != nil && idx.Subscriber() != nil {
			slug := cfg.Slug
			hub := s.realtimeHub
			idx.Subscriber().OnBroadcast = func(eventType string, data any) {
				hub.Broadcast(eventType, slug, data)
			}
		}
		if err := idx.Init(ctx); err != nil {
			log.Printf("[%s] evm init: %v", cfg.Slug, err)
			return
		}
		if err := idx.Run(ctx); err != nil && ctx.Err() == nil {
			log.Printf("[%s] evm run: %v", cfg.Slug, err)
		}
	default:
		log.Printf("[%s] %s indexer: passive (no chain-specific implementation)", cfg.Slug, cfg.Type)
		<-ctx.Done()
	}
}

// mountIndexerAPI waits for the per-chain SQLite schema to settle, then
// publishes the standalone explorer API in the indexer/explorer route maps.
func (s *ChainSupervisor) mountIndexerAPI(ctx context.Context, cfg ChainConfig, dbPath string) {
	apiPrefix := "/v1/indexer/" + cfg.Slug
	deadline := time.Now().Add(5 * time.Minute)
	for {
		select {
		case <-ctx.Done():
			return
		case <-time.After(time.Second):
		}
		if _, err := os.Stat(dbPath); err != nil {
			if time.Now().After(deadline) {
				log.Printf("[%s] api waiting for %s: %v", cfg.Slug, dbPath, err)
				deadline = time.Now().Add(5 * time.Minute)
			}
			continue
		}
		if cfg.Type == "" || cfg.Type == "evm" {
			db, err := sql.Open("sqlite3", "file:"+dbPath+"?mode=ro")
			if err == nil {
				var n int
				_ = db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='evm_blocks'").Scan(&n)
				db.Close()
				if n == 0 {
					if time.Now().After(deadline) {
						log.Printf("[%s] api waiting for evm_blocks", cfg.Slug)
						deadline = time.Now().Add(5 * time.Minute)
					}
					continue
				}
			}
		}
		srv, err := explorer.NewStandaloneServer(explorer.Config{
			IndexerDBPath: dbPath,
			ChainID:       cfg.ChainID,
			ChainName:     cfg.Name,
			CoinSymbol:    cfg.CoinSymbol,
			APIPrefix:     apiPrefix,
		})
		if err != nil {
			if time.Now().After(deadline) {
				log.Printf("[%s] api: %v", cfg.Slug, err)
				deadline = time.Now().Add(5 * time.Minute)
			}
			continue
		}
		s.indexerRoutes.Store(cfg.Slug, srv.Handler())
		log.Printf("[%s] api mounted at %s/*", cfg.Slug, apiPrefix)
		return
	}
}

// runSubgraph spawns a graph indexer + engine for a single subgraph and
// publishes its handler at /v1/graph/{chain-slug}/{subgraph-name}/.
func (s *ChainSupervisor) runSubgraph(ctx context.Context, cfg ChainConfig, sg Subgraph) {
	dir := filepath.Join(s.dataDir, cfg.Slug, "graph", sg.Name)
	if err := os.MkdirAll(dir, 0755); err != nil {
		log.Printf("[%s/%s] mkdir: %v", cfg.Slug, sg.Name, err)
		return
	}
	store, err := graphstor.New(dir)
	if err != nil {
		log.Printf("[%s/%s] storage: %v", cfg.Slug, sg.Name, err)
		return
	}
	defer store.Close()
	if err := store.Init(ctx); err != nil {
		log.Printf("[%s/%s] storage init: %v", cfg.Slug, sg.Name, err)
		return
	}

	idx := graphidx.New(cfg.RPC, store)
	go func() {
		if err := idx.Run(ctx); err != nil && ctx.Err() == nil {
			log.Printf("[%s/%s] indexer: %v", cfg.Slug, sg.Name, err)
		}
	}()

	eng := engine.New(store, &engine.Config{
		MaxQueryDepth:  10,
		MaxResultSize:  1 << 20,
		QueryTimeoutMs: 30000,
	})
	schema := sg.Schema
	if schema == "" {
		schema = "amm"
	}
	if err := eng.LoadBuiltin(schema); err != nil {
		log.Printf("[%s/%s] schema %q: %v", cfg.Slug, sg.Name, schema, err)
		return
	}

	prefix := fmt.Sprintf("/v1/graph/%s/%s", cfg.Slug, sg.Name)
	mux := http.NewServeMux()
	gqlHandler := func(w http.ResponseWriter, r *http.Request) {
		r.Body = http.MaxBytesReader(w, r.Body, 10<<20)
		var req engine.Request
		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
			http.Error(w, `{"errors":[{"message":"invalid JSON"}]}`, http.StatusBadRequest)
			return
		}
		resp := eng.Execute(r.Context(), &req)
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(resp)
	}
	statusHandler := func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		st := idx.Status()
		fmt.Fprintf(w, `{"status":"ok","block":%d,"indexed":%d}`, st.LatestBlock, st.IndexedEvents)
	}
	mux.HandleFunc("POST "+prefix+"/graphql", gqlHandler)
	mux.HandleFunc("GET "+prefix+"/graphql", eng.HandleGraphiQL)
	mux.HandleFunc("POST "+prefix+"/ql", gqlHandler)
	mux.HandleFunc("GET "+prefix+"/ql", eng.HandleGraphiQL)
	mux.HandleFunc("GET "+prefix+"/health", statusHandler)

	s.graphRoutes.Store(cfg.Slug+"/"+sg.Name, http.Handler(mux))
	log.Printf("[%s/%s] graph mounted at %s/{graphql,ql,health} (schema=%s)",
		cfg.Slug, sg.Name, prefix, schema)

	<-ctx.Done()
}

// dispatchIndexer routes /v1/indexer/{slug}/* and /v1/indexer/* (default
// chain) to the per-chain handler installed by mountIndexerAPI.
//
// Edge case the older code got wrong: when the first path segment matches
// the slug regex but is NOT a registered chain (e.g. `main-page` in
// `/v1/indexer/main-page/blocks`), splitSlug peels it off as a "slug" and
// the dispatcher then forwarded only the REMAINING segments to the
// default chain. That turned `/v1/indexer/main-page/blocks` into
// `/v1/indexer/evm/blocks` server-side â€” a completely different endpoint
// with a different response shape (paginated envelope vs. raw array). The
// SPA's `?.slice(0,8)` on the home page's Latest Blocks widget tripped
// over the envelope and threw "o.slice is not a function".
//
// Fix: when the parsed slug isn't a real chain, re-join it with the rest
// before delegating, so the unrecognized segment is preserved as part of
// the path under the default chain.
func (s *ChainSupervisor) dispatchIndexer(w http.ResponseWriter, r *http.Request) {
	slug, rest, ok := splitSlug(r.URL.Path, "/v1/indexer/")
	if !ok {
		s.serveDefault(w, r, "")
		return
	}
	if h, found := s.indexerRoutes.Load(slug); found {
		h.(http.Handler).ServeHTTP(w, r)
		return
	}
	fullTail := slug
	if rest != "" {
		fullTail = slug + "/" + rest
	}
	s.serveDefault(w, r, fullTail)
}

// serveDefault forwards an unprefixed /v1/indexer/* request to the default
// chain. The standalone server is mounted under its slug, so we rewrite the
// URL to include the slug before delegating.
func (s *ChainSupervisor) serveDefault(w http.ResponseWriter, r *http.Request, rest string) {
	s.mu.Lock()
	def := s.defaultSlug
	s.mu.Unlock()
	if def == "" {
		http.Error(w, `{"error":"no default chain"}`, http.StatusNotFound)
		return
	}
	h, ok := s.indexerRoutes.Load(def)
	if !ok {
		http.Error(w, `{"error":"chain not ready"}`, http.StatusServiceUnavailable)
		return
	}
	r2 := r.Clone(r.Context())
	r2.URL.Path = "/v1/indexer/" + def + "/" + strings.TrimPrefix(rest, "/")
	h.(http.Handler).ServeHTTP(w, r2)
}

// dispatchExplorerLegacy preserves the older /v1/explorer/{slug}/* prefix
// for clients that haven't migrated to /v1/indexer/.
func (s *ChainSupervisor) dispatchExplorerLegacy(w http.ResponseWriter, r *http.Request) {
	slug, rest, ok := splitSlug(r.URL.Path, "/v1/explorer/")
	if !ok {
		http.NotFound(w, r)
		return
	}
	h, found := s.indexerRoutes.Load(slug)
	if !found {
		http.NotFound(w, r)
		return
	}
	r2 := r.Clone(r.Context())
	r2.URL.Path = "/v1/indexer/" + slug + "/" + strings.TrimPrefix(rest, "/")
	h.(http.Handler).ServeHTTP(w, r2)
}

// dispatchGraph routes /v1/graph/{slug}/{subgraph}/* to the matching engine.
func (s *ChainSupervisor) dispatchGraph(w http.ResponseWriter, r *http.Request) {
	rest := strings.TrimPrefix(r.URL.Path, "/v1/graph/")
	if rest == r.URL.Path {
		http.NotFound(w, r)
		return
	}
	parts := strings.SplitN(rest, "/", 3)
	if len(parts) < 2 {
		http.NotFound(w, r)
		return
	}
	key := parts[0] + "/" + parts[1]
	h, ok := s.graphRoutes.Load(key)
	if !ok {
		http.NotFound(w, r)
		return
	}
	h.(http.Handler).ServeHTTP(w, r)
}

// splitSlug returns the chain slug and the remainder of the path after a
// known prefix (e.g. "/v1/indexer/"). Returns ok=false if the path does not
// match the prefix or contains no slug.
func splitSlug(path, prefix string) (slug, rest string, ok bool) {
	if !strings.HasPrefix(path, prefix) {
		return "", "", false
	}
	tail := strings.TrimPrefix(path, prefix)
	if tail == "" {
		return "", "", false
	}
	parts := strings.SplitN(tail, "/", 2)
	if !slugPattern.MatchString(parts[0]) {
		return "", "", false
	}
	if len(parts) == 1 {
		return parts[0], "", true
	}
	return parts[0], parts[1], true
}
«ÿÉª;xºEü40000 .github ó;:ø5¾ŒÌ„¡Û,
�ö4‰8µ100644 .gitignore ¬ž'ê	‰×Ã€Ê!6ÐÂœã�¶>100644 CLAUDE.md ½`ŸÚîþÙ(z52æv#ºècæ100644 Dockerfile ö\\‰ÌQ×ìj‹ìqBçÓÆ„qŸ100644 LICENSE þ<¬:¸Mµ¶¥dŽPRÉø›100644 LICENSING.md __"]À+Ò°?�ãÿ¯®�×YÙ120000 LLM.md hëœôSÐúÝóªÊìsWé{¨é100644 README.md oƒ&;bk…èÉÜ›ƒÛV›vÿ4100644 chains.example.yaml Ò[$rQ”³j¦Ù“ÄU!ÆÐ100644 compose.yml àÓõ>”)S¢7>ðr¹Âš çÑ100644 config.go %Mµ¦•ÁÑïw8-’H³ †ï100644 frontend.go Y kVa‹Bð2�é8®€qÂ·100644 frontend_test.go (xN2}à­«·GÀR]#î|…ã+100644 go.mod E5ug¡)BLF±–9—,q¤…¦Þ100644 go.sum |Sï…)B’(2—(ŽÆÚ‚Ü¹I100644 main.go ßGª/× Ïsý´÷ÛÜ�®Ìµ100644 mdns.go _Öˆm2mÀWw1<&vÜ¢XN°100644 realtime.go zY¥‘Ÿ¯ª»W/rb4gÓŒÝë100644 realtime_sse.go Z*‚ø’Ú¶%éáe�&…Ê100644 realtime_test.go ˆãá2ƒ¸-£«ÒðCŒìGãMË"100644 registry.go ÷o:Åº±Z©jø¬šß+F)p�L100644 registry_test.go úäYs²¸5ÀZ ö½C±40000 scripts ÞB:¤ôþºN~=w$�=„ˆ^+E²40000 static …V,êôÔ‚ã•yíAñùXt¦oo100644 supervisor.go �O[>LbgXá)�	i}b ô—zKî†Gx~ �ÿºº�k¤TMJ+“ ñËP¾½³�«±“ @…ý“ÜÖ| b„-ÒRlAFW’=ç100644 frontend_test.go Ñê/ìÅ˜f	¢Gîh!dæÕÖ÷³óŠ¡YÀYïéÙój¨³Píž%+û?“‘)�C8³ë€x+ Ôÿºº"40000 .github „=rP·ÙÃù!F¡ÃZõ€=vf±"˜Kw…9xœsöquqÕËM ¹®¯xO °ÿ100644 actionlint.yaml !U¡išQà›â‡Fµ°˜!>F40000 workflows 4:õÌ9ž?H‹,Qs�x÷áßÆ\yŽé\x æÿOO�;®…–“M9æ¯ßg|çR%¬“æŽâé'x æÿOO�;uÛzµ]8œÇÖ4m“©0WÊ­‚”
ëé'x æÿOO�;©’ó'ôMaÂ:Cu:íëjh|ã
Né'x æÿOO�;ùHzYÔÿÌ¥?•øü„{Um·šoFé'x æÿOO�;ÕY ƒ\`¶0èÕè®†�î›òÍ† šé'x æÿOO�;báUíå_þ`½¾,vÜ¿F€Š¬˜Ñ…é'x æÿOO�;Ô¤�èvÍß«­4k­åo*ï™ÞÜé'x æÿOO�;–KÞ"0Þëpj¦;(dŒÇ
ñ8î
8é'x æÿOO�;—vÔÁh˜³&½äÄ:U–QÁ¨‘�^¦x& Ùÿ100644 docker.yml ‘&bceÿ™Âõ€J‰ÙÄàéê£º`x
õùname: Docker
on:
  workflow_dispatch:
  push:
    tags: ['v*']

permissions:
  contents: read
  packages: write
  id-token: write

# amd64-only on luxfi's native ARC pool â€” the SPA build (Next.js + 8GB
# heap) + Go cgo-sqlite Docker build under QEMU arm64 emulation pushes
# build time from ~5 min to ~2 hours. Cluster runners are amd64; Mac
# devs use OrbStack which has its own arm64 base image story. arm64
# rebuilds can come back via a separate matrix entry on a native
# arm64 runner â€” not via QEMU.
jobs:
  docker:
    runs-on: lux-build
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
        with:
          driver: docker-container
          driver-opts: network=host
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/luxfi/explorer
          flavor: latest=auto
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha,prefix=sha-
      - uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64
          labels: ${{ steps.meta.outputs.labels }}
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha,scope=explorer
          cache-to: type=gha,scope=explorer,mode=max
          provenance: false
          push: true
¨Máê¹ŒxÉ06Ïpackage main

import (
	"bytes"
	"embed"
	"encoding/json"
	"fmt"
	"io/fs"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
)

// staticFS is the embedded SPA. The Dockerfile populates static/ from a
// luxfi/explore build before the Go build runs; for local dev a placeholder
// index.html is committed so go:embed always succeeds.
//
//go:embed all:static
var staticFS embed.FS

// Frontend serves the embedded SPA, runtime config (/envs.js), and per-host
// brand assets (/icon.svg, /logo.svg). Brand assets read from disk on every
// request so a deploy can swap them without rebuilding the binary.
//
// SPA override: if cfg.StaticDir (or $EXPLORER_STATIC_DIR) points at an
// existing directory, file lookups prefer the on-disk copy over the
// embedded FS. This lets a ConfigMap-mounted SPA replace the embedded
// 60-byte placeholder without a binary rebuild â€” useful when the
// upstream `explore` Next.js app hasn't shipped a static export yet but
// we want a richer in-cluster default than `<div id="root"></div>`.
type Frontend struct {
	cfg      Config
	registry *ChainRegistry
	root     fs.FS    // embedded SPA (always present)
	overlay  fs.FS    // disk overlay (nil if cfg.StaticDir not set)
	index    []byte   // resolved index.html (overlay > embedded > builtin)
}

// NewFrontend returns a frontend handler bound to a config and chain registry.
func NewFrontend(cfg Config, r *ChainRegistry) (*Frontend, error) {
	root, err := fs.Sub(staticFS, "static")
	if err != nil {
		return nil, err
	}
	f := &Frontend{cfg: cfg, registry: r, root: root}

	dir := cfg.StaticDir
	if dir == "" {
		dir = os.Getenv("EXPLORER_STATIC_DIR")
	}
	if dir != "" {
		if st, err := os.Stat(dir); err == nil && st.IsDir() {
			f.overlay = os.DirFS(dir)
		}
	}

	if data := f.readStatic("index.html"); data != nil {
		f.index = data
	} else {
		f.index = []byte("<!doctype html><title>Explorer</title>")
	}
	return f, nil
}

// readStatic returns the bytes for `name` from the overlay if present,
// otherwise from the embedded FS. Returns nil if neither has the file.
func (f *Frontend) readStatic(name string) []byte {
	if f.overlay != nil {
		if data, err := fs.ReadFile(f.overlay, name); err == nil {
			return data
		}
	}
	if data, err := fs.ReadFile(f.root, name); err == nil {
		return data
	}
	return nil
}

// Mount installs / (SPA), /envs.js, /icon.svg, /logo.svg on mux.
func (f *Frontend) Mount(mux *http.ServeMux) {
	mux.HandleFunc("GET /envs.js", f.handleEnvs)
	mux.HandleFunc("GET /icon.svg", f.handleIcon)
	mux.HandleFunc("GET /logo.svg", f.handleLogo)
	mux.HandleFunc("/", f.handleSPA)
}

// handleSPA serves embedded static assets and falls back to index.html for
// any path the SPA owns (client-side routing).
//
// HEAD-method note: the bundled SPA opens EventSource (Server-Sent Events)
// channels for live block / token / tx streams and pre-flights each URL
// with a HEAD request:
//
//	fetch(url, {method: 'HEAD'}).then(r => r.ok ? openEventSource() : disable());
//
// If we serve a 200 (because of the SPA fallback to index.html), the SPA
// proceeds to open the EventSource â€” which then immediately aborts because
// the response body is text/html, not text/event-stream. Browser logs
// "EventSource's response has a MIME type ('text/html') that is not
// 'text/event-stream'" for every channel on every page load â€” noisy and
// disables the channel anyway.
//
// Fix: return 404 for HEAD requests on the SPA-fallback path. Real assets
// (anything in `f.root`) still serve normally on HEAD; only the index.html
// fallback gets the 404 so the SSE pre-flight cleanly fails and the SPA
// disables that channel on its own. Visible browser load is unchanged
// because the SPA only HEAD-probes its SSE endpoints, never the SPA URL
// itself.
func (f *Frontend) handleSPA(w http.ResponseWriter, r *http.Request) {
	p := strings.TrimPrefix(r.URL.Path, "/")
	if p == "" {
		if r.Method == http.MethodHead {
			http.NotFound(w, r)
			return
		}
		f.writeIndex(w)
		return
	}
	// Overlay wins if it has the file (any non-directory entry); falls
	// back to embedded; falls back to index.html (SPA-routing).
	if f.overlay != nil {
		if file, err := f.overlay.Open(p); err == nil {
			st, statErr := file.Stat()
			file.Close()
			if statErr == nil && !st.IsDir() {
				http.ServeFileFS(w, r, f.overlay, p)
				return
			}
		}
	}
	file, err := f.root.Open(p)
	if err != nil {
		if r.Method == http.MethodHead {
			http.NotFound(w, r)
			return
		}
		f.writeIndex(w)
		return
	}
	defer file.Close()
	stat, err := file.Stat()
	if err != nil || stat.IsDir() {
		if r.Method == http.MethodHead {
			http.NotFound(w, r)
			return
		}
		f.writeIndex(w)
		return
	}
	http.ServeFileFS(w, r, f.root, p)
}

func (f *Frontend) writeIndex(w http.ResponseWriter) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.Header().Set("Cache-Control", "no-cache")
	w.Write(f.index)
}

// handleEnvs returns runtime config the SPA reads instead of build-time env
// vars. This is what makes per-network customization possible without a
// rebuild: deploy the same image, mount a different chains.yaml, get a
// different brand + chain list.
func (f *Frontend) handleEnvs(w http.ResponseWriter, r *http.Request) {
	hostBrand := f.brandForHost(r.Host)
	hostSlug := f.chainSlugForHost(r.Host)
	chains := f.chainListJSON()
	// When the host pins a specific chain (e.g. explore.zoo.network â†’
	// zoo, explorer.zoo-test.network â†’ zoo), promote that chain to
	// default and demote any other config-level default. The SPA's chain
	// switcher honours the "default" flag.
	if hostSlug != "" {
		for i := range chains {
			if chains[i]["slug"] == hostSlug {
				chains[i]["default"] = true
			} else {
				chains[i]["default"] = false
			}
		}
	}

	env := map[string]any{
		"VITE_CHAINS":   chains,
		"VITE_NETWORKS": f.cfg.Networks,
		"VITE_BRAND":    hostBrand,
	}
	body, _ := json.Marshal(env)

	var buf bytes.Buffer
	buf.WriteString("window.ENV = ")
	buf.Write(body)
	buf.WriteString(";\n")

	w.Header().Set("Content-Type", "application/javascript")
	w.Header().Set("Cache-Control", "no-cache")
	w.Write(buf.Bytes())
}

// handleIcon serves the per-host or default icon.svg from disk.
func (f *Frontend) handleIcon(w http.ResponseWriter, r *http.Request) {
	f.serveBrandFile(w, r, func(b Brand) string { return b.IconFile }, "icon.svg")
}

// handleLogo serves the per-host or default logo.svg from disk.
func (f *Frontend) handleLogo(w http.ResponseWriter, r *http.Request) {
	f.serveBrandFile(w, r, func(b Brand) string { return b.LogoFile }, "logo.svg")
}

// serveBrandFile resolves the file path from the matched brand (per-chain or
// default) and sends it. Falls back to the on-disk overlay (if configured)
// then the embedded SPA's named asset.
func (f *Frontend) serveBrandFile(w http.ResponseWriter, r *http.Request, pick func(Brand) string, fallback string) {
	brand := f.brandStruct(r.Host)
	if path := pick(brand); path != "" {
		if data, err := os.ReadFile(path); err == nil {
			w.Header().Set("Content-Type", contentTypeFor(path))
			w.Header().Set("Cache-Control", "public, max-age=300")
			w.Write(data)
			return
		}
	}
	if data := f.readStatic(fallback); data != nil {
		w.Header().Set("Content-Type", contentTypeFor(fallback))
		w.Header().Set("Cache-Control", "public, max-age=300")
		w.Write(data)
		return
	}
	http.NotFound(w, r)
}

// brandForHost returns a JSON-shaped brand for /envs.js. It strips file paths.
func (f *Frontend) brandForHost(host string) map[string]any {
	b := f.brandStruct(host)
	return map[string]any{
		"name":        b.Name,
		"coin":        b.Coin,
		"accentColor": b.AccentColor,
		"iconUrl":     b.IconURL,
		"logoUrl":     b.LogoURL,
	}
}

// brandStruct returns the Brand for a request host: a chain whose name or
// slug matches the hostname wins; otherwise the global default.
//
// See hostMatchesSlug for the list of supported hostname shapes.
func (f *Frontend) brandStruct(host string) Brand {
	if c, ok := f.matchChain(host); ok && c.Brand != nil {
		return *c.Brand
	}
	return f.cfg.BrandDefault
}

// chainSlugForHost returns the slug of the chain pinned by the request host,
// or "" if the host does not name a chain. Used by handleEnvs to promote a
// per-host chain to the SPA's default selection.
func (f *Frontend) chainSlugForHost(host string) string {
	if c, ok := f.matchChain(host); ok {
		return c.Slug
	}
	return ""
}

// matchChain finds the first configured chain whose slug appears in the
// request host. Matches these hostname shapes, in order of specificity:
//   - "{slug}"                             â€” bare slug (curl localhost)
//   - "{slug}.tld..."                      â€” {slug}.lux.network, {slug}.hanzo.ai
//   - "explore-{slug}.tld..."              â€” explore-hanzo.lux.network
//   - "explorer-{slug}.tld..."             â€” explorer-hanzo.lux.network
//   - "api-explore-{slug}.tld..."          â€” api-explore-hanzo.lux.network
//   - "explore.{slug}.tld..."              â€” explore.hanzo.ai, explore.zoo.ngo
//   - "explorer.{slug}.tld..."             â€” explorer.hanzo.ai, explorer.zoo.network
//   - "api-explore.{slug}.tld..."          â€” api-explore.hanzo.ai
//   - "explore-{slug}-{env}.tld..."        â€” explore-hanzo-test.lux.network
//   - "explorer-{slug}-{env}.tld..."       â€” explorer-hanzo-test.lux.network
//   - "api-explore-{slug}-{env}.tld..."    â€” api-explore-hanzo-dev.lux.network
//   - "explore.{slug}-{env}.tld..."        â€” explore.zoo-test.network
//   - "explorer.{slug}-{env}.tld..."       â€” explorer.zoo-test.network
//   - "api-explore.{slug}-{env}.tld..."    â€” api-explore.zoo-test.network
//
// First chain match wins on ambiguous hosts.
func (f *Frontend) matchChain(host string) (ChainConfig, bool) {
	host = strings.ToLower(strings.SplitN(host, ":", 2)[0])
	for _, c := range f.cfg.Chains {
		if hostMatchesSlug(host, c.Slug) {
			return c, true
		}
	}
	return ChainConfig{}, false
}

// hostMatchesSlug is the pure string predicate for chainâ†”host matching.
// Kept separate from matchChain so unit tests can cover it directly.
func hostMatchesSlug(host, slug string) bool {
	if slug == "" {
		return false
	}
	if host == slug {
		return true
	}
	// Direct slug-as-subdomain: zoo.lux.network, hanzo.ai.
	if strings.HasPrefix(host, slug+".") || strings.HasPrefix(host, slug+"-") {
		return true
	}
	// Brand-owned domains: explore.zoo.network, explorer.zoo-test.network,
	// api-explore.zoo.ngo, etc. Each "explore"/"explorer" prefix combines
	// with "." or "-" as the slug separator.
	prefixes := []string{
		"explore-", "explorer-",
		"api-explore-", "api-explorer-",
		"explore.", "explorer.",
		"api-explore.", "api-explorer.",
	}
	for _, p := range prefixes {
		// {prefix}{slug}.tld  â€” e.g. explore.zoo.network, explore-zoo.lux.network
		if strings.HasPrefix(host, p+slug+".") {
			return true
		}
		// {prefix}{slug}-{env}.tld â€” e.g. explore-zoo-test.lux.network,
		// explorer.zoo-test.network
		if strings.HasPrefix(host, p+slug+"-") {
			return true
		}
	}
	return false
}

// chainListJSON returns a SPA-friendly chain list from the live registry,
// not the static config â€” chains added via mDNS/admin show up immediately.
var chainListMu sync.Mutex

func (f *Frontend) chainListJSON() []map[string]any {
	chainListMu.Lock()
	defer chainListMu.Unlock()
	out := []map[string]any{}
	if f.registry == nil {
		return out
	}
	for _, c := range f.registry.List() {
		out = append(out, map[string]any{
			"slug":    c.Slug,
			"name":    c.Name,
			"chainId": c.ChainID,
			"rpc":     c.RPC,
			"coin":    c.CoinSymbol,
			"type":    c.Type,
			"default": c.Default,
		})
	}
	return out
}

// contentTypeFor maps a file extension to a Content-Type, defaulting to
// application/octet-stream.
func contentTypeFor(path string) string {
	switch strings.ToLower(filepath.Ext(path)) {
	case ".svg":
		return "image/svg+xml"
	case ".png":
		return "image/png"
	case ".jpg", ".jpeg":
		return "image/jpeg"
	case ".webp":
		return "image/webp"
	case ".ico":
		return "image/x-icon"
	default:
		return "application/octet-stream"
	}
}

// fingerprint returns a short, stable identifier for the embedded asset
// bundle, useful for cache-busting log lines at startup.
func fingerprint() string {
	root, err := fs.Sub(staticFS, "static")
	if err != nil {
		return "0"
	}
	count := 0
	_ = fs.WalkDir(root, ".", func(_ string, _ fs.DirEntry, _ error) error {
		count++
		return nil
	})
	return fmt.Sprintf("assets=%d", count)
}
—®íîïÃàWx?ÀãÉa”<�net/http"
	"net/http/httptest‘T&testing"
)

// TestFrontendStaticOverlay verifies that NewFrontend prefers the on-disk
// static dir over the embedded FS when cfg.StaticDir points at a real
// directory. The overlay use-case: ConfigMap-mounting a richer SPA shell
// into an existing image without rebuilding.
func TestFrontendStaticOverlay(t *testing.T) {
	dir := t.TempDir()
	want := "<!doctype html><title>OVERLAY</title>"
	if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte(want), 0644); err != nil {
		t.Fatal(err)
	}

	cfmg := Config{StaticDir: dir, BrandDefault: Brand{Name: "Test"}}
	f, err := NewFrontend(cfg, NewChainRegistry()“t.Fatal(err)
	}

	mux := http.NewServeMux()
	f.Mount(mux)

	req := httptest.NewRequest("GET", "/", nil)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)

	got := rec.Body.String()
	if !strings.Contains(got, "OVERLAY") {
		t.Fatalf("expected overlay index.html, got %q", got)
	}
}

// TestFrontendNoOverlay verifies that with no StaticDir, the embedded
// index.html is served (current behaviour â€” pre-overlay).
func TestFrontendNoOverlay(t *testing.T) {
	cfg := Config{BrandDefault: Brand{Name: "Tes3t"}}
	f, err := NewFrontend(cfg, NewChainRegistry()“t.Fatal(err)
	}
	if f.overlay != nil {
		t.Fatal("expected nil overlay when StaticDir empty")
	}

	mux := http.NewServeMux()
	f.Mount(mux)

	req := httptest.NewRequest("GET", "/", nil)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)

	got := rec.Body.String()
	if !strings.Contains(got, "doctype html") {
		t.Fatalf("expected fallback index.html, got %q", got)
	}
}

// TestFrontendOverlayMissingFileFallsBack covers the case where the
// overlay dir exists but a particular file is missing â€” the request
// falls through to the embedded FS (and ultimately to index.html for
// SPA-routing).
func TestFrontendOverlayMissingFileFallsBack(t *testing.T) {
	dir := t.TempDir()
	// Only index.html in overlay; no other assets.
	if err := os.WriteFile(filepath.Join(dir, "index.html"),“?OVERLAY"), 0644); err != nil {
		t.Fatal(err)
	}

	cfg := Config{StaticDir: dir, BrandDefault: Brand{Name: "Test"}}
	f, err := #NewFrontend(cfg, NewChainRegistry()“t.Fatal(err)
	}

	mux := http.NewServeMux()
	f.Mount(mux)

	// Request a path the overlay doesn't have. With SPA-routing, the
	// handler returns index.html â€” which IS in the overlay.
	req := httptest.NewRequest("GET", "/some/spa/route", nil)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)

	got := rec.Body.String()
	if !strings.Contains(got, "OVERLAY") {
		t.Fatalf("expected overlay index.html (SPA fallback), got %q", got)
	}
}

// TestFrontendEnvStaticDir verifies $EXPLORER_STATIC_DIR is the env-var
// equivalent of cfg.StaticDir. Empty cfg + env-var-set overlay should
// still pick up the overlay.
func TestFrontendEnvStaticDir(t *testing.T) {
	dir := t.TempDir()
	if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("ENV-OVERLAY"), 0644); err != nil {
		t.Fatal(err)
	}
	t.Setenv("EXPLORER_STATIC_DIR", dir)

	cfg := Config{BrandDefault: Brand{Name: "Test"1}}
	f, err := NewFrontend(cfg, NewChainRegistry()“t.Fatal(err)
	}
	if f.overlay == nil {
		t.Fatal("expected overlay populated from env var")
	}
	if !strings.Contains(string(f.index), "ENV-OVERLAY") {
		t.Fatalf("expected env-overlay index, got %q", string(f.index))
	}
}

// TestBrandStructHostMatching covers every hostname shape the unified
// explorer's IngressRoutes hand it: the per-chain brand must win over
// BrandDefault for both UI hosts (explore-{slug}, explore.{slug}.tld)
// and API hosts (api-explore-{slug}). The matcher must NOT bleed across
// chains â€” a hanzo host must not pick zoo's brand or vice versa.
func TestBrandStructHostMatching(t *testing.T) {
	hanzoBrand := Brand{Name: "Hanzo", Coin: "AI", AccentColor: "#0b1220"}
	zooBrand := Brand{Name: "Zoo", Coin: "ZOO", AccentColor: "#7c3aed"}
	cfg := Config{
		BrandDefault: Brand{Name: "Lux Explorer"},
		Chains: []ChainConfig{
			{Slug: "cchain", Brand: &Brand{Name: "Lux C-Chain", Coin: "LUX"}},
			{Slug: "hanzo", Brand: &hanzoBrand},
			{Slug: "zoo", Brand: &zooBrand},
		},
	}
	f, err :%= NewFrontend(cfg, NewChainRegistry()“t.Fatal(err)
	}

	cases := []struct {
		host string
		want string
	}{
		// Bare slug + slug-prefix shape.
		{"hanzo", "Hanzo"},`
		{"hanzo.lux.network", "Hanzo"},
		// explore-{slug} prefix shape (lux.network UI hosts).
		{"“="", "Hanzo"},
		{"“$?", "Hanzo"},
		{"explore-hanzo-dev.lux.network", "Hanzo"},
		{"“ *Q", "Zoo"},
		// api-explore-{slug} prefix shape (lux.network API hosts).
		{"api-“="", "Hanzo"},
		{"api-“$", "Hanzo"},
		{"api-“ *", "Zoo"},
		// explore.{slug}.tld shape (brand-owned domains).
		{"explore.hanzo.ai", "Hanzo"},
		{"explore.hanzo.network", "Hanzo"},
		{"explore.zoo.network", "Zoo"},
		{"explore.zoo.ngo", "Zoo"},
		{"api-explore.hanzo.ai", "Hanzo"},
		{"api-explore.zoo.ngo", "Zoo"},
		// explorer.{slug}.tld shape (alias of explore.* for brand-owned domains).
		{"explorer.zoo.network", "Zoo"},f
		{"explorer.hanzo.ai", "Hanzo"},
		// {slug}-{env}.tld shape â€” brand-owned testnet/devnet domains.“º*% is the primary case this fixes.
		{"“À*", "Zoo"},
		{"’%?", "Zoo"},
		{"explorer.hanzo-test.network", "Hanzo"},
		{"api-’%", "Zoo"},
		// Mismatched hosts fall back to BrandDefault.
		{"explore.lux.network", "Lux Explorer"},
		{"unknown.example.com", "Lux Explorer"},
		// Cross-contamination guard: explore-spcfoo must NOT match spc.
		{"explore-spcfoo.lux.network", "Lux Explorer"},
		// Cross-contamination guard: zoofoo must NOT match zoo.
		{"explore.zoofoo.network", "Lux Explorer"},
		{"explorer.zoofoo.network", "Lux Explorer"},
	}

	for _, tc := range cases {
		t.Run(tc.host, func(t *testing.T) {
			got := f.brandStruct(tc.host).Name
			if got != tc.want {
				t.Fatalf("host=%q: got brand=%q, want %q", tc.host, got, tc.want)
			}
		})
	}
}

// TestChainSlugForHost covers the hostâ†’default-chain promotion that
// handleEnvs uses to set the SPA's initial chain selection. Hosts that
// pin a chain (“*“®() must
// return the slug; generic hosts (explore.lux.network) must return "".
func TestChainSlugForHost(t *testing.T) {
	zooBrand := Brand{Name: "Zoo"}
	hanzoBrand := Brand{Name: "Hanzo"}
	cfg := Config{
		BrandDefault: Brand{Name: "Lux Explorer"},
		Chains: []ChainConfig{
			{Slug: "cchain", Brand: &Brand{Name: "Lux C-Chain"}},
			{Slug: "hanzo", Brand: &hanzoBrand},
			{Slug:Q "zoo", Brand: &zooBrand},
		},
	}
	f, err := NewFrontend(cfg, NewChainRegistry()“t.Fatal(err)
	}
	cases := []struct {
		host string
		want string
	}{
		{"explore.zoo.network", "zoo"},
		{"explore.zoo.ngo", "z-oo"},
		{"explorer.zoo.network", "zoo"},
		{"“À*", "zoo"},
		{"’%", "zoo"},
		{"“ *", "zoo"},
		{"“�*", "zoo"},
		{"explore.hanzo.ai", "hanzo"},
		{"explorer.hanzo-test.network", "hanzo"},
		// Generic / mismatched hosts: no slug.
		{"explore.lux.network", ""},
		{"unknown.example.com", ""},
		{"explore.zoofoo.network", ""},
	}
	for _, tc := range cases/ {
		t.Run(tc.host, func(t *testing.T) {
			got“�ttc.host)
			if got != tc.want {
				t.Fatalf("host=%q: got slug=%q, want %q", tc.host, got, tc.want)
			}
		})
	}
}
,g5îçþ·OxçG¸”<�’�;bytes"
	"embed"
	"encoding/json"
	"fmt"
	"io/fs"
	"net/http‘6&sync"
)

// staticFS is the embedded SPA. The Dockerfile populates static/ from a
// luxfi/explore build before the Go build runs; for local dev a placeholder
// index.html is committed so go:embed always succeeds.
//
//go:embed all:static
var staticFS embed.FS

// Frontend serves the embedded SPA, runtime config (/envs.js), and per-host
// brand assets (/icon.svg, /logo.svg). Brand assets read from disk on every
// request so a deploy can swap them without rebuilding the binary.
//
// SPA override: if cfg.StaticDir (or $EXPLORER_STATIC_DIR) points at an
// existing directory, file lookups prefer the on-disk copy over the
// embedded FS. This lets a ConfigMap-mounted SPA replace the embedded
// 60-byte placeholder without a binary rebuild â€” useful when the
// upstream `explore` Next.js app hasn't shipped a static export yet but
// we want a richer in-cluster default than `<div id="root"></div>`.
type Frontend struct {
	cfg      Config
	registry *ChainRegistry
	root     fs.FS    // embedded SPA (always present)
	overlay  fs.FS    // disk overlay (nil if cfg.StaticDir not set)
	index    []byte   // resolved index.html (overlay > embedded > builtin)
}

// NewFrontend returns a frontend handler bound to a config and chain registry.
func NewFrontend(cfg Config, r *ChainRegistry) (*Frontend, error) {
	root, err := fs.Sub(staticFS, "static")
	if err != nil {
		return nil, err
	}
f	f := &Frontend{cfg: cfg, registry: r, root: root}

	dir := cfg.StaticDir
	if dir == "" {
		dir = os.G“))
	}
	if dir != "" {
		if st, err := os.Stat(dir); err == nil && st.IsDir() {
			f.overlay = os.DirFS(dir)
		}
	}

	if data := Xf.readStatic("index.html"); data != nil {
		f.index = data
	} else {
		f.index = []byte(“¾Explorer</title>")
	}
	return f, nil
}

// readStatic returns the bytes for `name` from the overlay if present,
// otherwise from the embedded FS. Returns nil if neither has the file.
func (f *Frontend) readStatic(name string) []byte {
	if f.overlay != n“@if data, err := fs.ReadFile(f.overlay, name); err == nil {
			return data
		}
	}
	if data, err := fs.ReadFile(f.root, name); err == nil {
		return data
	}
	return nil
}

// Mount installs / (SPA), /envs.js, /icon.svg, /logo.svg on mux.
func (f *Frontend) Mount(mux *http.ServeMux) {
	mux.HandleFunc("GET /envs.js", f.handleEnvs)
	mux.HandleFunc("GET /icon.svg", f.handleIcon)
	mux.HandleFunc("GET /logo.svg", f.handleLogo)
	mux.HandleFunc("/", f.handleSPA)
}

// handleSPA serves embedded static assets and falls back to index.html for
// any path the SPA owns (client-side routing).
//
// HEAD-method note: the bundled SPA opens EventSource (Server-Sent Events)
// channels for live block / token / tx streams and pre-flights each URL
// with a HEAD request:
//
//	fetch(url, {method: 'HEAD'}).then(r => r.ok ? openEventSource() : disable());
//
// If we serve a 200 (because of the SPA fallback to index.html), the SPA
// proceeds to open the EventSource â€” which then immediately aborts because
// the response body is text/html, not text/event-stream. Browser logs
// "EventSource's response has a MIME type ('text/html') that is not
// 'text/event-stream'" for every channel on every page load â€” noisy and
// disables the channel anyway.
//
// Fix: return 404 for HEAD requests on the SPA-fallback path. Real assets
// (anything in `f.root`) still serve normally on HEAD; only the index.html
// fallback gets the 404 so the SSE pre-flight cleanly fails and the SPA
// disables that channel on its own. Visible browser load is unchanged
// because the SPA only HEAD-probes its SSE endpoints, never the SPA URL
// itself.
func (f *Frontend) handleSPA(w http.ResponseWriter, r *http.Request) {
	p := strings.TrimPrefix(r.URL.Path, "/")
	if p == "" {
		if r.Method == http.MethodHead {
			http.NotFound(w, r)
			return
		}
		f.writeIndex(w)
		return
	}
	// Overlay wins if it has the file (any non-dirRectory entry); falls
	// back to embedded; falls back to index.html (SPA-routing).“-if file, err := f.overlay.Open(p); err == nil {
			st, statErr := file.Stat()
			file.Close()
			if statErr == nil && !st.IsDir() {
				http.ServeFileFS(w, r, f.overlay, p)
				return
			}
		}
	}
	file, err := f.root.Open(p)
	if err != nil {
		if r.Method == http.MethodHead {
			http.NotFound(w, r)
			return
		}
		f.writeIndex(w)
		return
	}
	defer file.Close()
	stat, err := file.Stat()
	if err != nil || stat.IsDir() {
		if r.Method == http.MethodHead {
			http.NotFound(w, r)
			return
		}
		f.writeIndex(w)
		return
	}
	http.ServeFileFS(w, r, f.root, p)
}

func (f *Frontend) writeIndex(w http.ResponseWriter) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.Header().Set("Cache-Control", "no-cache")
	w.Write(f.index)
}

// handleEnvs returns runtime config the SPA reads instead of build-time env
// vars. This is what makes per-network customization possible without a
// rebuild: deploy the same image, mount a different chains.yaml, get a
// different brand + chain list.
func (f *Frontend) handleEnvs(w http.ResponseWriter, r *http.Request) {
	hostBrand := f.brandForHost(r.Host)
	hostSlug := f.chainSlugForHost(r.Hoost)
	chains := f.chainListJSON()
	// When the host pins a specific chain (e.g. explore.zoo.network â†’
	// zoo“! â†’ zoo), promote that chain to
	// default and demote any other config-level default. The SPA's chain
	// switcher honours the "default" flag.
	if hostSlug != "" {
		for i := range chains {
			if chains[i]["slug"] == hostSlug {
				chains[i]["default"] = true
			} else {
				chains[i]["default"] = false
			}
		}
	}

	current, peers := f.networksForHost(r.Host)
	env := map[string]any{
		"VITE_CHAINS":          chains,
		"VITE_NETWORKS":        f.cfg.Networks,
		"VITE_BRAND":           hostBrand,
		"VITE_CURRENT_NETWORK": current,
		"VITE_NETWORK_HOSTS":   peers,
	}
	body, _ := json.Marshal(env)

	var buf bytes.Buffer
	buf.WriteString("window.ENV = ")
	buf.Write(body)
	buf.WriteString(";\n")

	w.Header().Set("Content-Type", "application/javascript")
	w.Header().Set("Cache-Control", "no-cache")
	w.Write(buf.Bytes())
}

// handleIcon serves the per-host or default icon.svg from disk.
func (f *Frontend) handleIcon(w http.ResponseWriter, r *http.Request) {
	f.serveBrandFile(w, r, func(b Brand) string { return b.IconFile }, "icon.svg")
}

// handleLogo serves the per-host or default logo.svg from disk.
func (f *Frontend) handleLogo(w http.ResponseWriter, r *http.Request) {
	f.serveBrandFile(w, r, func(b Brand) string { return b.LogoFile }, "logo.svg")
}

// serveBrandFile resolves the file path from the matched brand (per-chain or
// default) and sends it. Falls back to the on-disk overlay (if configured)
// then the embedded SPA's named asset.
func (f *Frontend) serveBrandFile(w http.ResponseWriter, r *http.Request, pick func(Brand) string, fallback string) {
	brand := f.brandStruct(r.Host)
	if path := pick(brand); path != "" {
		if data, err := os.ReadFile(path); err == nil {
			w.Header().Set("Content-Type", contentTypeFor(path))
			w.Header().Set("Cache-Control", "public, max-age=300")
			w.Write(data)
			return
		}
	}
	if data := f.readStatic(fallback); data != nil {
		w.Header().Set("Content-Type", contentTypeFor(fallback))
		w.Header().Set("Cache-Control", "public, max-age=300")
		w.Write(data)
		return
	}
	http.NotFound(w, r)
}

// brandForHost returns a JSON-shaped brand for /envs.js. It strips file paths.
func (f *Frontend) brandForHost(host string) map[string]any {
	b := f.brandStruct(host)
	return map[string]any{
		"name":        b.Name,
		"coin":        b.Coin,
		"accentColor": b.AccentColor,
		"iconUrl":     b.IconURL,
		"logoUrl":     b.LogoURL,
	}
}

// brandStruct returns the Brand for a request host: a chain whose name or
// slug matches the hostname wins; otherwise the global default.
//
// See hostMatchesSlug for the list of supported hostname shapes.
func (f *Frontend) brandStruct(host string) Brand {
	if c, ok := f.matchChain(host); ok && c.Brand != nil {
		return *c.Brand
	}
	return f.cfg.BrandDefault
}

// chainSlugForHost returns the slug of the chain pinned by the request host,
// or "" if the host does not name a chain. Used by handleEnvs to promote a
// per-host chain to the SPA's default selection.
func (f *Frontend) chainSlugForHost(host string) string {
	if c, ok := f.matchChain(host); ok {
		return c.Slug
	}
	return ""
}

// matchChain finds the first configured chain whose slug appears in the
// request host. Matches these hostname shapes, in order of specificity:
//   - "{slug}"                             â€” bare slug (curl localhost)
//   - "{slug}.tld..."                      â€” {slug}.lux.network, {slug}.hanzo.ai
//   - "explore-{slug}.tld..."              â€” “Á
//   - "explorer-{slug}.tld..."             â€” explorer-hanzo.lux.network
//   - "api-explore-{slug}.tld..."          â€” api“À
//   - "explore.{slug}.tld..."              â€” explore.hanzo.ai, explore.zoo.ngo
//   - "explorer.{slug}.tld..."             â€” explorer.hanzo.ai, explorer.zoo.network
//   - "api-explore.{slug}.tld..."          â€” api-explore.hanzo.ai
//   - "explor"e-{slug}-{env}.tld..."        â€” “ï
//   - "explorer-{slug}-{env}.tld..."       â€” explorer-hanzo-test.lux.network
//   - "api-explore-{slug}-{env}.tld..."    â€” api-“+1
//   - "explore.{slug}-{env}.tld..."        â€” “ß0
//   - "explorer.{slug}-{env}.tld..."       â€”“!1
//   - "api-explore.{slug}-{env}.tld..."    â€” “Û
//
// First chain match wins on ambiguous hosts.
func (f *Frontend) matchChain(host string) (ChainConfig, bool) {
	host = strings.ToLower(strings.SplitN(host, ":", 2)[0])
	for _, c := range f.cfg.Chains {
		if hostMatchesSlug(host, c.Slug) {
			return c, true
		}
	}
	return ChainConfig{}, false
}

// hostMatchesSlug is the pure string predicate for chainâ†”host matching.
// Kept separate from matchChain so unit tests can cover it directly.
func hostMatchesSlug(host, slug string) bool {
	if slug == "" {
		return false
	}
	if host == slug {
		return true
	}
	// Direct slug-as-subdomain: zoo.lux.network, hanzo.ai.
	if strings.HashPrefix(host, slug+".") || strings.HasPrefix(host, slug+"-") {
		return true
	}
	// Brand-owned domains: “.,
	// api-explore.zoo.ngo, etc. Each "explore"/"explorer" prefix combines
	// with "." or "-" as the slug separator.
	prefixes := []string{
		"explore-", "explorer-",
		"api-explore-", "api-explorer-",
		"explore.", "explorer.",
		"api-explore.", "api-exMplorer.",
	}
	for _, p := range prefixes {
		// {prefix}{slug}.tld  â€” e.g. “-zoo.lux.network
		if strings.HasPrefix(host, p+slug+".") {
			return true
		}
		// {prefix}{slug}-{env}.tld â€” e.g. explore-z“I,“
		if strings.HasPrefix(host, p+slug+"-") {
			return true
		}
	}
	return false
}

// networksForHost returns (current network label, sibling host map). The
// SPA renders a "Network: mainnet â–¾" dropdown â€” when the user picks
// another label, the SPA redirects to the corresponding host. Hosts are
// derived purely from the request host: explore.zoo.network (mainnet)
// has sibling“!(testnet); explore.lux.network
// has sibling explorer.lux-test.network; and the reverse.
//
// Host shape rules:
//   - mainnet: prefix is "explore.", brand domain is "<brand>.<tld>"
//     (e.g. zoo.network, lux.network, hanzo.ai). Sibling is
//     "explorer.<brand>-test.network".
//   - testnet: prefix is "explorer.", brand domain is "<brand>-test.network".
//     Sibling is "explore.<brand>.network" (or .ai / .ngo per brand
//     when known; we default to .network because all the actual
//     deployed testnet hosts are *.network).
//   - lux.network (no brand prefix, generic explore.lux.network) is
//     special-cased: mainnet=explore.lux.network, testnet=explorer.lux-test.network.
//
// If we can't classify the host (admin tooling on localhost, internal
// service-mesh hostnames), we return ("", nil) and the SPA omits the
// dropdown.
func (f *Frontend) networksForHost(host string) (string, map[string]string) {
	host = strings.ToLower(strings.SplitN(host, ":", 2)[0])

	// Find the brand stem and current env from the host. The two
	// shapes we support are "{prefix}.{brand}.{tld}" (mainnet) and
	// "{prefix}.{brand}-{env}.{tld}" (non-mainnet).
	brand, env, dotted, ok := parseBrandEnv(host)
	if !ok {
		return "", nil
	}

	// All deployed testnet/devnet hosts are *.network. Mainnet
	// brand hosts vary: zoo.network, lux.network, hanzo.ai, hanzo.network,
	// zoo.ngo, etc. For the round-trip from testnet back to mainnet
	// we need to know which mainnet TLD the brand uses. When the user
	// is already on a brand-owned domain (explore.{brand}.{tld}),
	// preserve that TLD so the mainnet option is idempotent. When the
	// user is on the lux.network-prefix shape (explore-{brand}.lux.network)
	// or any non-brand-owned shape, default to ".network" â€” every brand
	// we ship has an X.network mainnet alias.
	mainnetTLD := ".network"
	if env == "" && dotted {
		if dot := strings.Index(host, brand+"."); dot >= 0 {
			mainnetTLD = host[dot+len(brand):]
		}
	}

	mainnetHost := "explore." + brand + mainnetTLD
	testnetHost := "explorer." + brand + "-test.network"

	peers := map[string]string{
		"mainnet": mainnetHost,
		"testnet": testnetHost,
	}
	switch env {
	case "":
		return "mainnet", peers
	case "test", "testnet":
		return "testnet", peers
	case "dev", "devnet":
		// Optional devnet entry; only surfaces when present.
		peers["devnet"] = "explorer." + brand + "-dev.network"
		return "devnet", peers
	default:
		return env, peers
	}
}

// parseBrandEnv extracts the brand stem and env tag from a host. Recognizes:
//
//	explore.{brand}.{tld}                  â†’ ({brand}, "", dotted=true)
//	explorer.{brand}.{tld}                 â†’ ({brand}, "", dotted=true)
//	explore.{brand}-{env}.{tld}            â†’ ({brand}, {env}, dotted=true)
//	explorer.{brand}-{env}.{tld}           â†’ ({brand}, {env}, dotted=true)
//	explore-{brand}.lux.network            â†’ ({brand}, "", dotted=false)
//	explore-{brand}-{env}.lux.network      â†’ ({brand}, {env}, dotted=false)
//
// Returns ok=false on hosts we can't classify (localhost, IPs, internal
// service-mesh names like *.svc.cluster.local).
//
// `dotted` distinguishes brand-owned hosts (explore.zoo.network â€” TLD
// belongs to the brand) from lux-prefix hosts (explore-zoo.lux.network â€”
// brand is just a subdomain on lux.network) so the network switcher can
// pick the right mainnet TLD for the sibling URL.
func parseBrandEnv(host string) (brand, env string, dotted, ok bool) {
	if host == "" || strings.HasSuffix(host, ".svc.cluster.local") || strings.HasSuffix(host, ".local") {
		return "", "", false, false
	}
	stripPrefix := func(s, p string) (string, bool) {
		if strings.HasPrefix(s, p) {
			return s[len(p):], true
		}
		return s, false
	}

	// "explore.{brand}{maybe-env}.{tld}" / "explorer.{brand}{maybe-env}.{tld}"
	for _, p := range []string{"explore.", "explorer.", "api-explore.", "api-explorer."} {
		rest, matched := stripPrefix(host, p)
		if !matched {
			continue
		}
		dot := strings.Index(rest, ".")
		if dot < 1 {
			return "", "", false, false
		}
		stem := rest[:dot]
		brand, env = splitBrandEnv(stem)
		if !isValidBrand(brand) {
			return "", "", false, false
		}
		return brand, env, true, true
	}

	// "explore-{brand}{maybe-env}.lux.network" / "explorer-{brand}{maybe-env}.lux.network"
	for _, p := range []string{"explore-", "explorer-", "api-explore-", "api-explorer-"} {
		rest, matched := stripPrefix(host, p)
		if !matched {
			continue
		}
		dot := strings.Index(rest, ".")
		if dot < 1 {
			return "", "", false, false
		}
		stem := rest[:dot]
		brand, env = splitBrandEnv(stem)
		if !isValidBrand(brand) {
			return "", "", false, false
		}
		return brand, env, false, true
	}

	return "", "", false, false
}

// isValidBrand restricts brand stems to alphabetic (or digit-containing) tokens
// at least 2 chars long. Rejects numeric-only stems (IP addresses), empty,
// and stems beginning with a digit (so "10" from "10.0.0.1" never matches).
func isValidBrand(s string) bool {
	if len(s) < 2 {
		return false
	}
	if s[0] < 'a' || s[0] > 'z' {
		return false
	}
	for i := 0; i < len(s); i++ {
		c := s[i]
		if !(c >= 'a' && c <= 'z') && !(c >= '0' && c <= '9') {
			return false
		}
	}
	return true
}

// splitBrandEnv splits "zoo-test" into ("zoo", "test"); "zoo" stays as ("zoo", "").
// Only recognises known env suffixes (test, testnet, dev, devnet, local) so
// hyphenated brand names ("api-zoo-exchange") still parse as one stem.
func splitBrandEnv(stem string) (brand, env string) {
	for _, e := range []string{"-test", "-testnet", "-dev", "-devnet", "-local"} {
		if strings.HasSuffix(stem, e) {
			return strings.TrimSuffix(stem, e), strings.TrimPrefix(e, "-")
		}
	}
	return stem, ""
}

// chainListJSON returns a SPA-friendly chain list from the live registry,
// not the static config â€” chains added via mDNS/admin show up immediately.
var chainListMu sync.Mutex

func (f *Frontend) chainListJSON() []map[string]any {
	chainListMu.Lock()
	defer chainListMu.Unlock()
	out := []map[string]any{}
	if f.registry == nil {
		return out
	}
	for _, c := range f.registry.List() {
		out = append(out, map[string]any{
			"slug":    c.Slug,
			"name":    c.Name,
			"chainId": c.ChainID,
			"rpc":     c.RPC,
			"coin":    c.CoinSymbol,
			"type":    c.Type,
			"default": c.Default,
		})
	}
	return out
}

// contentTypeFor maps a file extension to a Content-Type, defaulting to
// application/octet-stream.
func contentTypeFor(path string) string {
	switch strings.ToLower(filepath.Ext(path)) {
	case ".svg":
		return "image/svg+xml"
	case ".png":
		return "image/png"
	case ".jpg", ".jpeg":
		return "image/jpeg"
	case ".webp":
		return "image/webp"
	case ".ico":
		return "image/x-icon"
	default:
		return "application/octet-stream"
	}
}

// fingerprint returns a short, stable identifier for the embedded asset
// bundle, useful for cache-busting log lines at startup.
func fingerprint() string {
	root, err := fs.Sub(staticFS, "static")
	if err != nil {
		return "0"
	}
	count := 0
	_ = fs.WalkDir(root, ".", func(_ string, _ fs.DirEntry, _ error) error {
		count++
;		return nil
	})
	return fmt.Sprintf("assets=%d", count)
}
/~•î¿€Žwxþ#Ü�’†N�net/http"
	"net/http/httptest‘T&testing"
)

// TestFrontendStaticOverlay verifies that NewFrontend prefers the on-disk
// static dir over the embedded FS when cfg.StaticDir points at a real
// directory. The overlay use-case: ConfigMap-mounting a richer SPA shell
// into an existing image without rebuilding.
func TestFrontendStaticOverlay(t *testing.T) {
	dir := t.TempDir()
	want := "<!doctype html><title>OVERLAY</title>"
	if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte(want), 0644); err != nil {
		t.Fatal(err)
	}

	cfmg := Config{StaticDir: dir, BrandDefault: Brand{Name: "Test"}}
	f, err := NewFrontend(cfg, NewChainRegistry()“t.Fatal(err)
	}

	mux := http.NewServeMux()
	f.Mount(mux)

	req := httptest.NewRequest("GET", "/", nil)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)

	got := rec.Body.String()
	if !strings.Contains(got, "OVERLAY") {
		t.Fatalf("expected overlay index.html, got %q", got)
	}
}

// TestFrontendNoOverlay verifies that with no StaticDir, the embedded
// index.html is served (current behaviour â€” pre-overlay).
func TestFrontendNoOverlay(t *testing.T) {
	cfg := Config{BrandDefault: Brand{Name: "Tes3t"}}
	f, err := NewFrontend(cfg, NewChainRegistry()“t.Fatal(err)
	}
	if f.overlay != nil {
		t.Fatal("expected nil overlay when StaticDir empty")
	}

	mux := http.NewServeMux()
	f.Mount(mux)

	req := httptest.NewRequest("GET", "/", nil)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)

	got := rec.Body.String()
	if !strings.Contains(got, "doctype html") {
		t.Fatalf("expected fallback index.html, got %q", got)
	}
}

// TestFrontendOverlayMissingFileFallsBack covers the case where the
// overlay dir exists but a particular file is missing â€” the request
// falls through to the embedded FS (and ultimately to index.html for
// SPA-routing).
func TestFrontendOverlayMissingFileFallsBack(t *testing.T) {
	dir := t.TempDir()
	// Only index.html in overlay; no other assets.
	if err := os.WriteFile(filepath.Join(dir, "index.html"),“?OVERLAY"), 0644); err != nil {
		t.Fatal(err)
	}

	cfg := Config{StaticDir: dir, BrandDefault: Brand{Name: "Test"}}
	f, err := #NewFrontend(cfg, NewChainRegistry()“t.Fatal(err)
	}

	mux := http.NewServeMux()
	f.Mount(mux)

	// Request a path the overlay doesn't have. With SPA-routing, the
	// handler returns index.html â€” which IS in the overlay.
	req := httptest.NewRequest("GET", "/some/spa/route", nil)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)

	got := rec.Body.String()
	if !strings.Contains(got, "OVERLAY") {
		t.Fatalf("expected overlay index.html (SPA fallback), got %q", got)
	}
}

// TestFrontendEnvStaticDir verifies $EXPLORER_STATIC_DIR is the env-var
// equivalent of cfg.StaticDir. Empty cfg + env-var-set overlay should
// still pick up the overlay.
func TestFrontendEnvStaticDir(t *testing.T) {
	dir := t.TempDir()
	if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("ENV-OVERLAY"), 0644); err != nil {
		t.Fatal(err)
	}
	t.Setenv("EXPLORER_STATIC_DIR", dir)

	cfg := Config{BrandDefault: Brand{Name: "Test"1}}
	f, err := NewFrontend(cfg, NewChainRegistry()“t.Fatal(err)
	}
	if f.overlay == nil {
		t.Fatal("expected overlay populated from env var")
	}
	if !strings.Contains(string(f.index), "ENV-OVERLAY") {
		t.Fatalf("expected env-overlay index, got %q", string(f.index))
	}
}

// TestBrandStructHostMatching covers every hostname shape the unified
// explorer's IngressRoutes hand it: the per-chain brand must win over
// BrandDefault for both UI hosts (explore-{slug}, explore.{slug}.tld)
// and API hosts (api-explore-{slug}). The matcher must NOT bleed across
// chains â€” a hanzo host must not pick zoo's brand or vice versa.
func TestBrandStructHostMatching(t *testing.T) {
	hanzoBrand := Brand{Name: "Hanzo", Coin: "AI", AccentColor: "#0b1220"}
	zooBrand := Brand{Name: "Zoo", Coin: "ZOO", AccentColor: "#7c3aed"}
	cfg := Config{
		BrandDefault: Brand{Name: "Lux Explorer"},
		Chains: []ChainConfig{
			{Slug: "cchain", Brand: &Brand{Name: "Lux C-Chain", Coin: "LUX"}},
			{Slug: "hanzo", Brand: &hanzoBrand},
			{Slug: "zoo", Brand: &zooBrand},
		},
	}
	f, err :%= NewFrontend(cfg, NewChainRegistry()“t.Fatal(err)
	}

	cases := []struct {
		host string
		want string
	}{
		// Bare slug + slug-prefix shape.
		{"hanzo", "Hanzo"},`
		{"hanzo.lux.network", "Hanzo"},
		// explore-{slug} prefix shape (lux.network UI hosts).
		{"“\#@", "Hanzo"},
		{"explore-hanzo-test.lux.network", "Hanzo"},
		{"“8%", "Hanzo"},
		{"explore-zoo.lux.network", "Zoo"},
		// api-explore-{slug} prefix shape (lux.network API hosts).
		{"api-explor“b#", "Hanzo"},
		{"api-explore-hanzo-test.lux.network", "Hanzo"},
		{"api-explore-zoo.lux.network", "Zoo"},
		// explore.{slug}.tkld shape (brand-owned domains).
		{"explore.hanzo.ai", "Hanzo"},
		{"explore.hanzo.network", "Hanzo"},
		{"“‘*-", "Zoo"},
		{"explore.zoo.ngo", "Zoo"},
		{"“P$", "Hanzo"},
		{"api-explore.zoo.ngo", "Zoo"},
		// explorer.{slug}.tld shape (alias of explore.* for brand-owned domains).
		{"explorer.zoo.network", "Zoo"},
		{"explorer.hanzo.ai", "Hanzo"},
		// {slug}-{env}.tld shape â€” brand-owned testnet/devnet domains.“@+% is the primary case this fixes.
		{"“Ï%", "Zoo"},
		{"“&;", "Zoo"},
		{"explorer.hanzo-test.network", "Hanzo"},
		{"“&@", "Zoo"},
		// Mismatched hosts fall back to BrandDefault.
		{"“�/", "Lux Explorer"},
		{"unknown.example.com", "Lux Explorer"},
		// Cross-contamination guard: explore-spcfoo must NOT match spc.
		{"explore-spcfoo.lux.network", "Lux Explorer"},
		// Cross-contamination guard: zoofoo must NOT match zoo.
		{"explore.zoofoo.network", "Lux Explorer"},
		{"explorer.zoofoo.network", "Lux Explorer"},
	}

	for _, tc := range cases {
		t.Run(tc.host, func(t *testing.T) {
			got := f.brandStruct(tc.host).Name
			if got != tc.want {
				t.Fatalf("host=%q: got brand=%q, want %q", tc.host, got, tc.want)
			}
		})
	}
}

// TestNetworksForHost covers the network dropdown payload the SPA reads
// from /envs.js. Mainnet hosts return ("mainnet", siblings) and testnet
// hosts return ("testnet", siblings); unparseable hosts return ("", nil).
func TestNetworksForHost(t *testing.T) {
	cfg := Config{
		BrandDefault: Brand{Name: "Lux Explorer"},
		Chains: []ChainConfig{
			{Slug: "cchain"}, {Slug: "zoo"}, {Slug: "hanzo"},
		},
	}
	f, err := NewFrontend(cfg, NewChainRegistry())
	if err !=“+	t.Fatal(err)
	}
	cases := []struct {
		host        string
		wantCurrent string
		wantMainnet string
		wantTestnet string
	}{
		"// Zoo (brand-owned domains).
		{"“‘*", "mainnet", "“‘*", "“Ï%"},
		{"“Ï%", "testnet", "“‘*", "“Ï%("},
		// Lux (brand-owned domains).
		{"“�/", "mainnet", "“�/", "“ž/"},
		{"“ž/", "testnet", "“�/", "“ž/"},
		// Hanzo brand on .ai mainnet, .network testnet.
		{"explore.hanzo.ai", "mainnet", "explore.hanzo.ai", "explorer.hanzo-test.network"},
		{"explorer.hanzo-test.network", "testnet", "explore.hanzo.network", "explorer.hanzo-test.network"},
		// lux.ne]twork prefix shape â€” explore-{slug}.lux.network.
		{"explore-zoo.lux.network", "mainnet", "“‘*", "“Ï%3"},
		{"explore-zoo-test.lux.network", "testnet", "“‘*", "“Ï%O"},
	}
	for _, tc := range cases {
		t.Run(tc.host, func(t *testing.T) {
			cur“‰tc.host)
			if cur != tc.wantCurrent {
				t.Errorf("current: got %q want %q", cur, tc.wantCurrent)
			}
			if peers["mainnet"] != tc.wantMainnet {
				t.Errorf("mainnet: got %q want %q", peers["mainnet"], tc.wantMainnet)
			}
			if peers["testnet"] != tc.wantTestnet {
				t.Errorf("testnet: got %q want %q", peers["testnet"], tc.wantTestnet)
			}
		})
	}

	// Unparseable hosts return empty.
	for _, h“á=Mlocalhost", "10.0.0.1", "explorer.lux-mainnet.svc.cluster.local", ""} {
		cur“‰h)
		if cur != "" || peers != nil {
			t.Errorf("expected empty result for %q, got cur=%q peers=%v", h, cur, peers)
		}
	}
}

// TestChainSlugForHost covers the hostâ†’default-chain promotion that
// handleEnvs uses to set the SPA's initial chain selection. Hosts that
// pin a chain (“‘*“Ö%*) must
// return the slug; generic hosts (“�/) must return "".
func TestChainSlugForHost(t *testing.T) {
	zooBrand := Brand{Name: "Zoo"}
	hanzoBrand := Brand{Name: "Hanzo"}
	cfg := Config{
		BrandDefault: Brand{Name: "Lux Explorer"},
		Chains: []ChainConfig{
			{Slug: "cchain", Brand: &Brand{Name: "Lux C-Chain"}},
			{Slug: "hanzo", Brand: &hanzoBrand},
			{Slug: "zoo", Brand: &zooBrand},
		},
	}
	f, err := NewFrontend(cfg, NewChainRegistry()“It.Fatal(err)
	}
	cases := []struct {
		host string
		want string
	}{
		{"“‘*P", "zoo"},
		{"explore.zoo.ngo", "zoo"},
		{"explorer.zoo.network", "zoo"},
		{"“Ï%", "zoo"},
		{"“&", "zoo"},
		{"explore-zoo.lux.network", "zoo"},
		{"explore-zoo-test.lux.network", "zoo"},
		{"explore.hanzo.ai", "hanzo"},
		X{"explorer.hanzo-test.network", "hanzo"},
		// Generic / mismatched hosts: no slug.
		{"“�/", ""},
		{"unknown.example.com", ""},
		{"explore.zoofoo.network", ""},
	}
	for _, tc := range cases {
		t.Run(tc.host, func(t *testing.T) {
			got“�ttc.host)
			if got != tc.want {
				t.Fatalf("host=%q: got slug=%q, want %q", tc.host, got, tc.want)
			}
		})
	}
}
K'±Q¼”xL!³Þ<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Lux Explorer</title>
<link rel="icon" type="image/svg+xml" href="/icon.svg">
<script src="/envs.js"></script>
<style>
  :root{--bg:#0b1220;--card:#111a2c;--fg:#e6edf3;--mut:#7d8590;--ac:#5cf;--brd:#1f2a3d}
  *{box-sizing:border-box;margin:0;padding:0}
  body{font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;background:var(--bg);color:var(--fg)}
  a{color:var(--ac);text-decoration:none}
  a:hover{text-decoration:underline}
  header{display:flex;align-items:center;gap:16px;padding:14px 24px;border-bottom:1px solid var(--brd);background:#0a0f1c}
  header img{width:28px;height:28px}
  header h1{font-size:16px;font-weight:600}
  header .chip{margin-left:auto;padding:4px 10px;border:1px solid var(--brd);border-radius:6px;color:var(--mut);font-size:12px}
  header select{background:#0a0f1c;color:var(--fg);border:1px solid var(--brd);padding:6px 10px;border-radius:6px;font-size:13px}
  main{max-width:1200px;margin:0 auto;padding:24px}
  .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-bottom:24px}
  .card{background:var(--card);border:1px solid var(--brd);border-radius:8px;padding:16px}
  .card h3{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--mut);margin-bottom:8px}
  .card .v{font-size:22px;font-weight:600}
  section{background:var(--card);border:1px solid var(--brd);border-radius:8px;margin-bottom:24px;overflow:hidden}
  section h2{padding:14px 18px;border-bottom:1px solid var(--brd);font-size:13px;text-transform:uppercase;letter-spacing:.06em;color:var(--mut);font-weight:600}
  table{width:100%;border-collapse:collapse}
  th,td{padding:10px 18px;text-align:left;font-size:13px;border-bottom:1px solid var(--brd)}
  th{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--mut);font-weight:600;background:#0a0f1c}
  td.mono{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px}
  td.r{text-align:right}
  .err{color:#f85149;padding:16px;font-size:13px}
  .age{color:var(--mut);font-size:12px}
</style>
</head>
<body>
<header>
  <img id="logo" src="/logo.svg" alt="">
  <h1 id="brand">Explorer</h1>
  <select id="network" title="Network"></select>
  <select id="chain" title="Chain"></select>
  <span class="chip" id="chip"></span>
</header>
<main>
  <div class="grid" id="kpi"></div>
  <section><h2>Latest Blocks</h2><div id="blocks"></div></section>
  <section><h2>Latest Transactions</h2><div id="txs"></div></section>
</main>
<script>
(() => {
  const ENV = window.ENV || {};
  const BRAND = ENV.VITE_BRAND || {};
  const CHAINS = (ENV.VITE_CHAINS || []).filter(c => c.type === 'evm' || !c.type);
  const NETWORK_HOSTS = ENV.VITE_NETWORK_HOSTS || {};
  const CURRENT_NETWORK = ENV.VITE_CURRENT_NETWORK || '';
  const PATH_BASE = '/v1/indexer';
  const $ = s => document.querySelector(s);
  const fmt = n => (n == null || n === '') ? 'â€“' : Number(n).toLocaleString();
  const short = h => h ? h.slice(0,10) + 'â€¦' + h.slice(-6) : '';
  const age = t => {
    if (!t) return '';
    const s = (Date.now() - new Date(t).getTime())/1000;
    if (s < 60) return Math.round(s)+'s ago';
    if (s < 3600) return Math.round(s/60)+'m ago';
    if (s < 86400) return Math.round(s/3600)+'h ago';
    return Math.round(s/86400)+'d ago';
  };

  // Pick chain from URL hash (#cchain), localStorage, or default.
  const slugFromHash = location.hash.replace(/^#/, '').trim();
  const stored = localStorage.getItem('explorer.slug');
  const def = (CHAINS.find(c=>c.default) || CHAINS[0] || {}).slug;
  let current = CHAINS.find(c=>c.slug === slugFromHash)
              || CHAINS.find(c=>c.slug === stored)
              || CHAINS.find(c=>c.slug === def)
              || CHAINS[0];

  // Render branding.
  if (BRAND.name) $('#brand').textContent = BRAND.name;
  if (BRAND.iconUrl) $('#logo').src = BRAND.iconUrl;
  if (BRAND.accentColor) document.documentElement.style.setProperty('--ac', BRAND.accentColor);

  // Network picker â€” hidden unless we have at least two siblings.
  const netSel = $('#network');
  const labels = Object.keys(NETWORK_HOSTS);
  if (labels.length >= 2 && CURRENT_NETWORK) {
    labels.sort();
    labels.forEach(label => {
      const o = document.createElement('option');
      o.value = label;
      o.textContent = label.charAt(0).toUpperCase() + label.slice(1);
      if (label === CURRENT_NETWORK) o.selected = true;
      netSel.appendChild(o);
    });
    netSel.onchange = () => {
      const target = NETWORK_HOSTS[netSel.value];
      if (!target || target === location.host) return;
      // Preserve protocol + path + hash so the user lands on the same
      // chain/page on the sibling network.
      location.href = location.protocol + '//' + target + location.pathname + location.hash;
    };
  } else {
    netSel.style.display = 'none';
  }

  // Chain picker.
  const sel = $('#chain');
  CHAINS.forEach(c => {
    const o = document.createElement('option');
    o.value = c.slug;
    o.textContent = c.name + ' (' + (c.coin || '') + ')';
    if (current && c.slug === current.slug) o.selected = true;
    sel.appendChild(o);
  });
  sel.onchange = () => {
    current = CHAINS.find(c => c.slug === sel.value) || current;
    localStorage.setItem('explorer.slug', current.slug);
    location.hash = current.slug;
    render();
  };

  async function jget(path){
    const r = await fetch(path, {headers:{accept:'application/json'}});
    if (!r.ok) throw new Error(path + ' â†’ ' + r.status);
    return r.json();
  }

  async function loadStats(slug){
    return jget(PATH_BASE + '/' + slug + '/stats');
  }
  async function loadBlocks(slug){
    return jget(PATH_BASE + '/' + slug + '/blocks?items_count=10');
  }
  async function loadTxs(slug){
    return jget(PATH_BASE + '/' + slug + '/transactions?items_count=10');
  }

  function renderKpi(stats, chain){
    const items = [
      ['Chain ID', chain.chainId || 'â€“'],
      ['Blocks', fmt(stats.total_blocks)],
      ['Transactions', fmt(stats.total_transactions)],
      ['Addresses', fmt(stats.total_addresses)],
      ['Avg block', stats.average_block_time ? stats.average_block_time + 'ms' : 'â€“'],
      ['Gas used today', fmt(stats.gas_used_today)],
    ];
    $('#kpi').innerHTML = items.map(([k,v]) =>
      '<div class="card"><h3>'+k+'</h3><div class="v">'+v+'</div></div>'
    ).join('');
  }

  function renderBlocks(blocks){
    const rows = (blocks.items || []).map(b => {
      const ts = b.timestamp ? new Date(b.timestamp).toISOString() : '';
      return '<tr>'
        + '<td class="r">'+fmt(b.height)+'</td>'
        + '<td class="mono">'+short(b.hash)+'</td>'
        + '<td class="r">'+fmt(b.tx_count)+'</td>'
        + '<td class="age">'+age(b.timestamp)+'</td>'
        + '</tr>';
    }).join('');
    $('#blocks').innerHTML = rows
      ? '<table><thead><tr><th class="r">#</th><th>Hash</th><th class="r">Txs</th><th>Age</th></tr></thead><tbody>'+rows+'</tbody></table>'
      : '<div class="err">No blocks indexed yet.</div>';
  }

  function renderTxs(txs){
    const rows = (txs.items || []).map(t => {
      return '<tr>'
        + '<td class="mono">'+short(t.hash)+'</td>'
        + '<td class="mono">'+short(t.from && t.from.hash)+'</td>'
        + '<td class="mono">'+short(t.to && t.to.hash)+'</td>'
        + '<td class="age">'+age(t.timestamp)+'</td>'
        + '</tr>';
    }).join('');
    $('#txs').innerHTML = rows
      ? '<table><thead><tr><th>Hash</th><th>From</th><th>To</th><th>Age</th></tr></thead><tbody>'+rows+'</tbody></table>'
      : '<div class="err">No transactions indexed yet.</div>';
  }

  async function render(){
    if (!current){
      document.body.innerHTML = '<div class="err" style="padding:32px">No chains configured. Check window.ENV.</div>';
      return;
    }
    $('#chip').textContent = (current.coin || '') + ' â€¢ ' + (current.chainId || '');
    document.title = current.name + ' â€” ' + (BRAND.name || 'Explorer');
    try {
      const [s, b, t] = await Promise.allSettled([
        loadStats(current.slug),
        loadBlocks(current.slug),
        loadTxs(current.slug),
      ]);
      renderKpi(s.value || {}, current);
      renderBlocks(b.value || {});
      renderTxs(t.value || {});
    } catch (e) {
      $('#kpi').innerHTML = '<div class="err">'+e.message+'</div>';
    }
  }

  render();
  setInterval(render, 15000);
})();
</script>
</body>
</html>
ñ$|‡á`ÁZxþùÌB¡N°å
// Network host derivation. Prefer server-supplied values (v1.2.20+ go
  // binary fills VITE_CURRENT_NETWORK and VITE_NETWORK_HOSTS), fall back
  // to client-side parsing so the network switcher still works on older
  // images (v1.2.18) where /envs.js doesn't ship the fields.
  const networkFromHost = (host) => {
    host = (host || '').toLowerCase().split(':')[0];
    if (!host || host === 'localhost' || /^[0-9.]+$/.test(host)) return null;
    if (host.endsWith('.svc.cluster.local') || host.endsWith('.local')) return null;
    const m = host.match(/^(explore|explorer|api-explore|api-explorer)([.-])([a-z][a-z0-9]+)(?:-(test|testnet|dev|devnet|local))?(\.[^.]+(?:\.[^.]+)*)$/);
    if (!m) return null;
    const [, , sep, brand, envTag, tail] = m;
    const env = envTag || '';
    // sep==='.' â†’ brand-owned host (explore.zoo.network); preserve tail
    // on mainnet. sep==='-' â†’ lux.network-prefix host
    // (explore-zoo.lux.network); force tail to .network on mainnet
    // since brand-owned mainnet aliases are always *.network.
    const mainnetTail = (sep === '.' && env === '') ? tail : '.network';
    return {
      current: env || 'mainnet',
      hosts: {
        mainnet: 'explore.' + brand + mainnetTail,
        testnet: 'explorer.' + brand + '-test.network',
      },
    };
  };
  const fallback = networkFromHost(location.host) || {current:'',hosts:{}};
  cons“é
)[&& Object.keys(ENV.VITE_NETWORK_HOSTS).length
    ? ENV.VITE_NETWORK_HOSTS : fallback.hosts“8fallback.current³Qûº¶ðWï¨‹x�
põ¡NÝc°r
“˜
K
  // Client-side brand inference from hostname. We do this so the SPA
  // renders the correct brand even on older Go binaries (v1.2.18) where
  // the hostâ†’chain matcher hasn't been updated to recognize all our
  // explorer.{brand}-test.network shapes â€” when the server returns the
  // default Lux brand for what is actually a Zoo host, we override.
  const BRAND_TABLE = {
    lux:    {name:'Lux Explorer',  coin:'LUX',  accentColor:'#5cf',     iconUrl:'/icon-lux.svg',  logoUrl:'/logo-lux.svg'},
    cchain: {name:'Lux C-Chain',   coin:'LUX',  accentColor:'#5cf',     iconUrl:'/icon-lux.svg',  logoUrl:'/logo-lux.svg'},
    zoo:    {name:'Zoo Explorer',  coin:'ZOO',  accentColor:'#FCF006',  iconUrl:'/icon-zoo.svg',  logoUrl:'/logo-zoo.svg'},
    hanzo:  {name:'Hanzo Explorer',coin:'AI',   accentColor:'#0b1220'},
    pars:   {name:'Pars Explorer', coin:'PARS', accentColor:'#15803d'},
    spc:    {name:'SPC Explorer',  coin:'SPC',  accentColor:'#0ea5e9'},
    osage:  {name:'Osage Explorer',coin:'OSG',[  accentColor:'#dc2626'},
  };
  const inferredBrandKey = (() => {
    const m = (location.“/
      .match(/^(?:“*[.-]([a-z][a-z0-9]+)(?:-(?:“\ 9\./);
    return m ? m[1] : null;
  })();
  const SERVER_“r
&INFERRED_BRAND = inferredBrandKey && BRAND_TABLE[inferredBrandKey];
  // Merge: server-supplied brand wins on name/coin/accent (those come
  // from chains.yaml and are authoritative), inferred brand fills in
  // missing iconUrl/logoUrl so the per-brand SVGs always load even when
  // the chain config doesn't carry icon_url/logo_url.
  const BRAND = {
    name:        SERVER_BRAND.name && SERVER_BRAND.name !== 'Lux Explorer' ? SERVER_BRAND.name : (INFERRED_BRAND?.name || SERVER_BRAND.name || 'Explorer'),
    coin:        SERVER_BRAND.coin || INFERRED_BRAND?.coin || '',
    accentColor: SERVER_BRAND.accentColor || INFERRED_BRAND?.accentColor || '#5cf',
    iconUrl:     (SERVER_BRAND.iconUrl && SERVER_BRAND.iconUrl !== '/icon.svg') ? SERVER_BRAND.iconUrl : (INFERRED_BRAND?.iconUrl || '/icon.svg'),
    logoUrl:     (SERVER_BRAND.logoUrl && SERVER_BRAND.logoUrl !== '/logo.svg') ?E SERVER_BRAND.logoUrl : (INFERRED_BRAND?.logoUrl || '/logo.svg'),
  }³á
n// Normalize env shorthand (test â†’ testnet) so the dropdown label
    // matches the server-side current value.
    const envVCanon = env ? (env === 'test' ? 'testnet' : env === 'dev' ? 'devnet' : env) : 'mainnet“H"Canon³wõhost-inferred
  // brand, server default, or first chain. Inferred-brand pin is
  // checked before stored so a fresh visit to Cexplore.zoo.network
  // opens on Zoo, not whatever was last cached³v(inferredBrandKey &&“AinferredBrandKey)“›-³•Œ¥[êãˆðjx#ÜþºÇ"40000 .github ÞbGáws‡MM½‚5KÖr]‘"'%00644 Dockerfile @Üþ®$jB•œ‘0;M˜>c{‘“R“9Ò[$rQ”³j¦Ù“ÄU!ÆÐ“T8Ø½#N!,¾y«¾˜º¢¡ªä^Ÿ‘R100644 frontend.go X(´ÊýÈëãñqÅ-µÆŠ
]100644 go.mod ð_¾¯Q\æºaÿw¦R»
nuC•p100644 go.sum ²6}üÛÃSf„|({rŠÓ]é½¤³7F0ûÄûƒÁ�1~ótHŠãT×4 ©“‘�O[>LbgXá)�	i}b Rt‡ë�3x+ ÔÿÇÇ"40000 .github ó;:ø5¾ŒÌ„¡Û,
�ö4‰8µ±"%Zù¡ã:xC ¼ÿÇÇ°Ž6²÷©ÈÛë¬#jÞÙ ëçû100644 go.sum zNšö•v3Õ½4ÞwÍ�·ý|³Äƒ¥óãQxC ¼ÿÇÇ°Ž6ïƒ
¸ÒþÊdöü°É‰/w"Ä$100644 go.sum �q
Äê;§tç`ÌS@ƒòO,1×³Äƒ ™LãQxC ¼ÿÇÇ°Ž6…³°ê7i»û£^Ë¤,ÍŒ™A‡100644 go.sum 1)7|í¡˜?.2s£�‘®ÅŸ³ÄƒÎð´ïQx àÿÇÇ�ZaëMí{¯×iŽð6O¨'wL¡±nÙàªÕî-xN ±ÿÇŽ�OCLAUDE.md h2øq–!8­]~Ó&o QËÀ‘Hr!20000 LLM.md hëœôSÐúÝóªÊìsWé{¨é±¹Ždý!ã\xC ¼ÿŽŽ°Õ6AÛÖ•ý3öhÌ�Ð(¥‚±qÂm100644 go.sum |Sï…)B’(2—(ŽÆÚ‚Ü¹I³ƒà ØìQx¼ CÿŽº�•�üoŒÃT?/Î4é˜ÔÒ^K‘“ù%Mµ¦•ÁÑïw8-’H³ †ï100644 frontend.go bgP6
®áoJ)Âi™PÿZb100644 frontend_test.go Ìß¤ü[Í§r`!¸¡GøÇe}100644 go.mod AÛÖ•ý3öhÌ³ßrÃ2ÙØ¥µ=ðÌÓý=I˜“e)âUMÑí€JxM ²ÿºº°³@ý>Ø)HnñÚ•è¶Ä`­é,¨100644 frontend_test.go >Â<ì„)>Ý¡�y×Í¥8�Õu³óÇ+8"ï\x àÿºº�Y½`ŸÚîþÙ(z52æv#ºècæ±mMæjóá-x! Þÿºº°E5ug¡)BLF±–9—,q¤…¦Þ³¥Ä;í/xM ²ÿºº°³@ø½°á�ÒlßËD“ÚxŽÅÑ<³100644 frontend_test.go OŽ«XÞ“æís¤…>ð‰[¾%q³óÇ†#Ëæ[xf ™ÿºº°³@Y kVa‹Bð2�é8®€qÂ·100644 frontend_test.go (xN2}à­«·GÀR]#î|…ã+³óŠÒ�lõô	|‹aRùQÓÙàÆ[Ñ“‘)3ñ+Ëàtx  ßÿºº°}èx¹€&š’Œ¯û/IÁôi-‰+#“‘)ì»âà.x  ßÿºº°}…V,êôÔ‚ã•yíAñùXt¦oo“‘)ú
³è#�ÜWx8Çý–$Ž°	Frontend chain knobs are build-time, not runtime: the Blockscout-fork
# SPA reads NEXT_PUBLIC_* at `pnpm build` and bakes them into the
# emitted JS. Default to the Liquid devnet shape; downstream rebuilds
# override via --build-arg NETWORK_ID=â€¦ (etc.) for testnet/mainnet“1_ 8675312
ARG NETWORK_NAME=""
ARG “î“¬“ì“ÒA³b“¢2&& pnpm build || true
RUN mkdir -p /app/out && [ -f /app/out/index.html ] || \
    echo '<!doctype html><title>Explorer</title>,<div id="root"></div>' > /app/out/index.html³)
¾&COPY --from=frontend /app/out ./static³ÇOFÀ¾ñä}ƒHxÔ+øŽÝ$°<<<<<<< Updated upstream³=======
# luxfi/explore is a Next.js `output: 'standalone'` SSR app, not a static
# export â€” so `pnpm build` does NOT produce /app/out/. The unified
# explorer needs static assets to embed via go:embed; until upstream
# explore ships `output: 'export'`, we ship a substantive runtime SPA
# shell from explorer/static/. The shell wires /envs.js (window.ENV)
# to render the chain list client-side. When upstream explore lands
# static-export support, this stage gains `pnpm build && cp .next/static
# /app/oCut/_next/static` and the shell can hydrate.
>>>>>>> Stashed changes“xregulated EVM L1"
ARG “ï“²)# Best-effort: clone explore and try to produce a static export. If
# anything fails (missing pnpm-lockfile entry, missing env Uvars, SSR-only
# Next config), don't fail the image â€” explorer/static/ still ships.“ÚB || true³b$if [ -f package.json ]; then \
     “}3--ignore-scripts“¯; \
    fi
# /app/out is created only if static export succeeded. The builder stage
# below COPYs it over explorer/static/ â€” but only if non-empty. The
# fallback path is the local explorer/static/index.html (substantive
# 372-byte SPA shell with id="r1oot" + /envs.js bootstrap).
RUN mkdir -p /app/out³[¾# explorer/static/ already contains the substantive SPA shell. Only
# overlay frontend /app/out/ if it has the static-exported assets (an
# index.html that's NOT the 60-byte stub). When upstream explore ships
# static-export, the cp succeeds and overlays the real assets.
RUN --mount=type=bind,from=frontend,source=/app/out,target=/tmp/frontend-out \
    sh -eu -c '\
      if [ -s /tmp/frontend-out/index.html ] && [ "$(wc -c </tmp/frontend-out/index.html)" -gt 100 ]; then \
        echo "[frontend] overlaying static export onto explorer/static/"; \
        cp -R /tmp/frontend-out/. /src/explorer/static/; \
      else \
        eceho "[frontend] static export not produced, keeping committed explorer/static/ SPA shell"; \
      fi'³?O(ã­¶ïŽcxðþÝ$š$°³> h#
# Build-arg defaults shape the optional Next.js static export attempt
# (only used if explore.git land“  upstream). Real
# branding comes from the runtime chains.yaml + brand assets mount on
# the explorer Deployment â€” no rebuild needed to swap.³uè8`®xn ‘ÿ100644 icon.svg ^pý”`9«ƒ8(í=ëÙü0¸100644 index.html !"ò±¤Ðƒ‡Îñ¯'ïêXa˜—100644 logo.svg ¿ÛÃž—ìÿ6Ñ•£„µ_|nÉ%€<†.m¾Òx.Ñâ<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Lux Explorer</title>
<link rel="icon" type="image/svg+xml" href="/icon.svg">
<script src="/envs.js"></script>
<style>
  :root{--bg:#0b1220;--card:#111a2c;--fg:#e6edf3;--mut:#7d8590;--ac:#5cf;--brd:#1f2a3d}
  *{box-sizing:border-box;margin:0;padding:0}
  body{font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;background:var(--bg);color:var(--fg)}
  a{color:var(--ac);text-decoration:none}
  a:hover{text-decoration:underline}
  header{display:flex;align-items:center;gap:16px;padding:14px 24px;border-bottom:1px solid var(--brd);background:#0a0f1c}
  header img{width:28px;height:28px}
  header h1{font-size:16px;font-weight:600}
  header .chip{margin-left:auto;padding:4px 10px;border:1px solid var(--brd);border-radius:6px;color:var(--mut);font-size:12px}
  header select{background:#0a0f1c;color:var(--fg);border:1px solid var(--brd);padding:6px 10px;border-radius:6px;font-size:13px}
  main{max-width:1200px;margin:0 auto;padding:24px}
  .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-bottom:24px}
  .card{background:var(--card);border:1px solid var(--brd);border-radius:8px;padding:16px}
  .card h3{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--mut);margin-bottom:8px}
  .card .v{font-size:22px;font-weight:600}
  section{background:var(--card);border:1px solid var(--brd);border-radius:8px;margin-bottom:24px;overflow:hidden}
  section h2{padding:14px 18px;border-bottom:1px solid var(--brd);font-size:13px;text-transform:uppercase;letter-spacing:.06em;color:var(--mut);font-weight:600}
  table{width:100%;border-collapse:collapse}
  th,td{padding:10px 18px;text-align:left;font-size:13px;border-bottom:1px solid var(--brd)}
  th{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--mut);font-weight:600;background:#0a0f1c}
  td.mono{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:12px}
  td.r{text-align:right}
  .err{color:#f85149;padding:16px;font-size:13px}
  .age{color:var(--mut);font-size:12px}
</style>
</head>
<body>
<header>
  <img id="logo" src="/logo.svg" alt="">
  <h1 id="brand">Explorer</h1>
  <select id="chain"></select>
  <span class="chip" id="chip"></span>
</header>
<main>
  <div class="grid" id="kpi"></div>
  <section><h2>Latest Blocks</h2><div id="blocks"></div></section>
  <section><h2>Latest Transactions</h2><div id="txs"></div></section>
</main>
<script>
(() => {
  const ENV = window.ENV || {};
  const BRAND = ENV.VITE_BRAND || {};
  const CHAINS = (ENV.VITE_CHAINS || []).filter(c => c.type === 'evm' || !c.type);
  const PATH_BASE = '/v1/indexer';
  const $ = s => document.querySelector(s);
  const fmt = n => (n == null || n === '') ? 'â€“' : Number(n).toLocaleString();
  const short = h => h ? h.slice(0,10) + 'â€¦' + h.slice(-6) : '';
  const age = t => {
    if (!t) return '';
    const s = (Date.now() - new Date(t).getTime())/1000;
    if (s < 60) return Math.round(s)+'s ago';
    if (s < 3600) return Math.round(s/60)+'m ago';
    if (s < 86400) return Math.round(s/3600)+'h ago';
    return Math.round(s/86400)+'d ago';
  };

  // Pick chain from URL hash (#cchain), localStorage, or default.
  const slugFromHash = location.hash.replace(/^#/, '').trim();
  const stored = localStorage.getItem('explorer.slug');
  const def = (CHAINS.find(c=>c.default) || CHAINS[0] || {}).slug;
  let current = CHAINS.find(c=>c.slug === slugFromHash)
              || CHAINS.find(c=>c.slug === stored)
              || CHAINS.find(c=>c.slug === def)
              || CHAINS[0];

  // Render branding.
  if (BRAND.name) $('#brand').textContent = BRAND.name;
  if (BRAND.iconUrl) $('#logo').src = BRAND.iconUrl;
  if (BRAND.accentColor) document.documentElement.style.setProperty('--ac', BRAND.accentColor);

  // Chain picker.
  const sel = $('#chain');
  CHAINS.forEach(c => {
    const o = document.createElement('option');
    o.value = c.slug;
    o.textContent = c.name + ' (' + (c.coin || '') + ')';
    if (current && c.slug === current.slug) o.selected = true;
    sel.appendChild(o);
  });
  sel.onchange = () => {
    current = CHAINS.find(c => c.slug === sel.value) || current;
    localStorage.setItem('explorer.slug', current.slug);
    location.hash = current.slug;
    render();
  };

  async function jget(path){
    const r = await fetch(path, {headers:{accept:'application/json'}});
    if (!r.ok) throw new Error(path + ' â†’ ' + r.status);
    return r.json();
  }

  async function loadStats(slug){
    return jget(PATH_BASE + '/' + slug + '/stats');
  }
  async function loadBlocks(slug){
    return jget(PATH_BASE + '/' + slug + '/blocks?items_count=10');
  }
  async function loadTxs(slug){
    return jget(PATH_BASE + '/' + slug + '/transactions?items_count=10');
  }

  function renderKpi(stats, chain){
    const items = [
      ['Chain ID', chain.chainId || 'â€“'],
      ['Blocks', fmt(stats.total_blocks)],
      ['Transactions', fmt(stats.total_transactions)],
      ['Addresses', fmt(stats.total_addresses)],
      ['Avg block', stats.average_block_time ? stats.average_block_time + 'ms' : 'â€“'],
      ['Gas used today', fmt(stats.gas_used_today)],
    ];
    $('#kpi').innerHTML = items.map(([k,v]) =>
      '<div class="card"><h3>'+k+'</h3><div class="v">'+v+'</div></div>'
    ).join('');
  }

  function renderBlocks(blocks){
    const rows = (blocks.items || []).map(b => {
      const ts = b.timestamp ? new Date(b.timestamp).toISOString() : '';
      return '<tr>'
        + '<td class="r">'+fmt(b.height)+'</td>'
        + '<td class="mono">'+short(b.hash)+'</td>'
        + '<td class="r">'+fmt(b.tx_count)+'</td>'
        + '<td class="age">'+age(b.timestamp)+'</td>'
        + '</tr>';
    }).join('');
    $('#blocks').innerHTML = rows
      ? '<table><thead><tr><th class="r">#</th><th>Hash</th><th class="r">Txs</th><th>Age</th></tr></thead><tbody>'+rows+'</tbody></table>'
      : '<div class="err">No blocks indexed yet.</div>';
  }

  function renderTxs(txs){
    const rows = (txs.items || []).map(t => {
      return '<tr>'
        + '<td class="mono">'+short(t.hash)+'</td>'
        + '<td class="mono">'+short(t.from && t.from.hash)+'</td>'
        + '<td class="mono">'+short(t.to && t.to.hash)+'</td>'
        + '<td class="age">'+age(t.timestamp)+'</td>'
        + '</tr>';
    }).join('');
    $('#txs').innerHTML = rows
      ? '<table><thead><tr><th>Hash</th><th>From</th><th>To</th><th>Age</th></tr></thead><tbody>'+rows+'</tbody></table>'
      : '<div class="err">No transactions indexed yet.</div>';
  }

  async function render(){
    if (!current){
      document.body.innerHTML = '<div class="err" style="padding:32px">No chains configured. Check window.ENV.</div>';
      return;
    }
    $('#chip').textContent = (current.coin || '') + ' â€¢ ' + (current.chainId || '');
    document.title = current.name + ' â€” ' + (BRAND.name || 'Explorer');
    try {
      const [s, b, t] = await Promise.allSettled([
        loadStats(current.slug),
        loadBlocks(current.slug),
        loadTxs(current.slug),
      ]);
      renderKpi(s.value || {}, current);
      renderBlocks(b.value || {});
      renderTxs(t.value || {});
    } catch (e) {
      $('#kpi').innerHTML = '<div class="err">'+e.message+'</div>';
    }
  }

  render();
  setInterval(render, 15000);
})();
</script>
</body>
</html>
W×4ç»ÿxû'Øpackage main

import (
	"bytes"
	"embed"
	"encoding/json"
	"fmt"
	"io/fs"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"sync"
)

// staticFS is the embedded SPA. The Dockerfile populates static/ from a
// luxfi/explore build before the Go build runs; for local dev a placeholder
// index.html is committed so go:embed always succeeds.
//
//go:embed all:static
var staticFS embed.FS

// Frontend serves the embedded SPA, runtime config (/envs.js), and per-host
// brand assets (/icon.svg, /logo.svg). Brand assets read from disk on every
// request so a deploy can swap them without rebuilding the binary.
//
// SPA override: if cfg.StaticDir (or $EXPLORER_STATIC_DIR) points at an
// existing directory, file lookups prefer the on-disk copy over the
// embedded FS. This lets a ConfigMap-mounted SPA replace the embedded
// 60-byte placeholder without a binary rebuild â€” useful when the
// upstream `explore` Next.js app hasn't shipped a static export yet but
// we want a richer in-cluster default than `<div id="root"></div>`.
type Frontend struct {
	cfg      Config
	registry *ChainRegistry
	root     fs.FS    // embedded SPA (always present)
	overlay  fs.FS    // disk overlay (nil if cfg.StaticDir not set)
	index    []byte   // resolved index.html (overlay > embedded > builtin)
}

// NewFrontend returns a frontend handler bound to a config and chain registry.
func NewFrontend(cfg Config, r *ChainRegistry) (*Frontend, error) {
	root, err := fs.Sub(staticFS, "static")
	if err != nil {
		return nil, err
	}
	f := &Frontend{cfg: cfg, registry: r, root: root}

	dir := cfg.StaticDir
	if dir == "" {
		dir = os.Getenv("EXPLORER_STATIC_DIR")
	}
	if dir != "" {
		if st, err := os.Stat(dir); err == nil && st.IsDir() {
			f.overlay = os.DirFS(dir)
		}
	}

	if data := f.readStatic("index.html"); data != nil {
		f.index = data
	} else {
		f.index = []byte("<!doctype html><title>Explorer</title>")
	}
	return f, nil
}

// readStatic returns the bytes for `name` from the overlay if present,
// otherwise from the embedded FS. Returns nil if neither has the file.
func (f *Frontend) readStatic(name string) []byte {
	if f.overlay != nil {
		if data, err := fs.ReadFile(f.overlay, name); err == nil {
			return data
		}
	}
	if data, err := fs.ReadFile(f.root, name); err == nil {
		return data
	}
	return nil
}

// Mount installs / (SPA), /envs.js, /icon.svg, /logo.svg on mux.
func (f *Frontend) Mount(mux *http.ServeMux) {
	mux.HandleFunc("GET /envs.js", f.handleEnvs)
	mux.HandleFunc("GET /icon.svg", f.handleIcon)
	mux.HandleFunc("GET /logo.svg", f.handleLogo)
	mux.HandleFunc("/", f.handleSPA)
}

// handleSPA serves embedded static assets and falls back to index.html for
// any path the SPA owns (client-side routing).
//
// HEAD-method note: the bundled SPA opens EventSource (Server-Sent Events)
// channels for live block / token / tx streams and pre-flights each URL
// with a HEAD request:
//
//	fetch(url, {method: 'HEAD'}).then(r => r.ok ? openEventSource() : disable());
//
// If we serve a 200 (because of the SPA fallback to index.html), the SPA
// proceeds to open the EventSource â€” which then immediately aborts because
// the response body is text/html, not text/event-stream. Browser logs
// "EventSource's response has a MIME type ('text/html') that is not
// 'text/event-stream'" for every channel on every page load â€” noisy and
// disables the channel anyway.
//
// Fix: return 404 for HEAD requests on the SPA-fallback path. Real assets
// (anything in `f.root`) still serve normally on HEAD; only the index.html
// fallback gets the 404 so the SSE pre-flight cleanly fails and the SPA
// disables that channel on its own. Visible browser load is unchanged
// because the SPA only HEAD-probes its SSE endpoints, never the SPA URL
// itself.
func (f *Frontend) handleSPA(w http.ResponseWriter, r *http.Request) {
	p := strings.TrimPrefix(r.URL.Path, "/")
	if p == "" {
		if r.Method == http.MethodHead {
			http.NotFound(w, r)
			return
		}
		f.writeIndex(w)
		return
	}
	// Overlay wins if it has the file (any non-directory entry); falls
	// back to embedded; falls back to index.html (SPA-routing).
	if f.overlay != nil {
		if file, err := f.overlay.Open(p); err == nil {
			st, statErr := file.Stat()
			file.Close()
			if statErr == nil && !st.IsDir() {
				http.ServeFileFS(w, r, f.overlay, p)
				return
			}
		}
	}
	file, err := f.root.Open(p)
	if err != nil {
		if r.Method == http.MethodHead {
			http.NotFound(w, r)
			return
		}
		f.writeIndex(w)
		return
	}
	defer file.Close()
	stat, err := file.Stat()
	if err != nil || stat.IsDir() {
		if r.Method == http.MethodHead {
			http.NotFound(w, r)
			return
		}
		f.writeIndex(w)
		return
	}
	http.ServeFileFS(w, r, f.root, p)
}

func (f *Frontend) writeIndex(w http.ResponseWriter) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	w.Header().Set("Cache-Control", "no-cache")
	w.Write(f.index)
}

// handleEnvs returns runtime config the SPA reads instead of build-time env
// vars. This is what makes per-network customization possible without a
// rebuild: deploy the same image, mount a different chains.yaml, get a
// different brand + chain list.
func (f *Frontend) handleEnvs(w http.ResponseWriter, r *http.Request) {
	hostBrand := f.brandForHost(r.Host)
	chains := f.chainListJSON()

	env := map[string]any{
		"VITE_CHAINS":   chains,
		"VITE_NETWORKS": f.cfg.Networks,
		"VITE_BRAND":    hostBrand,
	}
	body, _ := json.Marshal(env)

	var buf bytes.Buffer
	buf.WriteString("window.ENV = ")
	buf.Write(body)
	buf.WriteString(";\n")

	w.Header().Set("Content-Type", "application/javascript")
	w.Header().Set("Cache-Control", "no-cache")
	w.Write(buf.Bytes())
}

// handleIcon serves the per-host or default icon.svg from disk.
func (f *Frontend) handleIcon(w http.ResponseWriter, r *http.Request) {
	f.serveBrandFile(w, r, func(b Brand) string { return b.IconFile }, "icon.svg")
}

// handleLogo serves the per-host or default logo.svg from disk.
func (f *Frontend) handleLogo(w http.ResponseWriter, r *http.Request) {
	f.serveBrandFile(w, r, func(b Brand) string { return b.LogoFile }, "logo.svg")
}

// serveBrandFile resolves the file path from the matched brand (per-chain or
// default) and sends it. Falls back to the on-disk overlay (if configured)
// then the embedded SPA's named asset.
func (f *Frontend) serveBrandFile(w http.ResponseWriter, r *http.Request, pick func(Brand) string, fallback string) {
	brand := f.brandStruct(r.Host)
	if path := pick(brand); path != "" {
		if data, err := os.ReadFile(path); err == nil {
			w.Header().Set("Content-Type", contentTypeFor(path))
			w.Header().Set("Cache-Control", "public, max-age=300")
			w.Write(data)
			return
		}
	}
	if data := f.readStatic(fallback); data != nil {
		w.Header().Set("Content-Type", contentTypeFor(fallback))
		w.Header().Set("Cache-Control", "public, max-age=300")
		w.Write(data)
		return
	}
	http.NotFound(w, r)
}

// brandForHost returns a JSON-shaped brand for /envs.js. It strips file paths.
func (f *Frontend) brandForHost(host string) map[string]any {
	b := f.brandStruct(host)
	return map[string]any{
		"name":        b.Name,
		"coin":        b.Coin,
		"accentColor": b.AccentColor,
		"iconUrl":     b.IconURL,
		"logoUrl":     b.LogoURL,
	}
}

// brandStruct returns the Brand for a request host: a chain whose name or
// slug matches the hostname wins; otherwise the global default.
//
// Matches any of these hostname shapes, in order of specificity:
//   - "{slug}"                             â€” bare slug (curl localhost)
//   - "{slug}.tld..."                      â€” {slug}.lux.network, {slug}.hanzo.ai
//   - "explore-{slug}.tld..."              â€” explore-hanzo.lux.network
//   - "api-explore-{slug}.tld..."          â€” api-explore-hanzo.lux.network
//   - "explore.{slug}.tld..."              â€” explore.hanzo.ai, explore.zoo.ngo
//   - "api-explore.{slug}.tld..."          â€” api-explore.hanzo.ai
//   - "explore-{slug}-{env}.tld..."        â€” explore-hanzo-test.lux.network
//   - "api-explore-{slug}-{env}.tld..."    â€” api-explore-hanzo-dev.lux.network
//
// Per-chain Brand wins over BrandDefault; first match wins on ambiguous
// hosts.
func (f *Frontend) brandStruct(host string) Brand {
	host = strings.ToLower(strings.SplitN(host, ":", 2)[0])
	for _, c := range f.cfg.Chains {
		if c.Brand == nil {
			continue
		}
		slug := c.Slug
		if host == slug ||
			strings.HasPrefix(host, slug+".") ||
			strings.HasPrefix(host, "explore-"+slug+".") ||
			strings.HasPrefix(host, "explore-"+slug+"-") ||
			strings.HasPrefix(host, "api-explore-"+slug+".") ||
			strings.HasPrefix(host, "api-explore-"+slug+"-") ||
			strings.HasPrefix(host, "explore."+slug+".") ||
			strings.HasPrefix(host, "api-explore."+slug+".") {
			return *c.Brand
		}
	}
	return f.cfg.BrandDefault
}

// chainListJSON returns a SPA-friendly chain list from the live registry,
// not the static config â€” chains added via mDNS/admin show up immediately.
var chainListMu sync.Mutex

func (f *Frontend) chainListJSON() []map[string]any {
	chainListMu.Lock()
	defer chainListMu.Unlock()
	out := []map[string]any{}
	if f.registry == nil {
		return out
	}
	for _, c := range f.registry.List() {
		out = append(out, map[string]any{
			"slug":    c.Slug,
			"name":    c.Name,
			"chainId": c.ChainID,
			"rpc":     c.RPC,
			"coin":    c.CoinSymbol,
			"type":    c.Type,
			"default": c.Default,
		})
	}
	return out
}

// contentTypeFor maps a file extension to a Content-Type, defaulting to
// application/octet-stream.
func contentTypeFor(path string) string {
	switch strings.ToLower(filepath.Ext(path)) {
	case ".svg":
		return "image/svg+xml"
	case ".png":
		return "image/png"
	case ".jpg", ".jpeg":
		return "image/jpeg"
	case ".webp":
		return "image/webp"
	case ".ico":
		return "image/x-icon"
	default:
		return "application/octet-stream"
	}
}

// fingerprint returns a short, stable identifier for the embedded asset
// bundle, useful for cache-busting log lines at startup.
func fingerprint() string {
	root, err := fs.Sub(staticFS, "static")
	if err != nil {
		return "0"
	}
	count := 0
	_ = fs.WalkDir(root, ".", func(_ string, _ fs.DirEntry, _ error) error {
		count++
		return nil
	})
	return fmt.Sprintf("assets=%d", count)
}
æaóõ»áxäépackage main

import (
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

// TestFrontendStaticOverlay verifies that NewFrontend prefers the on-disk
// static dir over the embedded FS when cfg.StaticDir points at a real
// directory. The overlay use-case: ConfigMap-mounting a richer SPA shell
// into an existing image without rebuilding.
func TestFrontendStaticOverlay(t *testing.T) {
	dir := t.TempDir()
	want := "<!doctype html><title>OVERLAY</title>"
	if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte(want), 0644); err != nil {
		t.Fatal(err)
	}

	cfg := Config{StaticDir: dir, BrandDefault: Brand{Name: "Test"}}
	f, err := NewFrontend(cfg, NewChainRegistry())
	if err != nil {
		t.Fatal(err)
	}

	mux := http.NewServeMux()
	f.Mount(mux)

	req := httptest.NewRequest("GET", "/", nil)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)

	got := rec.Body.String()
	if !strings.Contains(got, "OVERLAY") {
		t.Fatalf("expected overlay index.html, got %q", got)
	}
}

// TestFrontendNoOverlay verifies that with no StaticDir, the embedded
// index.html is served (current behaviour â€” pre-overlay).
func TestFrontendNoOverlay(t *testing.T) {
	cfg := Config{BrandDefault: Brand{Name: "Test"}}
	f, err := NewFrontend(cfg, NewChainRegistry())
	if err != nil {
		t.Fatal(err)
	}
	if f.overlay != nil {
		t.Fatal("expected nil overlay when StaticDir empty")
	}

	mux := http.NewServeMux()
	f.Mount(mux)

	req := httptest.NewRequest("GET", "/", nil)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)

	got := rec.Body.String()
	if !strings.Contains(got, "doctype html") {
		t.Fatalf("expected fallback index.html, got %q", got)
	}
}

// TestFrontendOverlayMissingFileFallsBack covers the case where the
// overlay dir exists but a particular file is missing â€” the request
// falls through to the embedded FS (and ultimately to index.html for
// SPA-routing).
func TestFrontendOverlayMissingFileFallsBack(t *testing.T) {
	dir := t.TempDir()
	// Only index.html in overlay; no other assets.
	if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("<!doctype html>OVERLAY"), 0644); err != nil {
		t.Fatal(err)
	}

	cfg := Config{StaticDir: dir, BrandDefault: Brand{Name: "Test"}}
	f, err := NewFrontend(cfg, NewChainRegistry())
	if err != nil {
		t.Fatal(err)
	}

	mux := http.NewServeMux()
	f.Mount(mux)

	// Request a path the overlay doesn't have. With SPA-routing, the
	// handler returns index.html â€” which IS in the overlay.
	req := httptest.NewRequest("GET", "/some/spa/route", nil)
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, req)

	got := rec.Body.String()
	if !strings.Contains(got, "OVERLAY") {
		t.Fatalf("expected overlay index.html (SPA fallback), got %q", got)
	}
}

// TestFrontendEnvStaticDir verifies $EXPLORER_STATIC_DIR is the env-var
// equivalent of cfg.StaticDir. Empty cfg + env-var-set overlay should
// still pick up the overlay.
func TestFrontendEnvStaticDir(t *testing.T) {
	dir := t.TempDir()
	if err := os.WriteFile(filepath.Join(dir, "index.html"), []byte("ENV-OVERLAY"), 0644); err != nil {
		t.Fatal(err)
	}
	t.Setenv("EXPLORER_STATIC_DIR", dir)

	cfg := Config{BrandDefault: Brand{Name: "Test"}}
	f, err := NewFrontend(cfg, NewChainRegistry())
	if err != nil {
		t.Fatal(err)
	}
	if f.overlay == nil {
		t.Fatal("expected overlay populated from env var")
	}
	if !strings.Contains(string(f.index), "ENV-OVERLAY") {
		t.Fatalf("expected env-overlay index, got %q", string(f.index))
	}
}

// TestBrandStructHostMatching covers every hostname shape the unified
// explorer's IngressRoutes hand it: the per-chain brand must win over
// BrandDefault for both UI hosts (explore-{slug}, explore.{slug}.tld)
// and API hosts (api-explore-{slug}). The matcher must NOT bleed across
// chains â€” a hanzo host must not pick zoo's brand or vice versa.
func TestBrandStructHostMatching(t *testing.T) {
	hanzoBrand := Brand{Name: "Hanzo", Coin: "AI", AccentColor: "#0b1220"}
	zooBrand := Brand{Name: "Zoo", Coin: "ZOO", AccentColor: "#7c3aed"}
	cfg := Config{
		BrandDefault: Brand{Name: "Lux Explorer"},
		Chains: []ChainConfig{
			{Slug: "cchain", Brand: &Brand{Name: "Lux C-Chain", Coin: "LUX"}},
			{Slug: "hanzo", Brand: &hanzoBrand},
			{Slug: "zoo", Brand: &zooBrand},
		},
	}
	f, err := NewFrontend(cfg, NewChainRegistry())
	if err != nil {
		t.Fatal(err)
	}

	cases := []struct {
		host string
		want string
	}{
		// Bare slug + slug-prefix shape.
		{"hanzo", "Hanzo"},
		{"hanzo.lux.network", "Hanzo"},
		// explore-{slug} prefix shape (lux.network UI hosts).
		{"explore-hanzo.lux.network", "Hanzo"},
		{"explore-hanzo-test.lux.network", "Hanzo"},
		{"explore-hanzo-dev.lux.network", "Hanzo"},
		{"explore-zoo.lux.network", "Zoo"},
		// api-explore-{slug} prefix shape (lux.network API hosts).
		{"api-explore-hanzo.lux.network", "Hanzo"},
		{"api-explore-hanzo-test.lux.network", "Hanzo"},
		{"api-explore-zoo.lux.network", "Zoo"},
		// explore.{slug}.tld shape (brand-owned domains).
		{"explore.hanzo.ai", "Hanzo"},
		{"explore.hanzo.network", "Hanzo"},
		{"explore.zoo.network", "Zoo"},
		{"explore.zoo.ngo", "Zoo"},
		{"api-explore.hanzo.ai", "Hanzo"},
		{"api-explore.zoo.ngo", "Zoo"},
		// Mismatched hosts fall back to BrandDefault.
		{"explore.lux.network", "Lux Explorer"},
		{"unknown.example.com", "Lux Explorer"},
		// Cross-contamination guard: explore-spcfoo must NOT match spc.
		{"explore-spcfoo.lux.network", "Lux Explorer"},
	}

	for _, tc := range cases {
		t.Run(tc.host, func(t *testing.T) {
			got := f.brandStruct(tc.host).Name
			if got != tc.want {
				t.Fatalf("host=%q: got brand=%q, want %q", tc.host, got, tc.want)
			}
		})
	}
}
HÁ<ÅlˆÌ[x óÿ¡#¡#�*3±+v‹Úg‹¼1x øÿ§±°±+Úêûcx: ÅÿûO£G°Ü“¤“ù¤if “À!c.Slug+".") || host == c.Slug³#"Ø�‹Ìg¬#x øÿ›,Ÿ°Ÿ.Þl…Þx óÿ²$Ô#°³bÐÏyâyxÒ -ÿ£G¤;°_“	P
	index    []byte“è*idx, err := fs.ReadFile(root, "index.html"“d“<@“õ+, index: idx},³	 ³W?embedded SPA's named asset if no
// disk override is configured³¢Œ“¼fallback); err =³PS	ÍD¢®xn ‘ÿ100644 icon.svg ^pý”`9«ƒ8(í=ëÙü0¸100644 index.html §Ož;ùÁ@’6‰w4²ÑLÏ100644 logo.svg ¿ÛÃž—ìÿ6Ñ•£„µ_|nÉ%€Ê¸,(³xcœþ<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Explorer</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" type="image/svg+xml" href="/icon.svg" />
    <script src="/envs.js"></script>
  </head>
  <body>
    <div id="root">Loading explorerâ€¦</div>
  </body>
</html>
P{pÛï6ˆÓPxo�ü¡#“!�*3‘+h0.1.0“Ñ‘«	!0.0.0-00010101000000-000000000000“Ñ‘ê
0‘õˆQProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a8“!
“}š!decred/dcrd/dcrec/secp256k1/v4 v4“`“Hx2.5“A³Ü½replicate v0.6“Á“T10801064154-80525e88d958“A“¡5“qib/pq v1.12“Q“]<0.7“q ge v1“`“À“ä-“<]ontainer v0.0.4“q“˜“Õ database v1.17.44“q
ids v1.2.9“q“	“R$“_	 “À“r:“R!“Í02“Q!“	/v4 v4.9“Amagefile/mage v1.1“a“E	S4“�
“€	“»	“€
“ã	*“›
2“Á“È
�ierrec“Ã(“®“‡t7.5“A“®procfs v0.19““I51127171934-4e34e03a991a“A“˜Qsuperfly/ltx v0.5“±“2“±“n3’^2’$otel/metric v1.42’$“¸2“ÈF2 v2.4.3“€crypto v0.49“ñ“Z312153236-7ab1446f8b90“€#4“ñ	net v0.52“ñ“é-2“ñ
text v0.35“ñ“[
3“fJ“ÕA0“h“3[48.0 // indirect
)

replace (“Ñgraph => ../graph“Ñindexer => ../indexer
)
"Y6ì…x, Óÿ“!‘°{“æ›³¿Ž“x²rypto v1.17.45“�³…	€=ì;x, Óÿ‘ô�“1.2.0“ ‘¬1.4.1±Õ‹9“`“³ƒ	<mÓê:x* Õÿô¹�¸5-0.20260524134132-0c0ba56e32d7±¹bª1â¦ˆ¸Mxb�å÷›°‡°nProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a81 h1:TBzelXBdnzDy+HCrBMcomEnhrmigkWOI1/mIPCi2u4M=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260215031811-a0ab0b218a81/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQk³``“m“4ö00 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc“Z10³ŒP22.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug“¾!92.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0“`#“Ð"³‚"­“}126.0 h1:YxjIynlo3iKtpf6GeQngeB5RbexIKPPKsZIoq5EHe3E“1“}196.0/go.mod h1:PBvHNQ7cXBJejTWDWxXWvtUwjT5O66pTC7npxLWu8H0“q2+³B2eG10801064154-80525e88d958 h1:qxLoi6CAcXVzjfvu+KXIXJOAsQB62LXjsfbOaErsVzE“î3*10801064154-80525e88d958“04\05 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE“¼4&5“ã4D³ß6Ž01 h1:x1nbl/338GLqeDJ/FAiILallhAsqubLzEZu/pXtHUow“�91“¸9û20.7 h1:ksHieAp50umwqxqgyHk9WiOmXM54kia3IEDb5H7FsM8“!=Baccel v1.0.7/go.mod h1:iZD3oxffiMEIT/KvzD8bgwC/cBn4AYlMW3QJpbRa4RE“!=“M;24.0 h1:tU5q65RQSQdaVq64Z/DVUhiPQMoMPQT6pr41LsvG0AQ“!=“M;4“¡;K“ß<“�<™“*>³æ=V03 h1:bcqUO8twMOhHWCgN/RbjfWulVPbvUQcxDkssaRrCx4g“!=“/?73/go.mod h1:ENkJ121cmDEkwQPDiKK4QhnTnW9u37PGpepbrdVcAmc“!=“,@“è?¨00 h1:VtH6kvZrCEjCnkHPkhExDU+GJ0ZulYX4rnA+lXJdJHw“Á@ 70/go.mod h1:ee525i8Recbpb0jVTDZYZBr1MmvJ27OITJHZ/nlNMBw“!=“~A37.44 h1:hfiTls7sqbweW+o4iaZqB8P997paC+vpgWmhN6v5MJ0“!=“~A:7.44/go.mod h1:6Ey5y3I0WNLHbxIlIdFqUuKfBg+b0fAgTA8FgRgQ8zg“!=;geth v1.16.69 h1:CHO6xTZ+A+3itk94ts4uyVRJajNVP3RxWTjJp5qGOl“ñIBgeth v1.16.69/go.mod h1:8eEO1hW5sa6OH2VeCMaCPnRz28JBxFvCPBCWPLsU2c“ñI“|B00 h1:jwOXnlDGfx2TjqE5GhP5T3Rv9cGh7jHSfV6DceggmAc“!=“|B70/go.mod h1:bf35zaU3Nm5tiJj1d5SoZP8DYYJl4aCVp32rOY5vj2w“!=“C	09 h1:+yjdhXW99drnd2Zlp1u/p8k3G23W3/1btJQ4ogHawUI“!=“C	79/go.mod h1:khJOEdOPxd22yn0jcVrnbX1ADa0GHn5Y74gvCzN5BYc“!=“ÀC04 h1:r/c47r6WEK+EfkTeYCj3tWKTH5dUWQauuLUmK/jDvBw“!=“ÀC4“ZDI“ÀCC4105612-65220f1104cd h1:s34y2+uPyyqsXuJKWQu5bL2Y1sWevkBJr2xF7eRkrFc“!=“ÀC4105612-65220f1104c“YDJ“ÀCC4130605-aade4dd9fd4c h1:OCaB/KxgU8hrKtIBMwSoA8mt2Pf8B+23UhKQf0EbRDo“!=“ÀC4130605-aade4dd9fd4c“ZDI“ÀCC4134132-0c0ba56e32d7 h1:vgt+Q5m+lJzLLIuErWkjg2lbbKE3wtoZD9X41MoRokg“!=“ÀC4134132-0c0ba56e32d7“ZDR01 h1:rIfFRodb9jrD/w7KayaUk0Oc+37PaQQdKEEMJCjR8gw“!=“ïD	61/go.mod h1:64IE3xRMJcpkQwnPUfJw3pDj7wU0kRS7BZ9wM7R72j“ñI“�E12.4 h1:iVz7s0gToCNUU/2cLT8gUa8MLzc0YRp4jBmeXBeKdX“ñI“�E92.4/go.mod h1:i+Am9YvFsqDE75ZW8MPE62XCXGukXNiN/7mD9SKxxZY“!F"“òE¤/0 h1:VB3mQcETc9j5SY1S6lAgFtuGr/rjWuDgPYnxS+OKWM“ F“�F	60/go.mod h1:/3dheKVjUk2yiS/ocH1IDzeLXOIe+kpVsErIGDOZdi“ F“.G01 h1:6tVarXMnNR3Xzud8FYUHjtXabTll3HI/OEqG0tgLAcI“!F“.G71/go.mod h1:PkD4D4JoGuyKtfUkqPNYkrg9xKrJeNVZFdW/5XvAe/A“!F“ H
“ÝG–“I²03 h1:yDEuuJ6bm9H8DzOzzveXI1AZlu/AeSpHNRrY1akbyc0“!=@utils v1.1.3/go.mod h1:lskchZiYYRBFsSyYg3NWLPw211uRyxIs3/wAzrfSI“ðI9zap v0.2.1 h1:5uU3Z47Nu/FNcLqYeKZ4B0f+0ZDF79/2M/9FRN/1/Hw“!=@zap v0.2.1/go.mod h1:CobyEg6MNO6qZ5wn3kVEw6QCxFVZvvGMnEKsC5sJZtI“!=>zapdb/v4 v4.9.3 h1:KV1DrbZqtZsZvrWyPw5D8WHR0DxzqW2jHjqx6obxgoM“!=zapdb/v4 v4.9.3/go.mod h1:BrpLaDeYy+smON4ASDPCFKWrQ58CXhy3WNEPGna7vuU=
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mar“›K×04 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE“ÿL“¸L4“ÉLY/0 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE“¡L“hM60/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3“¡L“N	³ËM“%R22.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o“R92.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc³oR£ierrec“Ñ0cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4³«V@“ºY³fYF17.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI“ZCommon v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw“�\"319.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws“�\":19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw“]]+G51127171934-4e34e03a991a h1:r4YWl0uVObCbBFvj1VsIlyHzgZwZOHvY1KdRaQjzzUc“]]+51127171934-4e34e03a991a³ ]é“~a³ bPsuperfly/ltx v0.5.1 h1:vGUeBhKvBKZ2s2TsTrOSahz+m0PswfqOWDAD+ICiiYY=
github.com/superfly/ltx v0.5.1/go.mod h1:Nf50QAIXU/ET4ua3AuQ2fh31MbgNQZA7r/DYx6Os77s“9d#22.1 h1:J4X2hrGzJvt+wqltuvcSjHQ7ujQxA9gb6PeMs4qlUWs“9d#92.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ“Že 23.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw“Že 93.0/go.mod h1:AafrCXVvGRJJOImMajgJ2M7rVmWyisVK7sFshbxnVrg“Že?nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ“ŽeFnottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo³!hi22.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho“iBotel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc“i“*l12.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/“i“*l92.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI“i“8m22.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY“i“8m92.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc³€mL03 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0“ün63/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s“q“op“	pÔ349.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4“q:49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA“ÇqE312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA“ÇqL312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ“Ÿr“Hrn24.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI“Ÿr94.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY“t³Ws¤22.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0“t952.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36S³|uþ96.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg“Žw342.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo“Žw42“Åx 25.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8“ýx95.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA³èy!23.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s“ðz83.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk“Œ{ø26.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk“k|96.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E“‘³}“ÅƒÄ“ô„—27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis“½…97.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0“†22.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw“†92.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0³­†Q02 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo“.ˆ2“Hˆè0.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo“þ‰³Ï‰4J1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4“M‹í348.0 h1:ElZyLop3Q2mHYk5IFPPXADejZrlHu7APbpB0sF78bq4“mŒ:48.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig³¿Œ8È7ƒÙà´+x  ßÿ¹¹�Æ0605-aade4dd9fd4c“q±êO»•Dâ´"x íÿ°‡Í…°¡A“ÐB
³ŽB"AOXµá´{x! Þÿ¹¹�Å05612-65220f1104cd“q±êO°H
Hâ´sx íÿ°‡êƒ°¾@“ÐB
³ŽB"AR$ìã¶x3 Ìÿô½�³(d5e65d4b50c127ce8982ef94f448dd17822448f7±¹b´…âncx ñÿêƒ‡‚°Û?³¾@,A3Sc¦x& Ùÿ100644 docker.yml ÕCï¡bájŠ{‘!Ù_®Gj`ëGq~ê…‚*x*ÕþŠÕ�…Multi-arch on luxfi org's native amd64 ARC runner pool; arm64 via QEMU
# (matches lux/graph + lux/indexer docker.yml). Mac devsP run native arm64
# under OrbStack â€” zero Rosetta is allowed. Semver tags only“ühanzo-build-linux-amd64“$Iqemu“í#platforms: linux/arm6³PÃ
,linux/arm“ùªiŠæ�:x éÿÕÇ°s	lux-build³ŠKbs¥&x31 ½ôÌ’ŒÒ$Õc5ßc={’².[®9a%ñÀêµ¡��™‰	XIfz^~Q*ÃÄ·QÝÉaŸŸÝÍŒ2?È¼QøªÑ®PU.ùÉÙ©Ei™9©?âÔÂÖê~ÛßÊ|^8yÃóï÷ ª‚\]|]õrS–ë›œl/hièSøuÁ_™(ÑPEÉ‰™yÅz©‰¹9©z•‰¹9ámœýÍn[é¯�j|áYnËŒ¿[aÊósò‹�ê€ÊúÊ
þ�[8ÿùt·y³¥ŽgÏY_YÌU–ž¯—›ŸÂ lð�aëKîGÒ¦Ÿ×ÕÜç¶v:ÄƒPQ\šËÐtðrfóæ«>9wvŸ6µOÓˆ;ÍU‘t˜^z>Ã;¥è¿wUßrðìIórýÜ«úaJR€N*aðãªd“2ØTâ´”cMÚáÕßì�JÌMM
K’3tSs“RSôŠ3ÌÔÎ®–ÖˆlŠg/µ/÷ú£µþkØ¨aE©‰9%™¹© .¬ù³°Cni‹¶ª¿fÁ¯/«þ )‹/I-.©íxüÐ¨y‡îâÕ—>8×÷¼qì{Z	®6=³¸¤¨¤,+ò×�ÇjïZâxéeUdËÊ`4ep#Ÿ8¸ø“öªÚ¨´½,™û=›%Kfê›€“PqrQfAI1Ã='«%_þíò«³-Wé·méˆÓvÝ ,Ýô‡ëƒx+ Ôÿåå"40000 .github jÊÍe}™Z²‘Lööuú°£‘Ö±"CdÈaë:x+ Ôÿåå"40000 .github ™ù“Ì#+«{"È»9Š<;'Ý"±"CJS`ë9x+ Ôÿåå"40000 .github ^œù$Ë':r»y.ª™¯bI‚‘±"CM¢Çà9x  ßÿåå°__Öˆm2mÀWw1<&vÜ¢XN°“sòÙHIë.x+ Ôÿåå"40000 .github *ßŽ%w@KAë‰­¬<‚%êf±"C9Ìë9x+ Ôÿåå"40000 .github m¦ÏÊÖÁë­“P¨Ï�‰¤½ü±"CnF¹ï9x àÿåå�4‡ý‹õ(EßÝ2Õ£þ_”uè)°±Hðëì#-x<ÃýåÑ40000 .github ¾�òs¬åóÎ£jQ’Äö�ÊJhÝ100644 .gitignore EyTHò)Ïþ<WÍ®\wÝè100644 Dockerfile -YË!Afip„·H•Þt100644 README.md oƒ&;bk…èÉÜ›ƒÛV›vÿ4100644 chains.example.yaml Ñdeû.¤"?Ø�JÀâö100644 compose.yml àÓõ>”)S¢7>ðr¹Âš çÑ100644 config.go Ø½#N!,¾y«¾˜º¢¡ªä^Ÿ‘R100644 frontend.go ˆ3‰d�=­‹�<@>°5ßc&100644 go.mod m(Å›¯S¹žÂ^nãb‚àâ100644 go.sum 7uJ	û«œ‘âž!*‚Ž§gr100644 main.go BÁoX8Vøïâ0þ_9BõL““P&“ c@÷o:Åº±Z©jø¬šß+F)p�L100644 registry_test.go úäYs²¸5ÀZ ö½C±“C"J40000 static 0ûÄûƒÁ�1~ótHŠãT×4 ©100644 supervisor.go b|3ŒyaGŽ-,ûJÇ9'P5¢Ü£ëƒJx+ ÔÿÑÑ"40000 .github MxC{Vä¯±Q±cƒƒÊkhé§±"¯K`×á:x! ÞÿÑÑ°eÓƒÝš»wÝ¬dZ+Þq9ÏñÈ>³yXˆEå/xE ºÿÑÑ"40000 .github &Ò›_#ò{(ãaê*ðêÐºŠƒ±"C7uJ	û«œ‘âž!*‚Ž§gr³yXœ!7ïSx àÿÑÑ�®Ò[$rQ”³j¦Ù“ÄU!ÆÐ±ÂÖô[å-xe šÿÑóD40000 .claude äó,õ9ã‰ÿ…ž;·- åu40000 .github .õB��Þø·ˆô41œƒùd“*‘"8á_E§�Àf»Âs¦e`Rô¶óø²±ncüõ+Ëàsx  ßÿóÑ‘"4¬ž'ê	‰×Ã€Ê!6ÐÂœã�¶>±j‰óZGã.xC ¼ÿÑÑ"40000 .github ”Ÿ~?×¦~¼®ºSñ<�¼µë›Q‘"8lŽ¼àÅÉêp¹æSÑ@jw±nc¢>ëQx+ ÔÿÑÑ"40000 .github ÓÇ,žd/`ìjÒ*^ÇëøNm±"¯K¨ï9x àÿÑÑ�ZL`>$‘T,]$ˆéÆ.�YøIuq±ncÁ¨bë-x+ ÔÿÑÑ"40000 .github «!r<M…S)0Úã=�Š—ÐÙÍ±"¯=v]ë9x+ ÔÿÑÑ"40000 .github äÛÎÏÌ±Ø˜¯äªAÀjAÞX±"¯{*¦á9x! ÞÿÑÑ°CtÍÖÞÍ,îAxÝ»úžžìëÆ³Wzkã/xC ¼ÿÑÑ°C6ø¬ÔCÒ�˜œVO¬k�¶ØóHBÕ100644 go.sum º@?èõ0hÁYò¿Øçs�� ›³yXbïQxO °ÿÑœ�uDLICENSE þ<¬:¸Mµ¶¥dŽPRÉø›100644 LICENSING.md __"]À+Ò°?�ãÿ¯®�×YÙ±ncˆÚ%ë]x+ Ôÿœœ"40000 .github Ø—ƒä¼Ò¶AùE›oþÜ�XO÷Äž±"úsOã9xC ¼ÿœœ°Ž6‚Xó& ÙR²ê–H»)ü°î¶%100644 go.sum 	‘tt
›à»›ÐohåÑCEÊ˜ç³ÄX½¥ãQxC ¼ÿœœ°Ž6–´<V+ýòdþ_­æ SÓqÊB]100644 go.sum ‹nf#"7ó?ùÈ]!0GGˆOB:ï³ÄX©ÏëQx+ Ôÿœœ"40000 .github ÞbGáws‡MM½‚5KÖr]±"ú+ 'á9x! Þÿœœ°lX(´ÊýÈëãñqÅ-µÆŠ
]³€œìÂ¹ä/xd ›ÿœÇ°Ó’•
ëExöšW8Äê[SGRÁê“ç6?‹�ŠwY��Z<Õ&U×£(®ú§100644 realtime_sse.go “aþ£Â¡Nf%¸l\ÖÂòÈ^“1ëŒf+�ërx{ „ÿÇÇ°ŽYõŒ5•3Öd "J›„1x28100644 go.sum ²6}üÛÃSf„|({rŠÓ]é½¤100644 main.go ?¦ˆÍ\ò\®$·¬ÿƒPJ¦²�³çLÏÖ37›pUëì€•L¤#@ˆ[1ì€	x ãÿÇÇ°38ÿ ?-#øAZ^»jD¬·~ÓÝp—ƒËâ+xR ­ÿÇÇ°ÓßGª/× Ïsý´÷ÛÜ�®Ìµ“ç6zY¥‘Ÿ¯ª»W/rb4gÓŒÝë“1
íe ìSrt²ãŠIØËj™S“\ë€&¾à`x  ßÿÇÇ°HZ*‚ø’Ú¶%éáe�&…Ê“\ëÊÞLá’�xœ›¦²\h+ÓdcV¿ÉkÄ'¿aœ|‰Íq²4»áä`­É‹8ŒêÕÔ
ò
r’J3sRjjJŠJS¹‚Býr³S2‹tô
ôóKK€j£tÓàú™y)©z%¹9
± ½1\
@�šœ‘¯ n£˜’Ÿ\RY�ª ’·³)É,ÉIµs­(ÈÉ/J-²Ñ‡ðulR2Ë2Sl•ŠòóK”ìlô�|;u;l–lÖäÚÇ¨æì© «›V”Ÿk$òJRóRnÔÓ/.I,ÉLÞ|œÏŸ S*UAlÔ5x óÿô›�¸4±¹b$5#l†œ=x óÿ¾o•o°C³lR"%yâàŽ«gx°OþÊTÌQ°_SSE response headers â€” set BEFORE the HEAD short-circuit so
		// the pre-flight sees them and the SPA's `u.ok` check passes
	4	// (200 + text/event-stream â†’ open EventSource).
“p8“�%[“�Ë// HEAD pre-flight from EventSource open. Standard HTTP requires
		// HEAD to return the same headers as GET would, with no bodAy.
		// We've already set them above; just acknowledge and return“J*“«,“&.³–Ó“r6“�%ˆ“N&“«,³&5”˜“ql‹ïvx óÿŒ=�:°ì³w"r�åŽ½oxU ªÿƒ5µ.°‘“qframed“�A#framed = append(framed, '\n', '\n')“O"framed)
	}
}
“@³£àÌöBàƒ@x@ ¿ÿÌQþ:°÷“Â“t+“¸Fif c.channel != eventType {“ŽL’body³#y/'.á„*x! Þÿ•o‘h°€)“s“ž3£Ý,“".rest³?.V	ÝQÑgÚ)x øÿô›°;¦è£YxH ·ÿêƒ‡‚°A?/1 h1:6NXqd1aDXvhIN13U0O3UvehDjG/9fQ0tb+w8ZpTjWs“�>“Ü?1³u@uAc´á‹òax îÿŒ=Ö7°€³«A³wA1ê€=x åÿ‘h¦^°Ò³ü>³™“n³ý—s	ÿá‹ó+x îÿŒ=½0°€³«>³>wüäéx$ Ûÿ¤;¸1°“Q“/
n“.“/“.“–P³.vÌË
h¦x& Ùÿ100644 docker.yml {92þj‘ÉØ‰8Éé¬b8«—âà†á	¦x‘ nÿêƒæ€°<37.45 h1:uGK0y4+aLipE/M0YIQ5hcsWv0ZG0E4cPv03a94K/eLE“!?“ù;97.45/go.mod h1:GnAkhQ7HNs3X0Tzx5nOONS3kl0yRmWHbDcRO5ffILs“á:³ú;ø“•>Ÿ³¾@,Ak0Æn€ x ñÿæ€½ÿ°<³­<¹C4ø®¦x& Ùÿ100644 docker.yml Ú€çlœkç‰YÆ¸·×û|°`ïe)Êà¦?x ïÿÕÓ°slux³x]6÷Ìç�x èÿæ€œþ°i7³8–“œ=³Z=Cz5ûlßUx óÿ‘‘�*1±+f¯ž¦x& Ùÿ100644 docker.yml £³×—87ä:ße³ >£o²C,¡à§Qx` ŸÿÕƒ�¦LARC runner pool. arm64 via QEMU until a
# native arm64 runner joins the pool“B1lux³x]C žêoxz …ÿƒÁ
�…
amd64-only‘�.Ywill be added back
# once a native arm64 scale-set is provisioned (CLAUDE.md: no QEMU).
#‘óŽ³Ú¦“Œ÷"ò*Ê¦x& Ùÿ100644 docker.yml ­3â+	ë«œÛÊ³’Xi¼…ŠÃŠ]5ãŽ>xœ5Œ1
Â0FQ°BGApü@p³7D'”â›4&ù%IQ'Ñ+®‚rsòÆÁñ½�ïÝ{·ÞãÓTÌCÙ‚¬W>P°¢RV6ídyÝ)¬p,HÊ|mP:2•H‡8::_àÃo=	°"ÔLëhD„€}­4GP&’åðuD,d’4³2#'³t“¯àÆå??a±ÞæËùlBeÙ�†88�¬&ÆÓß£y%»ç´ûn}à¥D¨xH ·ÿ100644 ci.yml êþÿòÅiæöVäPôy˜� sÌ100644 docker.yml ­3â+	ë«œÛÊ³’Xi¼…ŠÃŠWa L·Pxøúname: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read

# go.mod uses local replace directives to ../indexer and ../graph. CI checks
# out all three repos as siblings so `go build ./...` resolves without any
# network hop to a tagged version.
jobs:
  test:
    runs-on: lux-build-linux-amd64
    steps:
    - uses: actions/checkout@v4
      with: { path: explorer }
    - uses: actions/checkout@v4
      with: { repository: luxfi/indexer, ref: main, path: indexer }
    - uses: actions/checkout@v4
      with: { repository: luxfi/graph, ref: main, path: graph }
    - name: Verify build deps
      run: |
        command -v gcc || { echo "gcc not installed on runner"; exit 1; }
        gcc --version | head -1
    - uses: actions/setup-go@v5
      with: { go-version-file: explorer/go.mod }
    - name: Refresh go.sum (proxy cache inconsistency)
      working-directory: explorer
      run: rm -f go.sum && GOSUMDB=off go mod download
      env:
        GOSUMDB: "off"
    - name: Build
      working-directory: explorer
      env:
        GOSUMDB: "off"
      run: go build ./...
    - name: Test
      working-directory: explorer
      env:
        CGO_ENABLED: "1"
        GOSUMDB: "off"
      run: go test -race -count=1 ./...
C‘Œd¨xH ·ÿ100644 ci.yml Bñ©‘Ž7Óêƒ[2t«w‰ô×è^"100644 docker.yml ­3â+	ë«œÛÊ³’Xi¼…ŠÃŠ!ä	‰ix” kÿ‡
Á	°uInstall build deps
      run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends gcc libsqlite3-dev
   ³:“]q“å"™è1¬k�x ôÿ¤˜°£$>  x0 Ïÿ100644 scheduled_tasks.lock �Sã»îg	Õ«³FÙÝit)™š®ºxZ ¥ÿ{"sessionId":"cf4e8ffa-52d2-4e07-b22c-dbf44cba2479","pid":5284,"acquiredAt":1778041169393}Êï¯gŒ‰qx øÿ±¨°(
›7¨xH ·ÿ100644 ci.yml Ò5TÁØ·´µ¼¼[¹jx6100644 docker.yml ­3â+	ë«œÛÊ³’Xi¼…ŠÃŠ¯Pìj‚Kx
 õÿÁ	�°N’ÁÆºo�\x ðÿ¤�°Ú“ã5£$1ífì‡�xìþ»/¾�š³º`“´zmainnet“-A<
    indexer:
      poll_interval: 30s
      start_block: 0
“‘¶“ÿ“^7    “¯a    logo_url: /logo.svg

    tokens:
      featured: []
      allow_list: []
      block_list: []“emAmainnet/ext/bc/2Y625Kvdd4wZ5LKx9kLSf1jGALyARmXVN1YsTb4F3Hzv4bPDbp³ÜhAmainnet/ext/bc/2GiQb73CeJESjc4omFv2YtQHZrRgJf25NXPzAr5J6UNHRcDV2F³Ph@mainnet/ext/bc/rtjwvtE1tEvrokmpeYdTq7b2zqZgmybKwR5MLjKMGAR1W78dQ“Â¹Amainnet/ext/bc/2pUskxqaL5Bpx7uRUGG1fDjPckjxQ4UKX4sLKeaS1NdSVBJd3F“†K,¶¡¨xH ·ÿ100644 ci.yml ïÅê$¨ÏJà˜«¬qvT¢-xÇ100644 docker.yml ­3â+	ë«œÛÊ³’Xi¼…ŠÃŠ°”n‡Qx ñÿÁ	‡°o“÷W’Á*=´é˜x9 Æÿœþœþ°0*5Rl34569IHJ5hXOvrkjbadfKNt/2rGZd83vhcSIRSP³10ëNI“¯xO °ÿ100644 actionlint.yaml £…Àj @\/B|öÈV5ƒÇÎñé40000 workflows nÊ9w�Õ~ÿÐ4YQ_ïí‡3ºa²�!é\x æÿOO�;_,�ïêýtÏ ]£§£¦Aá¦Ž‰©¹xy †ÿself-hosted-runner:
  labels:
    - hanzo-build-linux-amd64
    - hanzo-build-linux-arm64
    - hanzo-deploy-linux-amd64
c&ú§xw ˆÿ100644 ci.yml ü0è5�-FW,ÏòRo±RG�)ó100644 docker.yml bU/ÖÑ>4qÍÜZþˆ�Xq/åì100644 workflow-sanity.yml ~¨Žzêƒ‰2–/V)(ßõ„Á>o¯.Íì€x ãÿww�4-0V7óY=¦,±x»Äëå´]ƒ¬ê‘H/²ö¶î+xN ±ÿwwH100644 ci.yml mÌ�ã£Ò°’Õ1£IeiVƒZÎÂ›100644 docker.yml ã÷Éè‡Ç…Ü_amþ1}ó79%‘H/ô!nè\x( ×ÿww"100644 ci.yml Ñ+_È-Â‚äýhu….¡ã@�‘"USã„.x3 Ìÿ‡ù°Ehanzo³H±	|
       “øIrun: |
       “i�£Ü¾%x^¡ýname: Docker
on:
  workflow_dispatch:
  push:
    tags: ['v*']

permissions:
  contents: read
  packages: write

# Multi-arch (amd64 + arm64) on native hanzo ARC runners.
# No QEMU emulation. No GitHub-hosted fallback.
# Semver-only tags (v*); see hanzoai/.github docker-build.yml.
jobs:
  docker:
    uses: hanzoai/.github/.github/workflows/docker-build.yml@main
    with:
      image: ghcr.io/luxfi/explorer
      platforms: linux/amd64,linux/arm64
      runner-amd64: hanzo-build-linux-amd64
      runner-arm64: hanzo-build-linux-arm64
      runner-deploy: hanzo-deploy-linux-amd64
    secrets: inherit
+qÇ½	x� bÿname: Workflow Sanity
on:
  pull_request:
  push:
    branches: [main]

jobs:
  sanity:
    uses: hanzoai/.github/.github/workflows/workflow-sanity.yml@main
2§5å·)x—hýname: Release
on:
  push:
    tags: ['v*']
jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          push: true
          platforms: linux/amd64,linux/arm64
          tags: |
            ghcr.io/${{ github.repository }}:${{ github.ref_name }}
            ghcr.io/${{ github.repository }}:latest
ƒ¯Çaï„$x/Ðþ—Ì[name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
jobs:
  test‘:!‘—7‘¬setup-go@v5“go-version-file: go.mod
      - run: go mod download
      - run: go test ./... && go build ./...
  docker:
    needs: test
   $ if: github.ref == 'refs/heads/main'±;main“.“�ú\Mà�>x` ŸÿÌÇ�"
    - main‘)
    - main‘N#hanzo-build-linux-amd64
    steps:
“ç$‘«"with:
‘×$‘ý“'HPà´ÀxÅVQsÚ8~÷¯Ø£�Ò»«ì@ÚNË„Ì0	SŠ9Czé´FØëb,×–®Mfîéžïá~aÉ­d;i2¹·ò h%}’v¿owA˜mWÜbÛ8	KàÛ_ÿB_l–<b~xä³-Z…uBã ‹…°JD$Yä�ŒG °¼ÄäÂÊÑÊm ßþþ¼€ò|*)<]†Â;OŸ�Üª/qÎ"üõ*¡žL¾+?�ÐXi¶Ìì‚E2…§é%�""ÄŸògp¬îúÛø.´òú9Ú„m¥ùGZ{ÌwOÙ9F?xÚ† ƒ„1¼yî)í›z�²Ä"Â»�G#\™
H²RI#Ÿ†8+‘h¿Æè=µ.õhÈ£µi=÷Ž�Å;Û��œI·e¶_jÛÄØ;k»­m£ÉÀ>³Ý…kO�n eœv,kÍe�-M¼ÃÍ@˜8qk×°»Á¨äGº½éÉ@Úé;˜ãbGÄ>›Ž×~ ¦ðæ¨ÚU@!~`&éšA«Gý]€ô¤1t�·	Ÿu­ûæšÐ0FâBo¶Û¡_X3un††{:Ÿõ}„�ñ0Ž”Ë~wÜ7ƒ‘�c½�àé(â³XÝîY&4ò‚îã¯Õ9Ãk¨§Î5˜ÁC•ÅÔ;ÑeÈàÉˆ£xƒŒ@Š ¯Ae}aQBYñ�öäLì³ùbzz4õ½éhqÔ›Ù‹io~Òµ.Z;ñæKYœé4ë6ÙÐ-¡OR<•‘”aÝW­×í†¾MÊðÍ©%2g²ÍŠ©ÍÊˆq‰lZˆ”dP°+¾VÊ¹º™dLƒéw,u¼êæÍ¹Ï ±ö¢‚Toþ dµ3X:_˜�Ü„ðIA~4”Þ™hü4púó÷SÔüáAñÍ¨x ¹Ù¡]ä®+X�ž]
ÿÏÃ«øÑûšpx×©ÆMÚµKÚÀDHÎÒóÖ"¤Ñ¹WiµÎ<í–ÜÒ[A¼J¶Ã[Ú»1¼Ÿ•ž›,‘}�~¹dúo)ð2wà´zT®ž"Wt.ŠúPºvaEéjXQz-`#|ðÅe
ê}gú>çñ¦[–‰�×!6K,oãï˜k!ë$÷êœl¦W˜ÐD‡m–HÒ|ÞúøKÝˆzéäWÍ{pËóM¼dûÐB·XQ†2«´�q´'½£±=@9«Q8î+õcLÊöp4¶_>_ÌœS·o7 ±ršç<%¡¿
é:ÅõH“K g r«yÁ’”‹óB•ëRªt˜ÚE<,ýþq�]•ËïS­ŠCº TÊ‡ÿƒ?»R¡ØSîçŽBüÐï)¢¡ÿë§×JÑ~Ü,’|Ãj™ /7�}³ÝÒ¡»£Œx”x,‘|Å=*1�šù2½!”"kÜd‡•¥‰……€†uÒÜ¿¯ŒÜÚXÚïß™¿ÿÖ¶<ÈÈ9M.¬ReFîßÏRlÉ H/_¼Øo—mcbœÎl·½sÆ§om°Ts¨«× 7ï-°ävµ	Næóé¢7¸ÝÎ«½×{XgfƒúÏ÷ööãÄî�ç'ý»ÿÃ±'M.hØÝßS[…N÷…`¦H$‰YÂ…ßmá¼Roÿí .×Lùì¬02Æ6J{<©Ô‡ªjÊ@¥¶ÅúßR¢°ÍB¬—ª¹“Ø.lpÄIÞê–î†§…°kýsé*Õœ:˜!î3¸Ä¾­ƒ	Ô;G·)DÓ4w½7)P½œ�$öÔ¨}Dò@Ój½=™»ï§Îh2‡�òŽ�OÆ~DØù»Ïxû$Û# Explorer

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)
```

Each component runs standalone for independent scaling, or together in one container.

## 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
# 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

# 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

# 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
```

## 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
```

## 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
- [`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)
©�3»°xôô# 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"
×‰jÂê€–|xœ›(¿€e‚‡ni^fZfjÊD…œ‰/ø&«3ªNžÃè1ù(³áäóüLfF“×óJsææ§(”èZLNàUa22R²œÅ•yÉ Q¯Ä12ñdJòós |=}}…Ì¼”Ì¢Ôä.M. 5$`î5ÿxœ]Î;¯›f Æq�iÔ¤ª²$Qn=;�¹Øs”¨²æb›‹1`{9áfÀ¼€Á˜Û’-S”åÛ¹c—Ó©CU© j·~ƒ.ý
Ub%‘NûþÏïùáß³wO ýèOør^>ë\=zæžÁŸ¨›ð¯ï¾‡oø¯!Ç/q’8ðGÈGv‚£u‘‘Ò	po’�6Ši `!×ô¬v8_Pá+þñ€$P?íÄ©{Šã’ŽP´1bçÔ|™
Ó,ò�Å`2çEšYJ’)KæÊ–TˆËOáù5üÝú¾s¿€çnÜÅì´ÓGh Ùl«h®ñ]¢\+£ŠÝ¯ÓŠ±ç}Bwéz¤—°1ø›{ïâÔ]»,íxt�Iñ Ú‰^°Äœõ†¥T3êÕz¦Lò…/"”Õíã«o½Ï‚údO»ÒÎ©0DC—{n¬¹NmÊk^Ö£®kH³’�I9l ²½qÊ®Ñ.bJ˜Rêr>ÔÇU‰–‰æÈ½*áU¹ïèm‚.š‚…/ƒûðv ðOðú Ä[m”*´ª¬gXÝLE%Ã|Ç]®›IšÕKÓÞªG^èÿºã¶@ªé÷Ç;aô"Áv\_5Ê½Hæ­‘å}kÕ´›«Ûþ~sõ0œu	â£¼*Šl:N*}ÖŠ³,*4Õ¦» �2•†²’5ouÙ2…ñ½‹SwÍF=ðö¡%ÎU}ßCR¥69ÎÝØ"�åLXïdy9Ù(º3„¿Fàóˆ‡¿€ã«Ï¼ŸiÜl“•ÈP � ¶I(bÉ†Yp}n¤´aoJ-Ù_|é§ÀJüNšûh�i
ç%Öùß#k\9‡]VfÔ:+È¥Ô¬(u®^b«(W`vJF¹vÓcóqàÏñÞ-ô6ÁØð›q/Å=clÅPž†åˆÆ¤$ÝµE`¸ pëMçð«¢‚wÊ·ïAþ+è}€¤1xœmUÏoãDVZ°ii—Õ6ÐÕ¶¯^uå”Äi»°€Û
ºmÚTôMVå{ÜXuÆÑxœ¦¬ª²Gn áàÀ�#qàà†¸p]\¹sâÍØÉ&Ë²<žï½yï{ß{~ê>…/ç®h¯Åœr LðèÄ!ŽY©àÇ)a›–Û© Íˆ‚×"!NOÃDaÏCÑ‚öÖAü0ñâ.Åo„ùpB›õØ;£¡$Rú¥aä»°sxrxüáz Ëc®´ßÄ¹ íH¡òkY¹ÜöY²�(¡¸èÄ\À»Kï-åN«¬ëffG‡Ç¾þüük¨5G mlŸ$�2ãbfRýøhïêð¸züÉæáÁöîNfÓ!˜™ˆá?�)¸Fï«´ó(Óì\cbpšà)ÍOÛ¬mìfvC†±(d}v#äÖËÓ+·%DÇ­TÇ)}ÇCK+O~Ão£ÑÆÑnNÁNU1 Ç¦fîŸ¡¼æÙô©ª72Üü0Žø~ËQ�ûÞä§S/ÝÕ�´ãA‡Q[Õ½j£ÚG5ŠÓ6Ò0@i5åbÈ#=©KçætE£“´™x<lREz3Bñ$Á	Kˆ'Â˜%ÐAh_ò«Ï¢ä"ñHY¦a)‡–Y4Í e´aá‰iè‚»ëDäÔÙeÂ¶Ô«„Åêê¥Ò
.µJ`TbMÃ‹Yžn‡XTWÎÝx}�lkPûL)hlYÊtH$Z8ÊC&	-WòåU-“TÄå¾H 3%Q^è†DkHÙë°�O¨]”wÍï'LÃX+'áéfßä&Ô W­9õhØ¥¾23<Â<Ùøz‰Ó@¦7G›ÖæYSCÞÔEi¼2¿¢‘{1ñ!àq2 @L#`qˆ‘ùuÌžÈgãÓ3ÊbÔ†=*®Ê–ñþÊ¥yÍ/ò¨ýåc}4³Y[(B¥˜x§€&uA°~£%¿›Xõ{ýÑ­´i§ñ C?88¹lOôr
tqÛiÏ4ð¡NU}äÐó:å]ºŸöú\Õ”Ó–üæÕ_^³â3«„Œy}-W~19;>Ürˆ+È¥›ãrrjVþ5ù÷í!‘?‚ò÷©�[Úû`/D$¹õÏòÇÛ…EÕŸ”…ìÜì…¼“èÑ·Ð-";‹JŸrnzîJ…›ð®Jå¾ÎE'ÂQ÷Æ†ïswdBÐN½Ãq°¶…þû®Š*¿NìˆŽ˜ IjÃõ‘Ÿò§8––—`Dx…½N±Ü~7@d×ƒ7;á¡ £¸‡ÿÛÅh®y[^¹†û@÷I/‹îÑ… ‰Æ.ÃÚ,?Ä]¥<Ô„
Ù<Pª£œ+Æ�8ùÓ´X0±£P#ÎVÌ¨ê˜¤•
?>g›¢W‚þBîÞ{[^Þ¸§÷ý…^œz²è
oÊogÒpÄÈÙ;Ó/#çEÀ‡üuÆvº„S+G×§ì.ê^å>S-Ì©H9&ÿ™©Éßfn©RãŸÖkAW¶î<†ü·â>‡â�1jïL¥}iþ§A_âº	xš eÿ#!/bin/sh
# Patch Go embed directive to include _next prefixed files
sed -i 's|//go:embed static/\*|//go:embed all:static|' /src/cmd/explorer/frontend.go
Ù55‹µx êÿ# rebuild 1776010884
B¹_¤x$ Ûÿ40000 workflows nÊ9w�Õ~ÿÐ4YQ_ïí‡3ºa÷üŽ¤x$ Ûÿ40000 workflows É¿ þAé„ân¸„“ct6‘èúkçü;x èÿÇ½�rubuntu-latest‘‰¾n‘	Íì€ŠXx, ÓÿÞÕ�˜lux±�:lux“Ü'lux“(	lux-build“<"vÝo¤x$ Ûÿ40000 workflows  9[ñÚz¿ÝÇ—­>âLB’§Çðtrë€‹Ex; ÄÿÞ¦�2branches: [main, dev, test]‘-D“„secrets: inherit
nm‘¤x$ Ûÿ40000 workflows 2Aµ‡îŠ¥(ZLM`æ*³¼Ô²¥x340031QH*ÍÌIÑ«ÌÍap|—Ä*­cÎï—�o£}útï‚ö> Ìî/xN ±ÿ%n�%I100644 ci.yml äìtî&úí_‘ÏÂXU�œ‚100644 release.yml 0Õ*Y˜Ùº$ 4ƒ…øAzb'—^GC±‡xíUMoÛ0½çWC�CíöqÈ íæ¥Á¶¶H»¡ÃVŠÍ$ZlÉ�ä4AÚÿ>RJœ:mÑw˜/‰IŠ|�z¤•(°Ç•Ì3He®šVK«NàF›Ù8×7ÃLÚR¸tÊÆ²²þ`d„J§h;ð«R]“À‰	öç¯÷Ù`°ÔV:m–�$·,ýI©2\ ‰ª2³˜QN·¯0m×­ª9ã$½þÅåàg&ÓÔÄR“­ÿí¨—Ô†v^-Æ²½Î@¬þè‘å£#fË ¬3Tv²o …pF.6o R¥y•áÖ �©”BC,-æãhª­žKU-@Ùû·¡\€Ÿ2n¬MÑÓö!¾|ð?—ÑÏfäŸ‘°Ùˆ®öV«5›8à…»;Q¢)¤µR+ß†�jåP9º8ƒ"[#+E:¾¤#z+1-ëST–Ý"uœ¬MZHgºr‡ó …["2�ÎÐ´s=‘*
á‡ó7ë:¤3éÖ’
ý08‘t1Ë@‚î;Þ\ö†Cˆ£äFyý2Ù	e©F1¥×¦W
kIËYÈg15èlÜë_ž|?^ž}IN·ñ;�t‚„)jÔïjÔ’ò±»6ìÒ�Ehƒc^�ÛJÌ!LËm�ÂÛh2ºÇ8§;é†){a±˜£9 ¹tÔ…î|µ¢W¾Õõ5‡…AëÚéæ??
Ïô#w	y™,\â{pü> gª �P6ðbâ^11÷)&i”5ž�Í6äb„9)é‰ÁÝ<€ÃÛ¦Ù¿Éà¢vÚ½§	êæ�…ÒLAûâôSr•†ƒäs—WØ=Z ½ÁÑùÉã®äêüëÙ Ù:é`!”£uaU(ÄŒ�y”>k=—Ï-�GÆóÅ“øÎþ¡9›é#mT‡PT¹“‘0é´J-6’FSÃô­àÍ@Ÿ§&b=B ãÏÞö	¨K m:®þj�¼‚ß�XZÏÍ„Ê^è¾½Ý�r ò¼Ø·õxlÒ=ŒÏ´ÂÖ_¶o¤í„xxM ²ÿñÍ°<lux-build-amd64“©“ulux-build-arm64³©Èlux-build-amd64“í³¤Í+Û�¤x$ Ûÿ40000 workflows ¬áwr…öÿ6ã'ÂÐ`/2„q <0ä€Ò xœ{Êò”EÉÄ ôÒ3K2J“"z_v]\vâUÚüßu››ôâ7*Ù2Š0øqÕ²Iì*qZÊ±&íðêoö“‹? ‰¥A¤x$ Ûÿ40000 workflows ÙÈ[ÎLdçC2BÅ_¶½üœX÷‡N¥x% Úÿ100644 build.yml táÍ‘¤Ó° ‰Ñï‹˜.W[¼álÓc‚à€5xœ{Êò”eC<£û©[#[Kµ®Íjõ�HHŸþmÉS½ÉÅŒŸ õx˜ì�ž&xœU�±JAEK³³	(6
aM‚K"‚i-l— 0»;›Ù¼æM„T¦¤qÿÀZ°²ÿÃÆ�°v6+QÛË»çÞûî�?Ÿ_×:çÜ Ësˆs™Œ’!H ù@�áš§ Ì�ƒÊ™É¤ÛÓJ`ç›Y½Áí~1#×ÚzµKûèí4PþC	´ï^ÓŠÚ›mùÙÝºçu»ió%·�ÇTI�>g 2§”)‡TrÂ¦£Y2*õ%6,1`eÖÜL´dXÔ‘ºW˜OWSJ£OÎÄŠcRÓŽ]ø{6©nŸtê¹ÌŸ(·;žþö�FB)�ƒb±f˜†€lÌÛöÉŸÛxca©¾eªÑÑŸÇ­€ÚàÒr>Ê'ƒ°lq=UŽ©UrÓ¿xÙm|U¾v½‡·àƒxœ{Êò”eC<£ˆîÓ½­&{<zWìó­ŸÄÞöÆýQ¥ääbÆO ñ†ï�¡xœ›dòCeÃNv›ÀÒÔ¢J…¤œüäìäŒÄÌ¼b…”Ê¼ÄÜÌäÄœœJ…GSòRËR‹2‹R’óSRÀªr2‹KŠ'[r˜NžÇ«¦¤¯¯4¥ '±$-¿(WÁ1À“‹“bžŽBjQ‘ÂäR^[ÆÚÉx-`ÂÅÙ™™yé“™øB&OçÌËGqGfÞäCœê“3ùLÖà7š¼‹¿lr·†5Ð` un™EÅ%
)™ÅÉù@×¥¦@�´]¡$#³X!©(1/E!)59?7è¥Ô´ÄÒœ’É�Ü7k7ß¶` &ZÙ¥&xœ31 ½ôÌ’ŒÒ$†ˆÞ—]†…�x•6ÿ·AÝæ&½8CC3°’Ìô¼ü¢T†‰o£º“Ã>?»›e~�y£ðU£]; ª\ò“³S‹Ò2sR~Ä©…­Ôý¶¿•ù¼pò$†çßîAU¹:ºøºêå¦0,×7?8Ù^ÐÒ Ð9¦ðë‚¿2Q¢ Š’33óŠõR+srRõ*ssÂÛ8û'šÝ¶Ò_;3ÔøÂ³Ü–·Â”ççäÕ•õ•ü;·pþóénófKÏž³¾²˜ª,=_/7?…AÙàÃÖ—Ü�¤M?¯«¹Ïmítˆ¡¢¸4—¡éàåÌæÌW}rîì>mjŸ¦wšª"è0½ô|†wJÑï.<ª(¾åàÙ“æåú¹Wõ/Â”¤ �Tb  /±í‰uô‹�yÚ7¥NÊ¥WW•˜›š*$–$gè¦æ&¥¦èg0˜©�]-­ÙÏ^j_îõGký×°5PÃŠRsJ2sSA
\Xóga‡ÜÒmUÍ‚__V+ü@S_’Z\RÛñø¡QóÝÅ«/}p®ïyãþØ÷´\mzfqIQ%HYVä¯ÿŽÕ.ÞµÄñÒËªÈ–•ÁhÊàF>>qpñ'íUµQi{Y2÷{6K–ÌÔ7'¡âä¢Ì‚’b†{NVK¾üÛåWg[®ÒoÛÒ§íº	 y^õÔéy�§xœ�UM‹ÓPµ•™ÑÍ(r'Ìh‹iƒ"."E¤­ŒàÕRfñš¼´¡i’y/ét>B\»paÜøÜ	®ôg¸ð¸õOxßK¿§º¥§÷œûÞÉ½§ŸŸ|}øýçµ[oBÊŽ¡éxFÇhÛå‰~}óRÓH>­<ÝdEF[6(«Ä5	3Ë²&×$œªÐd&™ÕÝÍ%êòI}õO’[{œüZëÅ²×Þ±OA/�B{]%›ÉØð€Ùn‹ËžH…^÷UE…`EN±(3¦"Ó$-ÁŒ€:œÂ¾CËc•õ_~S’ÜÍû÷¸¶TÑh—t© .~Jàm­¬'ÿnèãÓK¸î…ÌÀJ¥kº\P”Ïf¢l”Íjt\ïÈMí€.ñ9˜„v=75…CàAÐ¦6Ãƒ§Î�N^ü*1ÚCN› Ý¶b‹2êi%‡¿¾€ë@+zÊZMk³=Â¦º—DÿýÔ ƒý‰¢M–ÝBG'ì+ºtø´ŽàuŒÔR#”×aÊ¤Å÷×Ów§B…Z$tÒH�Pñç¨Ô
å8MûsˆIŒ'˜b M<Ê‰çÍÜ‘BzÂ�Ê{Ïƒê»�åÏ/LÚŸTˆ=¨TÓ£38öah›3Ç³—o¼ˆ©ñDUÚ®`µé„Sƒ—ÛÕgl÷dÖ)‰M{µ- 9nÅçO-e}ÂøŒª€¦Ekˆ,õðI7è\¶<Oç™˜¸?rÀ‡›€d¸,GmêNÊ¤³ˆí$…«w×ç'VR[�;^G¤ÒÄêìËe=H>fà‘ë�ï¹ÅUàÛ÷q³°ƒã4‰ÑQ†ù;ó æÒ£˜f…ˆˆáwˆ"uD`Æ€O‚6þÂ�ì w_1GGÑ¶Mx=´,»ŸC=V H‹ópv)¥ÒhuäÇØZ(—@$2<E£ý@kZYc¾¡,ÓB,A^—	¼@«²P+žóz±ØÞ”ØÄµüñµ’k3ÄX2ûK0‚ifñ¢Â‘qQúÏESÃŠ/L3gäŸI`£®íÀiòíÊí1!8*:lõp6†^`Y>U‰~$ëñåÿš¿Tqà�xœ  ßÿåå°_B+äžŠ&©Ûp<çüä“�§ÿ“sòöÓí…fxœ›©}VyC%÷ä­|2Îúú
É‰™yÅ
iEù¹
‰
yù)©“_ñ¥ð¥¦eV(XÙ*()qMfä¯‡ŠØ*$%æ¥(h+(é*qqÖrqq— ‹RŠAÊ£c�A&:çç¥e¦WsqrVç”¦[)@µµ%ƒ­TÒQðKÌMµB§à¬ë•	
pJ$§‚ÄõS+Jô“&×pÔë(„T õ(¥–å•ç—%ƒ¸¹)yÅ@¾KjZbiN	ÌL[ ãsJ+”ju°9£ —3p:£èŽdý %¸3àf º»…¸,ŒÀçïdý¸}–J)‰éXm«åâLË/Rˆ×QHEÐèôTD¼ToÞ/x‹ ¯dš^à’Nxœ  ßÿåå°_ìÿ³õR¸Ë;õ‘’ÈÌ,ÕV]_“sò›oì�Lxœ]�ÍJÃ@…‰‰m3þ@u!XÁÛHµ…šºpQ‚.Ü(­J—
3Óšš&a2ÑJ)‚ø ê|—în|ŸÂµÏ •
î—s¿sÎÛÒÇÂmŒÊØbþØ‹ûÔ<Nãç®ƒëc÷&u#¬MèQ?2›ì¢lüùŒ*¬zg\êµ*Áu¯Ô²Z!ŸÕõ‚Û†b7p}Fa@t]çLÄÜ—jHðS…å”¶€Rd•¨d„Œqs‹Rþ+÷.*›š‘­}˜bo´2*Úl½m»žÄŠ "asaAé\çÒ8Ê�²6ã�î0["Ëx8~¤¥¹ø2¾‡µL=‰3sÊ´P&kü”1ð){µlÛžwb;g~™ùÔæœSÛõ#|ÈîæºÉknGI,(3#Ç÷°”‰ffß'
D~s×ïT`€�É•¼á|Cdå¦Ýcø8Ù(
³aU´¼¸#¯áWqª‘7úÿ®ÉÝÔ†ö¥©Ž<á•uxœ{ÊÒÏ4As¢ÿÜÉeŒê“�™” ;ëÜéx) Öÿ�µ�).gitignore ‘íZ‹cVóæÝiZ7Á±Õ2º¸‘"í^Ö„§	x— hÿ40000 .github é*MôÙ
š(‘ªQ7È×«a100644 Dockerfile ø^&V¬-ö¿…Ïc’ ç÷Þ100644 README.md ÎÔKVÑ¤æKŒÜÑ9¦
ã*å100755 patch-embed.sh 6&Í«(Y‚_u?wJü*¯õV¬xâ9¤x$ Ûÿ40000 workflows b˜2oQ`Ÿ�fwX‰Q¬d(j×‹VäzØ¥x% Úÿ100644 build.yml /x%üÈãïÉ=ÌA,�©:óñí·ôâŸUxœû(°‚sÂÄ‰´µ¹²ò“Š­¸Rò“³S‹@,…¢Ò¼bÝü<+…Ò¤Ò¼’RÝœÄ’Ôâ’ÉoëÅŠSKJt“J3sR*t“K2óóÊŒ'²‰MÎaú/äêî©P[«ŸSZ‘–©ŸZQ�“_”Z4ù6[ýä*6ÞÉÿ™wZ%'&g¤ê¦åçZ)”T¤Ú¦g$‚m† ˆtI>BR'7?%Õ67±bòZ–ÜÉ¦¬‚ ~^Dcä)€–2xœ]RMkA&äÔõP¡XŠ¾˜ IÈÒ öšÖµÔnj>ÄBšÙÝ·Ùm73ÛÙYI’ÐCÁ[ur�Á«g½	žÜ_ xôâì&Æ¥Ã<Ãð~<ïÇŸ�ÏË“||~ñö¬•Ò×§	ÉÛ¹JÊ{¹I¾QùeéV>žLå¯¥�wžD¡ á"xÔÁr(Ù.ñ(8Dh¶w€Qÿ´,^½)¿iGÕ…ù€“À¤�"”ÂÈš}l'wÇL½ä‹+¯ÇMn»ž@[D5­ßïk8
|Æ“Luð†d€Uh€åQÂ=ËZ<}O'ê,Ô^ÕkÿÜjH°õ_e×è=K¤f\gbV –¾O|X@~‚µqÆx9ÉS�Ë0†::°‡#qï(„CÎ¨@ê¨ g™ ½Ž©Ê„ ¹>k`È™n3zèª°ß4a¸ÊÌx�àÑ&ì>—?r×Æ�‰ê±KB|ÆŽ£ T†Ó¹aß³‰ÊŸ¿ƒ^ã²ÚuÚ5›ŸB©.µL}×hél”Óîk…ì'ÑMb¡ŸŽÄR¹4‡ÙÇªÙ<’íëwåòŠ!Wo¬É…MP«ÛfeÍ‡R…úýõŠPþ.4Ò}*_¦ü¦ØNù{þê|ëA­xm ’ÿ40000 .github é*MôÙ
š(‘ªQ7È×«a100644 Dockerfile „éÙÌÊ^[Žšƒv¸�N;žúª100644 README.md ;¡1ÄàN{8B·S^g¤ gS³z(ïå%€¥JxœUQÑjA%&ié*‚’¶n’j2)´¡]¡MÛÄlØe³3i¦�ÌlggÃö½úâKË¼
~ƒøþ@ÿ¡úìˆèlcRú23gî™sî=sžûòèbå%PŽIL$ô=ÿ„pk@â€	I` WÉå°tä«háp(	™àð¨$I Bû²™Ï³(P4UXm‰™¶àì¬¤¦–*¦ðóãLÈSCCî�XUŽÃ[ïé“|ý×(¯ª!mðÅ¨O9Á¥›Þv#Ê°€…q4C%ÊêòÇ“ž„Š!¸7åAFÊlU·«v²@™€Ûm¾ßïìu�vç°1T*°â{»¶Y5(Ô÷QÉÀ7²)©?¥Î¾µï}Héßi\+Â�ÇX2°t�d)¤‚�G™ÉÊ—ÄSF”›ô„ÊSÔL¥~—ÙÒŸ3�1Å�‚Bž×‘Áú:³;7	P—²kÓã›lA§Ó9ý5»üàµã¾Øk»€Béë?ÙñS³#„gi¡‰•µã¶àÕ¾Ûk;�&cýw®fµ§×`”G±Þ˜¯çßZI<e·QWîhéÅ…Óùÿª¯
ºkëïV¤ÝÏýûkÆ3ä$‚PxD»ýŸò
�:‘#
#
# Uses the pre-built indexer image as base, adds the frontend static export.
# No Go compilation needed â€” just layers the frontend on top.
#‘j!*Go backend (built by luxfi/indexer CI)
#  “Q7   â†� Next.js frontend (built by luxfi/explore CI)
#  “Q“"This: combined

ARG INDEXER_IMAGE=‘o:main
ARG NODE_VERSION=22“Elstatic export³°ƒ“j7\
   “ G“ü6@Combine indexer + frontend ----
FROM ${INDEXER_IMAGE}

USER root“*"v/frontend
USER explorer

# The explorer binary serves /srv/frontend at /* and API at /v1/explorer/*
ENV FRONTEND_DIR=/srv/frontend
_ä½Ê¶hx†yù# Explorer

Combined block explorer image: Go backend ([`luxfi/indexer`](https://github.com/luxfi/indexer)) + Next.js frontend ([`luxfi/explore`](https://github.com/luxfi/explore)) in a single container.

```
ghcr.io/luxfi/indexer   â†� Go backend (API + chain indexing)
ghcr.io/luxfi/explore   â†� Next.js frontend
ghcr.io/luxfi/explorer  â†� This: combined single image
```

## Run

```bash
docker run -p 8090:8090 \
  -e RPC_ENDPOINT=http://node:9650/ext/bc/C/rpc \
  -e CHAIN_NAME="My Chain" \
  -e COIN_SYMBOL=ETH \
  -v explorer-data:/data \
  ghcr.io/luxfi/explorer
```

Single port, single image. Backend API at `/v1/explorer/*`, frontend at `/`.

## Build

```bash
docker build -t ghcr.io/luxfi/explorer .
```

Pulls `luxfi/indexer` and `luxfi/explore` from GitHub at build time. Override branches:

```bash
docker build \
  --build-arg INDEXER_REF=v1.2.0 \
  --build-arg EXPLORE_REF=v2.7.2 \
  -t ghcr.io/luxfi/explorer .
```

## White-Label

```bash
docker run -p 8090:8090 \
  -e RPC_ENDPOINT=http://node:8545 \
  -e CHAIN_NAME="Awesome Chain" \
  -e COIN_SYMBOL=AWE \
  -e CHAIN_ID=12345 \
  ghcr.io/luxfi/explorer
```

## Multi-Chain

Mount a `chains.yaml`:

```bash
docker run -p 8090:8090 \
  -v ./chains.yaml:/etc/explorer/chains.yaml \
  -v explorer-data:/data \
  ghcr.io/luxfi/explorer --config=/etc/explorer/chains.yaml
```

## Related

- [`luxfi/indexer`](https://github.com/luxfi/indexer) â€” Go backend, 1528 tests, SQLite, E2E PQ encrypted backups
- [`luxfi/explore`](https://github.com/luxfi/explore) â€” Next.js frontend, Geist font, white-label
- [`luxfi/explorer-v1`](https://github.com/luxfi/explorer-v1) â€” Legacy Elixir stack (archived)
(C­xm ’ÿ40000 .github é*MôÙ
š(‘ªQ7È×«a100644 Dockerfile *n†Ÿ¸Št)g˜Ú*zïa]wL100644 README.md ;¡1ÄàN{8B·S^g¤ gS@&«å…Xñ‹ið�jj+"b‘úÿ