# v2 git bundle
43084ab84df098eaab86aa5f1bf5ced0942cb806 refs/heads/bundle
43084ab84df098eaab86aa5f1bf5ced0942cb806 HEAD

PACK      õ—xœ�ŽKNÅ0 ÷=…%6°xÈNÓç!Ä‚{ 'v âõ£4ïøô¬FšÅhz35S§ìã%?Rr’�óš”ÃD>L#{?lÒlé€,yòÝdZ´Ä„W_8 +Î‘ç¤Äc
”9ú÷ÚàCÚÏÉÝxïÒvxýMu‰xuøþ5K½=çu~bŽ×		.ÈˆÃiçÚ»ý;0¨mû¤cÞ .j÷3UÎŸºæuéMr‡¼ÞŽy¹4…Rïðø@ô4ü¤ïU Ÿxœ��_NÃ0Æßs
K<"FÒtÍŒb‚LâùãtiR%)+·'ƒ�€ÛŸýég»f"@¡]�{®$JßuZö¨PŠ�°Î.†Þc%[t¦XÁÃ¾§fI:e¥�†F¡:8aœ—’\óëµžS†w�?[.¤JÕ¹ÀóÅLùÐñ×qÖSØÙ4¿€P
‡N!JxàŠsÖºóT+ýÀ-å	Ì:/0EG[Cùv�v.S)`Ó¯ø)ÚLóõ³{©¾%GÐ)|c§5„aÝüôxƒÜ	)BM$w¸ƒã�¸è‘ÊïìL@ÛRnü�Ó‘Åt�rn!“P·¿ått­Ì™lßàƒ›Œ5k[ËŽý ø×…bŸ2x/Ðütree 68a63bc88e9feb5fbd10abd209fe87bf4f419ded
parent c2ab1ea2d487ac345c4e1d73397892b8e5bc2746
author Darkhorse7stars <wbin90620@gmail.com> 1779620243 -0700
committer Darkhorse7stars <wbin90620@gmail.com> 1779620243 -0700

deps: bump indexer to 0.20260524105612-65220f1104cd

Pulls luxfi/indexer#9 â€” formatAddress + addrCounters now read
both the legacy `transactions_count` / `contract_code` and the
SQLite-backed `tx_count` / `code` column names, and coerce nil
counters to int64(0) so the Blockscout-derived SPA can call
.toLocaleString() without crashing.

Repros against rpc.dev.redacted.com:
  GET /v1/indexer/evm/addresses/0x58a86aafb6cdd0989c799353c891e420fb530e0a
returned transactions_count: null + is_contract: false for a real
deployed AMM pool â†’ now returns the actual tx count + is_contract: true.
ù½I–/xö	ýtree b32d9907be943012c2b933d9f5964669730e2a6f
parent 1645c44a7976588af7767463ec7abe978c40a372
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.­ßú–]xÖ)útree 2d465242887e048e5300993fd4cb9bdebd7d736f
parent f72759aa0c8cd5b9190f71ee77f84ebf378820d5
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.]Wë.œGx|ƒûtree abfe5dc2b9cd0a7a48fa5dd67a21a5394276ab87
parent 3429fab105dded2a6751647272d7034d5658064f
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.ž"ƒä‘fxažùtree de3bf981ef4194c6bd1abf733996872270b27eef
parent b3af1c074fabe026a0381ab883a3f029962944b5
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.$1ŸGx€ûtree fdc48f3ac2b455d3f4ae846fa534011dbaa212b7
parent 781712344bf1a656e665fb2577ee89ed0858a01a
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`).{„Œ‘>xáütree da4ba9c87520199170911fa37074680541f61043
parent 01f121746be1bce47c7e45b533c61afbb81c03b7
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.ÖIUj˜ïxœ¥WÝrÛÆ¾ÇSœQ.HÆ$DQ¤(Ê®¥VãÌÄ‰GR“^d¦\ bC`�î.L±Ifr•Èäú`~’~g—?�ìv2SßÈvÏÏw¾ó�g¤¤É$ÉÒ¹LÆ‹Éù,IòEr™Ï—Ùl’.²‹ì,Ÿ‹<�ÍÏ£F©Mót"&‹³éÙåùÅ|q~y!¦—y’$—Ó4MçY>�§gçY$ZWÔ†^	³Æ_+çÖ	céÅb2¾˜ÍÎgÏž¼ù¬µÒØX×F6å6^)W´IœÖÕK:›ÏÓ³Ùx2¥Ñx>GxZ)ç¤¡/”{Ý&ôbwí³ÿy-Ê¥p}#EéT%W$²ŒiFi!´–%ÝÝÝ�ÔYS+í,åß’’Vg¥Ìèîí5õ?™¢èSÊÕC?7µv8CÓñ”jM¯o®_ùk8:ÊEY&"]S#\a£èþ‰©º‘ÚÒÍ; zW·&•Ô‡ÿí‚	þKõ·ÊfNÉ=ÐiäêµÔdÒ¨,	�Œå¥ZˆYŠ´ @ð±\EQ.]Zô[SéÇJ¢(ÙõømïçAŒütßÐŸ^’‰ë5ýÙGÕ‰©? +Ê”I‰ÿžGÑw¸áaá´(«¥Õ=G•€Äà’°V:c`qÚÔíª W“Ò™|ˆW•Ãˆ�xÄŒt­“ñ˜Þÿò;ÙÚ;`�S§Rf–ïrhþE'¾˜îá"UU2SÂÉrK"©�Hd*@©PÁ:Û’²´tòÁ�²÷å�tí¢ð@²½Q€/8pËÔp’Êze=’¿=‹¨mSkx(ê@o¾|sCnÛ Œ½ƒ›Þ –„cßìŽ¨÷�Ã^L×±Ò+ï5­QýÔ©ZÇQôþ—3pÞl÷Ä ¾çƒL´€åÈ=à—'ÆþïÈ¡mŽŽF+aùgº–fâ‡CïD©2áÐ~ƒ˜¾®•Ý>?Øß•;Û�  9ÜøjyhÖ­/˜g½mdªr•¢Ð[ÏØ]EÿÙJ‹*Ôz_ÊÇýÓ-˜É*‘YÆ®˜2Ñ©²$€ÖçJË}S=Ç`ŸMD{‹ÑŠïòKD4¤M¡ÀÅrÿ¾Q-Ö±S(-¥`“¹På¾âÑH9+Ëœ¬*Q/ˆ}qsúö›»ûSôT¼OÎŠ*tÃ.þ(Äß4Hœ*_x•IBKøš3áuMï”Uìòo÷uXÉØËÌÿ+X�eç[åäÕÚŒîXäý; ±J[Y
ÇJ²ôÍÎ±ŸkJe�Ofyø¹FËSOCáéŒx÷„�þY—ÇÇCÁ@‡¼þÕ‘¸þ§§ó’)ÌdÉ9}t¤|@0È·Öž¶Ì 4dayÜJBô—ž"/8Å—Ë G%;Êu¤ûTé�}ÃƒX±˜zhe Sß�ªÊJf^Wþ¸²œ”åä©²�| -'D[xbìñ £@�XI„(²˜ÞÞÒ'ÓÐ©O.“-·Úv�*ÞÁòã#�µwao…©Aykp×\E•X™>2ùöæú«î ä[»ß+S”R	NÚˆŠ,,7ÉU0Åà´owÍƒ-9_g¤üSZî{êÐ�xU/sñãV®pÓl—Ô·;fÊÑêw2¹µ%ãz½Ð@Ä×‚;íØßA9X‚ )xºÝ�7Føæe0` p®‰Ã=ó×VCOn|)‚]/i‚.¦£LÊ-ŒÄwÖŸã¾-ëCbÛ
�D™©ª¤µ¨($[¦ˆöô¡:Ö¶& Í+£Ðçó=ŠË�$m� D2ÅôOjLûCþ»(-Âè æ¼†„L¼~edÑÈ]ýÊëÔî
õµÜø³ƒ×ß„ˆñ`Ëtc>ƒguæ»—ûfcDÓà—ÒÞJnDÅÎxàú†[Î‚8žâ;Âà˜Ù•Ñ§YAÅE¾‹Ëû_;°5lpc1\“Æ{÷ÆªöÁïSð%šR¥Êº?ãaÐáèGfÌ³n_Ã€_`<�¬FG8 zÞñ0ÅåŸ "¦êãy¸ä7¥g‡dÿâ÷P7º‡Fð^ã÷î6vB[~ldÓì€ nZHß³h®07[wT„á± à�“Ýj`õ/É›)y)Ú	Ê¬Óœ _ÏvÄˆ»	üã;[Þ–~ñÕà„g…Ñàma§ƒ²x	TeÞã³„y ¸fmÃá|ã©X£Y;ô~/>[Z£¥FÂïÖü)ÁQnŸlyEL†ïõ÷S€sß$­è¶	6cð-‡_�ô–$>I`@×!o^Y3xîã¶Ø¬õ©>ŠÂ‘´ž¸§4¨Êâ·)ú ŒoJÀn†ãþƒüp`Ÿ4<Á@ÓÚÃ B¤ÃðíwÇÉ-&ô¦6k{®}+ö6ÜÇ°æåÍËâ
ÜiU™Q¼ÂE¦þùôÍç”(-Ìv°;æ@GŠOã8¦‘i5õöZòÄä'°²,Ôk\¸ï*òQDl›ØÔ¨DâÛæ€å¬kÈî $\&5Y³�ðØtŒjÚJ×o¿$T‹Ñá²a[�ÒD£ý¿è?‰ƒ$ì‘SxœeTQŽÛ6ýç)è‡mÀ–¥µ¼¶vÓ4ÔHŠ"ioÑ¢_¡Ä¡E˜"U’ÚµRÈ!r†,'é�»N¶é—%qfÞã{o"¬xÎsYðõf‹¥¨›µ¬/Ëí%6›º’e-Š‚WÕVV¬çM€BlWåE~!¶õ¶¸\•ÕZ`µjÄfU5…\o*¹ªŠ²b|­uðoZøµVæ Ï>¼ø€xäc¦ìs(6›ª,Vë¢„E¾ÎsÖØ®S! ƒ—*¼jxf¬Ã^�/*´C�QÁ7mjcR�¦ÒYÐˆÙ”y	ÖÀ«ÝÍ� ‰ÂþíÍBr­kÞ¡ç¡õ0ý®œ1vÛ"ÔƒE,Û£ñ°»£{îíà„é~¿›AÓrcPû4M«;êÒ–f-!œ`É‚=¢òÎ7z‡©Õ¡0
pOü¡+Æ $†¦�NÏá¯I&q“x:ù{–…ÍÔÁ÷ÏÁeö?$VO8MgpBy^kzž]3öu µ¥»�°èÍ$@Ç	8+Ü{‰ZÂSµ³Ã¡…`A�§¬�ž³8$Éæ0Ž´¸Èsøüñx› ¢H½³¢ð±7RKOøepÛòÀT×¡P< �×Ö‘56|ð˜j+FPÞ<…eD?c{ø€qÞâAR:ˆÄS—³÷žâ¡íÁ'%ŸàN<±ö½5„Ðrò^ÿôzaìÉÆÉ˜ÉŒ&ñ±#Àä€“n"ã˜ØˆÚXr¿	ÊšŒ±Ïÿ‰) z7žƒÓ”?'1)½†ëE8Ñ[
Æùw7^¢ósvà>¾6Gts
TtÇµ<Xçg¼±Ê�×_æ?Ú-Hˆñž“ ’`’[IZ¢õ.–¢ï{l”T=¦Ä>:úç€ž\°ælå—"ƒw”†]�BD¨Oì”Ö@¢Sê¥2xÞ¬kz ùqÔ×Áy";ÄÞxHŒæpß*Ê¢>#lr+Ñúº)Ðhäq¤äJŸgß( *xÔ¼Òä—3x¹»]¾ýe»¤�ÊÎ—ó¼ÃÇMOüÙÿ§DþT%ã•@ •HžÇÀwÊ«ùëïÉ‡fÿ‘Á�xÐ/þtree 4a3520bf2f2184bb8c1c19162cfdba7471d8af38
parent 9f018edb60cfc9aae09ffaf5ef87483d7b2837fa
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).
i•™ë™x	öþtree 2f2a5c66dc9a24f0e4d8b934ddcf08160dfbdf2f
parent c5227405f31443affb57c898a31069635e9a659c
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)
;©Q`›xäþtree fe946c3655d13d622306adf8142fa975c2226355
parent 1d853ed3289a0d44dc82c28eb1a9710af612e18b
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 34b016037668f9eaff4cc89ea5be3886c6c3d868
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
KUx›–xv‰þtree 1b601c107e5b2b5127157e64b2b0a7da33b69766
parent b043993d03df852a3fcaab07308bcdd43877b6b1
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 strategy„(qz“xã ÿtree 840c342b50cfd524a73f5fcd9413e28594e3e367
parent a858082e3bfaf4e0ddb3bc690719f420130614fe
author Zach Kelling <z@zeekay.io> 1778698780 -0700
committer Zach Kelling <z@zeekay.io> 1778698780 -0700

deps: luxfi/crypto v1.19.0
›HBÚ�x ÿþtree 0397891d3eeabb15b99bfad03a75e845514d49b6
parent f5cfd03bd4478d758cc45733b691d77016f46643
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)
Ð>Lñ‘xáþtree a84952a30b356e5c7a51e88fcc1b6daf11828d32
parent 4acc65c6d4f84d0c5c2faad94fdd7deaec5ac542
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 Liquidity local stack and devnet.
Šêž‰œxóþtree b1c7ed692ab2271e3f30a430260c0dacb79a9888
parent 3c6e210360ac381d850852d8e658d4e027363878
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
ÜP^’xýþtree 5fb3ec9b9f4ec2dcf69999cd72a5a5b490e7e538
parent 494e3fe64555b1487ba2c8547140abc3d94e6954
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
 4NÆ™x	öþtree 7b59cbbaa57ea015fe68d85dac4d25345445750b
parent d41699bb3361f78f0a9ee3ade771360809d5ce9e
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
YR�“xüþtree a2df3925c59af70f8680d33b1bc8888e18b55537
parent d17d22721eeed9cceaa3265313eb8b54dbbdd046
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
*œNÆ�xð ÿtree bad4bf082412b6cb2f5223dd0be204330b9850ee
parent 7f0765d1127a67fb76e6922a8c672fcf75cb4087
author Zach Kelling <z@zeekay.io> 1778042082 -0700
committer Zach Kelling <z@zeekay.io> 1778042082 -0700

chore: gitignore .claude/ runtime state
OHž˜x÷þtree 860487c5a72962cf6ab735df02dc6c8e3e119326
parent c979f90b2a92166c1875cf80308c9ea7b3c1986c
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 33658ed9cfea935b4f1b0921d86f6556ae030f91
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
`ÜQ ™x	öþtree ab972f511d87cebfd24c8fec81cf93ffc1e27d23
parent 89c9cd2762bb86f349e695ff8291b924aa83a45b
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
Ê`QÂ“#x3Ìýtree 0fc515d6feb88821662d1b110955301bde2fcc95
parent 9b8044b665e0e12d0ce4808d7b5a3194b62a0939
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.
úN»¥”!xëýtree aa632e6aa6b40cbaced617b1d2f1945e97b3087a
parent 9c1c9c7683028ed3cc2f0d7f364be29f98adc080
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 77c543529838e6db25f92f47f88291585cb379b8
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.
#¥Ö›xôþtree e9ef4f120b79eaea5dcdbc67fc3b7a64d84a2f64
parent 0c29cc0a65548b9d3700f3d43928b4cff4c6c153
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
¨íSÒ–xf™þtree a1f2c5fd9e7fc38a9e762f7514b9a9a8e6b4a78d
parent 630ffa251a6398d79e96d2b39ec5855cbd3df0d6
parent 755b76ac22240f7aa06d5d0b0b940f3eb1cade0c
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 runners4_oØ˜xø ÿtree a1f2c5fd9e7fc38a9e762f7514b9a9a8e6b4a78d
parent dd387f3433d8725948027b5520c0d610b339a48e
author Zach Kelling <z@zeekay.io> 1778036307 -0700
committer Zach Kelling <z@zeekay.io> 1778036307 -0700

ci: register hanzo runner labels for actionlint
Á‰KíŸxï ÿtree 02aed7906e41349de231ca755ff5d96fc6f8daf9
parent 8e4e88c1d09a11263d6b873ae8274b2598cc07b5
author Zach Kelling <z@zeekay.io> 1778035887 -0700
committer Zach Kelling <z@zeekay.io> 1778035887 -0700

ci: use hanzo-* runner labels (sanity)
>jGñœx,Óþtree d5a98873ed8293956340c809fffad804ae7a3d36
parent 0531a94d58c5b3881b12a8ef1bcb9a86a00ff550
parent 630ffa251a6398d79e96d2b39ec5855cbd3df0d6
author Zach Kelling <z@zeekay.io> 1778034163 -0700
committer Zach Kelling <z@zeekay.io> 1778034163 -0700

merge(main): keep multi-arch + semver-only triggers
¨|Z¬�xÀ?þtree d5a98873ed8293956340c809fffad804ae7a3d36
parent b35ef6b747fe94d1dd2b2d9c5ac4bef9f7ec1e92
author Zach Kelling <z@zeekay.io> 1778034119 -0700
committer Zach Kelling <z@zeekay.io> 1778034119 -0700

explorer: remove Liquidity branding cross-contamination

Drop the _lqd._tcp / lqd / liquid daemon entry from the mDNS service
discovery list. The lqd daemon is the Liquidity-side white-label node
binary; the OSS Lux explorer must not advertise it.
"q”Ø–x¦Yþtree 0b695143561ce964ded453a18b176ef8db3bddc2
parent 579d74c09a8664b3e10c1ade5217a0697471e10a
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.
‡™‡˜xøþtree 7fd06b2687f41531672d96953f410837b9ed557f
parent 5e1d5c3f0337d04b4711bc2e5ca5af160f549757
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.ÑP¦û“xóþtree 7fd06b2687f41531672d96953f410837b9ed557f
parent 5e1d5c3f0337d04b4711bc2e5ca5af160f549757
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.
÷·¥mžxþ ÿtree 05ec30403163c08342bb6aadcf78f72d50d2ce55
parent e8c2d51752df366e487b76a5f33d9b5eb357747c
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 90248d6c7bb2cae223a57b231d8d9bffb614e78f
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
ÁON<’xíþtree 67410a2f862abb06a0545f03e1f65d5d0e6869e3
parent 63a8610d3ebacf64a2634f38cccdf744d28990a6
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Âšxåþtree 0ff63ccea78bbb19a3597efae560484f7e17f0da
parent 82926899b7f341b085a4b12cfe0794dfe7a49a6e
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
xƒY¶šx
õþtree f80df6b97507f299c3c70d1c113711d00044b827
parent 672dc81e5757d00125d361f4dd4c58f5dd324fe3
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
£}R~™x	öþtree 5c2106f955decb2bce0e4bed364ab821991d67a8
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
½Q*Ÿ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¥&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N ±ÿÇÇ�Z
,µÚœí·åüXÒ
±¹LM(±n ð_¾¯Q\æºaÿw¦R»
nuC•p³¢‘�O[>LbgXá)�	i}b qu Àë\x+ ÔÿÇÇ"40000 .github ó;:ø5¾ŒÌ„¡Û,
�ö4‰8µ±"%Zù¡ã9x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£�‘®ÅŸ³ÄƒÎð´¯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: Åÿself-hosted-runner:
  labels:
    - lux-build-linux-amd64
D8ô¦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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œ¥VmoÛ6þî_qs‹¾¬•;K–¦Q0ÇV¯¶åÉvÖ +<Z¢mÂ’¨’”óÒØ¯ÙÛ/Ù‘ò{´ÀÉãÃãÝóÜñ	R6f4‚V~ô6‹¹ XB&Ô†K'1…#–q÷h2¢Q„æ½ní5°4¢·hþªô&‚dS b6D0*mhPÄ»ƒ¦pÂ)a©´ïHÃ.€¦s˜!_I#È¨�L*Dr"¢È[@œ–7$ƒÑšð<UèÐÎ+<QR%_CÊÑf”³8‚”RtÑ.•jÁ\øÃ+/è5ýŽ[±«‡f®ã7¼Õlµjæ¼÷Ý–xÃÀëúîT©L;Î=ÏGvÈ'ÎoÇÌYDÈÆ…�]çn‚n™Éf§á½÷‚o@-‚·‚ZïÚ€ºjÝËo ™È¯`–; OÐS˜Q¨C"5¥:…f­ô�ÎOÅ€›èÂ,å#	DÐÂÞR,¡:Æ
„ÎBB�ÄYÌÃ™y®¬13ÄÑ ‚’HBÇ{ßvg­f}ø#fi–xš”�ÈŒJ�“ “×_ˆ@¦2ì×ž&Ð˜ä±‚bZìSÎ"ˆè<¥
ä”dô-Dü&•
ÏL–�ˆÂçTQ˜3‚·,nAÄÝêÿîï†Í†ûÏ_ÃªBû% ÷ ¨Tˆëè¸á»tøm¼sD�Ÿ~Þ¤ÌƒEâŒ¥j=/âö‡véQo}øÑáÏû•êÖt§ÖöÜòâ–ÞU»¼µZ�×©_{×í3¿å¶~kô¯KÁ $›‰"¼gÊ­�„*M½«ÑÀ!YfìpÂ˜£ç–ÑLMÝŠŽ
-œºO?oxû ›Ã®ÿ vÉë\m%µÖmÏj=oØ­õ/]g^YªC|eºqoåjðð¨¡‰ÄÚT7^fg×NÀ¾`ÙÿÑðêÍv­Õs+G…±&Žßí#oz®e%äÖâHE™‘�Z’ÝS÷¨ò¦jÒbìpz†•‘Œ°î>{F2Xí‰cL²íž¦–VÝ˜mX¢þò”È©ÁJf`e&ù
TÛ~ k¼š(
�=UXF?ê½” 4œrx~òCÄCu—QÐë§'Š©˜žz‹üž8Åø$bs`‘[œ«òé‰ƒãÓçpú_gìT¢êñ‚‰«²è ’�b¬òXÑ×•*_ô¨¢EËˆsÂc’NPžë*¿)Nƒ€tÜ-Ê»õv§Ôn×Q3Z6,=�*.!ÉeŒÊšƒü3EÍ§VâZŠR„ß#Åç´7�X…3Zäì`««< íz¨¡ ˆú¶ƒk×ýî5Ø¨}óaø—¸Ë’·¦–í AuŽ3Áoïì"16Ç‚k¢#Ñé�§ºÃÓTÅw¦àNIzÏ	s�õ1‰¢¿Ì÷ìC{Q^Dl<¦m¡ëw%H*æÖ“÷,Ãí�_Ú`%<rñ2žå1âH$†-ó�´Çp3ÅIl†LÆÁPê¶´ ö4Ó”.üÞ Ý8sùxrÆ2ÝLjëF‚Kiá­°÷Ù&›"ÑÊ*Õi©#½Ní¬å50zT?oÕ.znÙj[H(ï¼ÙòöüAP÷Ê[§.rÊžYJ°$#ø„ZÝ³° °âh“‰DX	ÖXïA7/{®ßR<ÅÔ/EQ^ïá`”¹zëÙ;ÊÜ?^öø¡š:Þ·«•Çø+¤B¡RC“‚…ð9(áfBÕ‡ÊÜqÅÉ¥p°¸‘ØA©¯ihNŒ¢“V¬ö««÷jiÐó‚õèÊoÚ^ñŽ45¸Që×†Èn×LÁe¿ßÖ�À=>Ú{³WÂFç÷<0ß—^­Õ¿¬_zõwè+>K�'$v÷÷0Àæ„»z€”ÊÂg+Ã¬Tp]G¸Þn˜Ë‚õÉ·@?ÙðÅfn4åR™ãœ)%1f+.½EáVÐÅ~pÝõ›�>|(/oQþXú�¾²Óº_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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»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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œ•Wmo7þlýŠé¢9H½Õ*w)
ÔE>$vÚ3à¾(i?>™Ú¥´<s—’+E5üßo†/Ú+-Î@²"ç•Ïg8ËØ–CÅD=™ˆªQÚÂtr–l*›àGú_ó-ÿÒ$ü¹UÍÃ6õâÀ*™í^%“Ùd²XÀ…ª7bÂ€-9XÕÌ%ßq	('•ærÇÐjf…ªA*Vð6ZU�—hÜdNáÄ•«ÛÜÂãäì’Yv)4¸?ÜõÂß=É�'r¬
¡“ûÉÙ¿>~¼ySúëÜ¥µÍŠ!±¿Õ¬..ù†µÒ‚[Àˆ}M›«Â³�È{n÷J?Çt{–}‘:p÷…;`Py{ç–áˆ�ÛC€¼OÌZ57ÀÀà9$÷pA#[×s¿Ì[cU%þpèfð±ä¤‡F
£'…Ã¢*ÐÉ&�‚Öp
ÆÌ”¬á?ÁFpYXóƒB–²Ý.Þ³Š/Ž>]].>Ü\,>b�1¦9¨†ì2	ky€}Ék—ÞA4âcNº\ØYH	´*y|ïà]ôÉÄþþ¿FÕç‰ABâö=ŒnMáA§O
ÔHè¸uŒÐÕ%Dmø¢€s~%Šz�¢5

'­ÐQúVÜúðà|W¥P°m
ƒÊt
FIV³ÖÂæJÔ)‚b*e&gˆ3œÔ®›Ü+Úi�Þü¾„Óü{ØÿÞç$[ªµ’#vò¢ï¼[£À»š­%wWc­Pê÷„$
Ä5ÊÄy&¯R”é]­¥juÎOÛ’£cs©O¢x½K�2<õi�Åêª.øLöð]rkQŸ‰
…ßNñÞX^5öƒüœ€~ý¢YS¢q÷=ª:žhKÛ}UÑÍ1!dønXl¢*WmN©(Õ§Òâ¿?sf[�—ú˜‰n{ +¤ä˜KÏ+µãZ‹uT°:”l'0°QzT–Â5+é®ò�’òª¶\ïX—xÞ¥I+h	ø6ûîºÛ”m3H^½¤µ´LÛ·RåT…[‰ƒbC¤Õšh„eÈ¦nó
Ã ÇÖÅû×±üûL»v!6áìCÑîäÝý	W¡×,º+Ã;E÷!êÇÎOi®ãÆ‰3…+ÕÖ¶×P¨Ýyò’WZ¡&±Åî'·xtŸS‘}Z<FSsZd°ô‚Xè¹ÀºO©±n…´s‹xH™ÓŠ�J°>ÀùÚu–«j!Û/ìðz‹Å1»Ævñ– ü”UX8ÛZ˜=kæ»¦^YX¾ê‘¾Ç’ÊóV+8–‚MÉ±4Ø<›�KÖ†Ù_'ø+sÆ>Ë~û9BÔ…-¶’a®ûH¯­P¬<#f�hrÌ<¿ì•ÔXGÉ0NF_3Â;«KÇ)/TÑ|ºW+È	Sª}MhÿòæMW˜Ï‹kµU®ãé�ëáš³ð +„y |¤áNpÿsË�ÅN…¨ŸsÈY„7)­`�qT­%ušSÌ�a²·5Ó‡€®÷ü9´/1Ö>ja§å»ÖÉ»î:�“ç¼¶
ß§#yæ(«œH1¬ãª!8FŸ»!�²"€{©2'ÿ´ˆDÊI²òéÃõ‰Ã:+­–Iï°´÷IË¡£dõ´
gu¤‚öÆ*|Žzš²ZIƒ/>‘Óår)d”b[Jü‡a.©“„t©éò"lôVú¹	”®PÆ�Qd¥Tûk|ô>ÓÀˆ²’H:Æ5òŽz4u†“*\Ï¨XGÞ`ÅÑ@PÁm8–ÅãD!¢Ë„ÏÁªKôRF¨Âô€wŸ…XKdQY‡Õ5[sù<ž´ÛË ¿¦™¢AoÌ^¸ÝÔ°î½‹ã£xô†Þ#ùª+QÔ!n˜Å¦]cA [9žÉ×)¢:)¢ÎuÎÇY¤)YÝVùàïP°Ü @_Tœ×†m8D9¬±Þ`ÒL©;…‡	¶$×™fnquí`0®æcÒí˜xöüX›ýŠ³Ó…ª”˜Þÿç–Íÿx9ÿñ.|çw�/Ó^=}{ïç\êJa@¡‚iúÓkJ±Eã¾}÷þ·Õoo>�»40ý¾L�Wøˆ8©RºÙdÓÖyOçÔõ'�L/âÛVk¥gosÝÎ_ƒ2Ùô�*‰œMÎÄÆ¿yµ�$p¦9^�:ŒÔ�OØ+›½#…›iâjþs/öIê€rÊQÑÓäŒ Ë7Û zÔ�†Ý¨þ©®˜ÆÁQN½SCÞÙOÿ—ùð¯Ú§ç¥ sØ7¶œ\ÉÂ$MŠ»å­¸ËhZr€¼s˜>£¦Dü…[fÏ¤qvúº0Ån ‹8|ÓK¤ìW†wtéb6¥ÁÕ…í/pp9tû¢¸;Ç„q9âò^|FTD
'´¢Î'‡RPŒ,)�ŽWðôì’´Úxœ½Ym“Û¶þ,ý
˜3í‘¶DÝ¤ùÐ‘£tì{‰¯õ]<''ùàz�%úH‚É“Tçfú#úûKúì¤¨“œ—v&žK ‹Å¾<»•2¾“K%r™Ãaš—ÚÔÂ¼h[«ÊÃ•GjÁ_ŠX/Òb9ùXé‚&’¼¦�TO¦,T=YÕuIß5Ï”²^M’4Sô…&ªÚ€¯UÛ"ö†Áp8™ˆª–u_ÎEZ‰z¥Ÿ¹P1ó"o1s®ã;eˆ•(uÙdÒ¹m‘�IŒ²f“¤µ)3m”ˆš4[ˆH%4 ¾ßh7gš¢z.0/2ËL,Ô½�¢Ìd¬V:[(CÌÒb¡6áªÎ3’+ÖyžÖ5„ª´Xê)Ë(d¶–[HÒÄ±R‹*Ä>üõ–³©•rx/Íîž¼^Îùö—Fµ*ÀX™{u¨�‰[§¹‚E’.…?QÅ}~¬‚‘�ØW*3^éª&f‘¡YUª®@˜bKXÝ/Gb’é¥¦¯A(^ö‰Œ’«ÂEZÝ	]u¯Ì–˜õ�FU5ÝXBGÐêVÄ²ÕZ–$f.Öi½ÒMJV,ŒËâGi!Í6ÖÛRõîW›&®Å§á N–‚ÿ�ñ�†£–)–·âéÙ
®xë†XÐºfÊ¤"}Ø(4~÷ž\tøÀ*¼Qëî£êÆ$NÚ©®›)#"Ý`TÓmœ*I1�(Z	ÂaÒqŸ£OÒZAaŠGÂÚŽ„2F›€nHróXLg$û¼‰üÖü#áÙ¯^€%Lõd&Š4£�{2ƒáàT‹MŸÛ-lv‰`ðí1ÞÎU�³Äv1s*ó½¯ž,tÌ¦¡_U§u¦¾¾°Qc¾šØ1qz¶Òü±½ä'hc*ðß¨SÙT ÉTXyXœ©À©#’ÁYéê¯±ˆËgY%&Â‡wÃ‡[wÆ·#þJ™7g?�¾ËÑÇªxJÈÎ)‚®››€6½bË_b§ï}sñ¶;Ê‰$´nq�™à3Ô­8=ò+L}Ž¼¹GþSGÈ'=R‚ÓP7ÑbA‡ÖaÚ˜%·MX‰ œ<ºV€5â%‹­ Üå€$–z�¨ðã,UE=®Ò…‚µš!8Ø¯.^œ�s…�^ˆB×jjƒ¹!¡–G©Àäâ,æº1±‚IR3žcÊ.@›à…¨*
	eÓ{ðÖÞÁêµ¾S}n”Ìí}J£ÆI–.W¸Ÿ’ñJ|wûšÄ `I¶�¦VÞA¢êxå7�òÉŠ='Dxò„½ð�˜}-L¨ïÄ_Xöžä~ ¦y2BÁs§ƒ«D¬•U?NýâôTø‘ŠeS)¡“N—¤üCÕÃ—q*�æ¤@4t6¯õU÷Ÿý[¬WiÌ&*Dšçj‘"³e[!#$bX×žLÜh³QU©HéÅ–“¥ÚPÎÍ¡ ØËÕ=Û—5KP¯×¸òÜ²"6^O€“jÇq%	1¯¯®/#ƒÒñ>	p¸¬é<BLN:ñØÎœ9ZÓw©nˆ#ÓÈ3tåB§Õ–LN¬œlÖk7Âu‘V[¯¼L�%†¾<ý’ê{CE9½�;Ã�ë‡@™¹ !V>8×+ÊR@ü“�àêÇ n˜f™³y¡M[bJ§<ÇŒˆÿÎÐÄ«;iIIë$R%‹2¿è9´ˆ3%‰K"ÓÌz{ÏOz*€–{ÊKéjë"ß§U

$÷Ö˜P%Ì(õR±&[/íÂ�äˆ†#F`OìH. g©SÄ)œ†ìÓíp:•%Gñ¶Ã'-po�ÿü`ÒZN�nžMÃ8\RÒr•_øÖ¤ù£’tã›†o`'d°‰K\¥˜Í„çÙ¬• t¯-a–Ûá+ªYˆdÀ“7º¾¤Ôî¯!A@ÓÖ]ðl�„kïŠÌç¯ƒán«TSîR+{Dø-¢Õ/�gÒßC¦…J`’,<Ëtx(ìä¤¥9fü!ú‰sExU�§Æ~?¡w	˜*“Ë9ó9•ŽDÉ9îˆOõYs*¾Â:$I.„#jß;ãÝõø-°
©ÔëÀê9�A¼Ïš:ÿ™¼ê`/’‹£3Ú\èqLs–˜Oõ“�£}?1S©ÐÕ—�Šò6ˆ¨œ®¸È!Õ"epa<fRÄ­@Em"kŠú\Þ!B©�GµÖæNÄMUë<ý'Ò>  Ô•…€¶à–¶>gæÓ¶6'*‰ƒÒˆ;9\(ÝÓE	škÝ*ÜJJ .Ëg·nÛ‡g®&ÎPÞý�:~P�b[5>ëR›W˜Ð`%´üý5„øëüÛ¸úp ÒR.ËwQÞÑÉ½ï¯Þ^|8{õâêfîMÑX>£néæâíßÞþ�Á8Y†7VÑ=’—·/nÎy³èD±oS¾‰t4u¿á5,¸’™qH*êí¢¦¦¦9|Ù�6±©I¬3ÍYRß[Ã¥ô:¼¸ùÅ89[GáÓÁ‘-Ïÿ^xtÂ/ù¿,Ë,�ÙW&å½¬b“–õÿìý$ÇKº
£½ ê·ß§¶­§@NjÉ&C�àŠæ]Sù3>D‹%!ŸÎ¶áÈ¡UÕ‘ml—lÄ§¶jˆB:†ÈÅuKmU¿7*Õén]SòkîF‡»Ñ1ÝÝº¤½Û>K*øtÖÞÐ¾§P�À×¡©\¢¤F±o�À'X,°M…ÓCÀ5L…»RYŠË½^äñêÌ¨ä^²Q¡]ás'4*CÍ¿±”6F-Žjõ±v~•f‘yRˆÅJÜSáhWÁÙ	¶AôŸæüf±ƒ'*QHa  ¾>Ïíä“~é²�µì¶îõìDŠ´2Û¿â±Ò˜i™G÷=ð²‰€Èr3FZ˜ýéôÔs;m´“¤Éþaxp‰þÃC›Ö[^è·Ý§ãÛúk¯ôøF�Ë“Gõ���~ê½QÎåË.¨ñhBqU³ß”Õ.Œª£n»—çPZÛOcì~‡®·²ŽçÂýHæ£Ø²Y‹þEáÆœÔbTùý…3ŒyAÆ1T¦3m°…/vc^'€üÎdn¯EOÔê¼F Ó_#ô±k}}Zá;u$¼ì”(»—EºÜ”žãcÖ*Æ
7UÖ,$YDÏëÈ¥Z3Ì™uêºže¦#~ÌeŒú¼-zŠíLa…sEŠèõ+úµ^ÃÛñy¶¾á½ðÃ)œñ‹àÝé{ˆ.ö~M4Ô•¹:ãÌ4âÐžÔ�|
…´h”«²AÕžöJV®Y²Æáyæ…^@…¾•uæ¦-3ç'OÝA]0»y+/�[=9«íÕZ½0à¦Ú¤Ð^¶í•…»ŒÁo;íSàˆXñS•¢öÍÊÈÔú»âNrf¸O¥ÈÏoæ¹ÈÁµZéµhÊþ3HÈ¯æ�d×� _ÂëåþÑ^âQÁ(Þ½?±;8/ <h®þÊwEæÖ¨ÚžÎX}² 	lŸ®gO¸Øj{Ì#®Ñ½7Ó�®O££fµ½:c0:î�‚Â†Ÿµûˆgw(w  +]-°[G¼:·¦Œ]Çáí›3GÝFÌ`1ßæ‘Îì½
µkÚvÖÅówþD{oÇ¤çd{ O·ãwzÂO4pª¨¨×á÷ù~¶µ!Í?/péÐ¯wu\«îÁËzÅ‘dÙEz[@¡¥F3¯B½ýÁ*¼ØÔ.Ï²ßHÀŒÇµÕtgb�[­	fŸmòÌëÈÊâÍv$Kz(¦OuŒ–§;âµŠÊ#D<Ý·�ÐlÆ„ç{9i±Oñ9=z;8O eJè¨Ÿ­þ‰§®õ ,IéÕB›ý�‹>ÎüŽ<M¥’†Ÿ©7ã­.™©hR ê%³4uS:ƒö„ðû&üÿfñNù® aê•Áåt8ø ˜Ù2»£G÷#Kè¹ZüCWA~ 2�\„}º€øƒOa¶Ïžíý¬3äøh9¯‘Tèb‰ïÙ—ÊÙ\ a'Õ(ÿ—¨—³‰xœ•XÛ’£6}Î|…“J�ƒ’H¥*Ïû²%DÄ
áµýõiIì¯LÆ5.<>Ý’NŸ¾h:UM�0íT†\uDN×Z¸RiÐoo�:Ð0>†ÉÛ›†o“Ðpøõí—…A£´�’‘ïPŽŠŸÁ.4ÌÂ4ˆÂ8Š³(¡'šÄ§( :¦uCrÌÓ~ÍF³¡=\¢�†Ñ‹¯E_Á´à+ˆ¢ˆº—û	¢ÅÏã®êÇW®;fL�GÆoRH—½'¿‹¿˜¥4PÃ¹	EOn¬“áQ	¢èÛoœÔBŠaP¡P¤Î`—KÃè@È·Žn–þG«wüÛ—¿þ&_ðÛžpÍŒv3zê�è€ÜÏ—îëüáÇ¹‘ÓcSËkAiÀ"VFeLVÐÕ¥JPºÏÉ ú<ÚèàÞW±ú3“F�’ñ³ªkËHÒF6ŒÇ�i ×kËÆ–\bk�l�P×P‘Šk÷œŒÀ‡8;ž)¹¤‡KºIaå$ áZŒFƒ1j^sÓj�pAo§Žõâ{œ ”·‚ØßKv¸dHH¶–ªÑÄ¾YÇi˜ìbGS9,R½�U�RKfÊ©®Ac(ã,¤œÓç¢3{˜&QÙÅŽýHênºZh±IŽ‡ê�û#l@5ë+Î¹ƒV\õµ'~}-+å„Î[-ñ®…·XgéÁâ}’Wd·q¡þ®˜ )p‡.Ý¶²ú\é×‘¬o©§Y{Q˜¯š½3É˜nsþq‘×4*°žSš¥AeqEQ�²bÕÛY²iÔhˆ�¾†Ñ�“ê”¢$Ã7ÛŠ”ì„1˜=ÁTq�nÈØ×XÆ9HOô:3´Ÿë{ g¼…=YÍÐ
,¡ÌèžOË®ñø½Ã!Þ0Ñÿè?é\ß£o§ÝCVÌ°’�îœ4Ó=ç¢=%§ ÖOówØ[ïrom‹$¥hæfº‡£÷Áîpxð^÷�w6Xà¾$X•s9m„¸C=b÷÷à¢°¥ÌŽéó4vª“ëÖ>À®7‚Þ£ðcÚà
§+fkµ?ð'LÄˆO7¯¾xcsÎqÓŽÍÙ|½Rv:0ZõÄ*kŒÛú†ß©Gñßq'l2ª‡æ¿º…ó�ÃŠ$¥Ç€å/Ê#åE¼¾tÏõÄý¨ƒm»öãÍv�h;È{:÷»¾Â j¯Œ—³ü«/Óî˜ÉFÿx6ìTeËšmŸ3S]§zg�o”¡…>òÚ�§§­5FÖ×
¥yV½Ôã¢yd”Æ9Íé)Iƒ’¢„�N”­ºÓÐÝÔTaÕl¡olN×µY8L"ŠSû)ÎÓ"ˆÓ*eÇº¨ðU‡ã„f-oDš«õ³•ò¦�dåH¾3;ìùïlÄùºôó1Äþ<XªP3�`+ÏÍŽâV®d¬Î¯ý¿0PÆw±ô9VÐ‹2÷?Œ� «6S	:Tºù(‹ÏÃˆ
Ýud¾–|¾?—9§{çêúÑ–°�=·¥%ïŽ„â•#‰“c�³’¦)j¡<m9À¬±’çBºDõà—½``�o=wÿ¹@<Â\>¼¢s�2puk&Ù6L)éÝ½P™£ÃØ(Ê�(�!}RÙ|}ìQò5Žê-ö§üŽW¬ÐÇíIš¶ìèž;ÿ¨{§®ü‰€%Ê¶æÉ'Þ|Ë]�Ò7× žZøçëŒ“hñÎÝ|iüøß€åUþ�?aèŸ7îó5BÇÿZ²]¼ì‰x, Óÿ“!‘°{“æ›³¿Ž“x²rypto v1.17.45“�³…	€=ì;x, Óÿ‘ô�“1.2.0“ ‘¬1.4.1±Õ‹9“`“³ƒ	<mÓê:x* Õÿô¹�¸5-0.20260524134132-0c0ba56e32d7±¹bª1°»xœ¼½×’ãÆÖ%|?OñÝ#Zðî�˜XžaÜLÀ{ï>ýÏb•Îa•Jê–Î™	©ËuôZ{§Ù.wfEÈÔÿÖ�Èq–Yò?+ôÛã¿„à0QŒcô#¡È0ÓørøÿóÕédnìã÷`v0;uú‘ì˜M=˜>0%¡e�Ë¢af€jÿûEÿ€ÌºßšîÉu�Â‘#~¨øŠ2]tîÞNq	y}Fçspºžœ’îÁUwKü[ÖuY�üuÍç�†áß 70ï¨*úÒ_xXJ><£€êÄg
ÎA[åÝ? 	i{Î´¿{jŠ°é•lí†!zg/î¤¶�D«q†.jÄ�Vè¦ý-&øøÔ/s6ÉÄÁ¼‘Ðïòö¼]bg­;%å0 P¬@§Ñ±ý@ß¥Ê;Äì¼©�Piú[Ø/âPènW|³Zõ”Äœ2Th/7NÆ±�Î¼[l,„(CßSAó?+üüþ—ÖÄ-XMú$`E1è~9n#or©ÙHA8bn®‰%K?ƒ{rd‰(Eh³W§P,:`íHBã±¬³µsS¯—sí*„÷•úuš»ñ¹ôàßPâ}lOPcTLÈ·œ³V¦Òò›–Ž–RèÑb^ÓœØ¹Î™ÝóE\�Ð@ÔyTéïš%ï'Ã�6Ñ"—?ÇÁÕ¦6Cn™1= Ó¢.ú¾û­èÀ¼¯ž»û˜}œÄçLÌIŒõ*PÌ„A˜"ƒåK–Ý³&V¸*ÆæÏ`^'ºat
£Á:Ä‰œ8­â{¾¡ÐÕ<jxÄ=+S·ð¬:Þÿþ_Y1çKøÔ”¹/coLqõ#íÆoÚÇÕã‡Q7> ¦~CÞHŽ#.Z9È‹
‘"®�c\é£Y˜G”3Q­¤úÛÀE1�3ÿŒäE¡óîL—‹N`+nê¹ÏõŒLç[‚ì9ƒd8QÌÈaÛ4~�«ˆ“v.æýÉ÷a5´ÜL%œ:†õõÎÍ®às”&Ç‰R·$9±èDC#é&¬ØÿœèE±®òß6CfÔãZúùÚíG}1ënÔ°±m0=6”„¸ê/bÑÎÉØõÛs®è"I¹ºÈÂëSÏ�ï'Îä»{Ü´qi5�{î~
J-ûÇ4/J]g5ãQŸG1ž	IböÚ…e�ó{«¡Ü\µ(duÙ?H¿Êö±Ö]øÆI¼k&nu§ûVéÅ¨¢ï©¬‰Tìç(+ƒÒßp�lóêþg\/ê­+ÎÄ�9J�×²»¬¿ÍXÃÂ@	8o�]qkÞââê�”6E4vS—Î?‚eÎß–FÌE×þ¨‹pÆýC–7bì]ÉnÏ¨î¢Ì—òÔ{§«…3íW›œ†hHÖI’Æ7Þ|ÃÌø¢êv¢Ðntg¼'Š½D§b™b×åÄ¬AÔËå¾wËÀ|ÝÓØ•I4ûŠúÅ˜´`4s2=�ŒËƒ¾IÀ{µ6ÿçã›—0�€‡P˜‚á„PˆÀT@ÁoÒXì=©¯lÜÞù8r#«=¸„6›"«\C‚ÁF:q²`Úÿei^†¨èÔaxh_¸Kþf8¨ÈðŒ¹hÝ>ûÜåJh‚v®>QPûÒ¾}
ƒ0x:�Ý4=¦á¹WäélÞ­Òv®‡nî÷Ò;•òÐ»Í…¼´ÍWÔEápŸX`ÎD ïq¼¨#Z÷=NÎNc"˜(A<qumCS¢ª¼&¹–™ÖÂ7àúóÛôöç‰û€‡Æ>¬ê¬¯COÀ
­¢3ðm€C>™»�/ýBŒÝ);)1ök ¯8Øw•¥v \x:÷‡ŠsÑê
3Ê ‘Þ_osÆ£)æwè¯±Ÿ?éÇnî¢®“õ±M¦yLÞcò7ì�£èjDú&*#®('€l‚,nY\ÇŠ;™}BŸYû?#{QP2˜“Ht­pS”èÖºzš~Ü! ÒD*¨Œ2á×“ »·þ'ƒ÷ˆÛ´ÈžsŽüF¼aç¢Êl¥lT&©ÖNVµÀsgÏó¥CÞV ì«{9¥þö‹øud«`Å”»—Ü¨µ·äŽ×Ê@Áq‹ôfMÛŽ@æöªGÒÏ(ÆäéHƒzz:ú]‘^Ý C¬­tm/vçÄË=€tÉœ™©ÎT[ƒV‹�ý@øû¯ÙIv¼+¥àLñy$Œ¾¡"ÏÜ¹rãÔd¤íæÝ/2eP¸sô~Â“&Áüæ}’‹&ž>ø©‹`Iîf‡²ÕéE†bÖ+Nðuw|ÝŽ›‚�tõOi^¼aLÞró$Åºuæ{ÂzfT^ÌùÜ3.”s'r–lUÛº_d›P°	Ú‡ÿßø�GE=#�Õ½ç é·Ž©A{Qî·-3:™¿ž8Í×L×Ò¨‰/ˆõ«j}Cô¢Ø¤,¶h›m-rè­>Õ+Ð†Ô5‹dõ°±É	Ôc“jûßï�ÎÇŸºeŒ’éÝW¾OÙ˜¶Ó=¤�‚õ.ëQ;^#*§³3lÖ—b¦„ôÐ�ïGÿ×‹z*hß¦C¢ô‡N*	þ–æ†{tÏ`uP¢“`§� ¤]a~•2iã¾{|7�ovyX¡wíàr>Ü‡Œ(äÖ†“Þº5rdl8Jb*©årp‰ñ3›÷—T¯&pãœW5.þ¡iú@Bö�Q„Ä” ;u·£ÿòxmñ6ŠÔ»w•%¸¥ pUÜúè.´À…Z�”k‡2lÁLQúË‹ãß/ŠøîN–Ä
¯î¡9™É—ÃjÅ”`ž„_*5ŽŠåîòËŠ¬Xðºô¸R»ÛI™<¿b¦± r+· ”Á#áó$h!üUUÍ>)‹Âð¢Êâ¸˜¡²C(_/GÙcèiµh¶^.ëº…÷G6Æ�)aŸî&ú3¢)×"JþMDQÒÏ?’6êâ¢}ú}Ÿ(hÜ-]ˆåðns‚[9WáÚõÕ!¯>¬’ªA?³í¿Êú¢òñŒ›îç(Â¡›D!«ç:›µû�`ò^0ˆ*¼,vÛß%�ò$ª¦åéíéßÈ76^bi”É|Œk¤
¹£ý®X Êƒ£eælÇÂD‡ég–øgl¯©ŒÚ@�½ÌV	‰ÛM¯¯FäŸÁ3
ùbF÷D‡Ã_Ææð·Iû1™Š¬MâËXŒîûšíŽ¥|Lì’É�Âi¼BW>TÞÊÌ¾Â5ÌqtÝ�õ³ˆàI_£uó¢2Ãk
o»p×Û5xØæcj%�¥´ToÝ´Ú«ÌŸ„·äžÐ)ÄGèð®«~	×Ljµ„ÔËèe×–3Ô•¯(5±.–Ž`’@UeÓÏŒçÏù^c•Í¢¬fˆ±‰!ðâ»ËÉ:M‡vs:SsÛô€ŸCàgîáwÚ	}®ü£L"¹c h—G91¸^™}c‘!£½Õñ‹´ªˆU8Ô |~IÿE’¾ l<pr‰®>ƒì@ÔûK¹Sy(eIß¯­1¥8oÝžþ‹æ±:Šö�
z·0Ç^"­‘�¦�·êr”dÆ…@W\½krrQ¨‡iÏÿ„âE7ãƒæêTƒ2o ç"²ÂýJÓŒŸ5çžu)LÓ3eG¡ßž!V �g¥ú‚†&“[»¸ŒÝfStÑö?†>–Û¿j¯^^RwFìã%ÄªyZ©«±«,B‚{°#”ßõmæuÓ³_çéŠ8zrá¿ÁÏšsäGˆÒQ=ÃI3Þ™ÚáÅ(4MiO±·WÓ¸_5ý_Y^T:œIôl¢X¸·Ý½jG7Í‹®YOè|�c=Á´ î05Ý~y=ÏÓG²ú¬•_¢â :º�5Ç¸!³ž‡ãÌ÷Znqâë#1®E+Ã Œ7vò¼Ú7ƒ®Ø™mì[|R_7ú´©bÍ¸�5¹‡Lb vX¬HÿÇÈfjßïu ä£0­öE}±¥Y?‰#œxnz™%ex"	§yÃ©
Ä¨i$»ýÀ× V(UCGÖ<Ë·“o±˜²Üý*xŠj×LÊC¬_ ²ù2âaÒ�-	öÉXMï{ìYrêPQZ=u¨P¾'C`‘¡z#êfŠ3(&€|2´ŸÁ½.Ä×FÜÐµè‡ÉÄÀ|•°]AX0/ò*Â\&Æºê?ï¨(i« ž0¢ªKÓ·ùŠöí�&˜‹°NÞë"ÝfÑôLÚzZ«·Ôó.µâMl�…d†ÐS†d0qÙºûÛø/jÐ�™˜qˆû=�Aå”*x>Ýî`o;Nu=Ë²Œê×«ö…fê¼Ýò`Ê?bõ�’”­R0~³éË0‰uÞ—,»—×¼ËÈP%º+Í†ºßªˆ-®Ó/a¾ˆë®FÏDå$Ó1‰ÏÏ9
–(+Î†oâWU<dÑXrÆgè8X“(ËßÊzSŸlïGKÈè‘ÂD¡LÃ0Jýˆ©”¤‰ EÑ(zªB§‹„œ9÷v¶îz¶huwZÓkg.7ÑMÚAŽí?æ{QS&=Ê‹\D/ò)kœlV´f¶‡…®+Ôcy™$ Â�ègç'ou
0ŽÆç‡$z˜„¨Gp¢‚±ûÿ¬ØïgJºærI�[%�Õl
ù`×0»;náºñæÖJh¾és4Ûèï¼f9WÝ»MCTwV|ÙÁQ¿ä¥w×ò½L!5èÒÙS `Ðû¼•ãlúüGÑ�cñVüšçîc=|( �Dïj™Ç‘Ã#g¦[Ž*>±#‡to,Ú®ïÐµ¶øõ8WyÄžóumWHÚûÞmu:2Kíl×N[€Ô§ Ù¡£…¯øûToŸcóR/Æ Á“!Ðƒ±'ŒŒƒ§{–Ò¹%H	N«Pp9 pºE�Ë'–ê´¶ŠNhá‹_çûo³½‚ƒ='»�Ä2m®¦¦Ÿö0ºˆÍlañH�˜ö‚Ög/Ó\´oœùÒmqO^Œíá^åiáŽžÕv„NŽœ•w ½ÂcÎŽ&Ù›ÚðUŸpÞ¯�¾æþ|—&âº9‹fl$ý*ASÃ�o@#îUŽ§l=ÎHÒ¤.nYæóÜOm×õ/1ž.Zâ—p¨bMDOƒËp×ð,:,›!.z8­CO�Âå‚¸Ù/a¾ˆÛPÊIV†
ÎŽ¸Ló™é!Ì¥Fj—KÊµ¸]¶dMê~D>/ÑÇDy¾ýYñÿYñ‡ù~Æ	B†5ûÑgË}•F,ÅÕÍÆ�·®ânÜË’ô¤UÑf!ìì§x¯'räá†5sÄšð†fM×{#aà¸Ù½±±GµgÒ¥¿‹ò	ú
[wÙ¾}x:ñO§Ce|f÷S¸L'é˜Ü›:¹Ðl);‰OTl��:Áž†Ð¹ƒ0ó—¨Øoè{-£åëã@UJ�D¢„ÝÁà ‹âøÀX«#'eTÒÏà^�ŸCØá¾�V‘‰ÒHm3Þ°c4µ¢�ô9ª‡±9c�²zß¢Nsü»êÏŠùÅ½»ÁÊ\ä‚‡R-FÂ	TñqOñÌÜB5Qñµ°þ0A_á^—‘ö°P3kà±&¹)tÉ#¶µ•L.KSrÜšð½9&˜£_Pë Í~”Û¾ýyŸzô}—V^JÁ¼åŠ	9°³ªdrJEtÜÀn:bá!ÜZõ¸¯Š�ù"nzãLÕÕ­Íˆjó£ëêa0£€³³ËŒÓ¶àáwâ‚ÙØ-}DùëQ¡(Œ ‰ã?08a*LúÝtv~ã†‰ëp«©B·~cñ¤¿5S0°À˜T¨ÁËÙþs¾5#)Ã’q±Æ+ÌE¥wójãF¬§�ÐRt›lïyŒ¾£}ï„Kú6ÝøûWNQqìŒ´§Œ½)¾�¸&šãUG"<pgô­>ÀMóuõ|‡øºÁ�sŒ:ú¦òÀ£Ý±¤è±ÔÐìj»ÆW>.ZÍ©ù:Ùom.`Zó7MÆGŒ¼%G¿ÁôÂÕ<ÐD6M™K`š¦Ž‰öÐ•¶ÍRˆßAiª¶À‡êôÏH^‚™ÄY”)wWleÌÎæ;ÑÔgH¾Ùæñ±Yqà®'²"sõ×›=lú·éÆ?â¬TlÅj¨�JŠÀU^k %pAâ°º†Ý&ef�™ÎÞ¾®¤/°äÇÉlE¡"XÓŠÉžíoÒÔ^ïnÁNžz‘kÚÙ=tý'b’Ÿ"“þZM{ahÄ8
B@¯*âbY§´|çšƒcÇYyEu°øêVž°ýc¡¤ŸŽ±ÑGŒ ?!ô#€ñ4É”Žß£_‡Å ®yÏ}Å.†Ì‚œ×Óóf7M0ÐIHM×í?¡zuñ·>eØK™7’ž ",ÍtPÉM:Š«=Ìªdr±qÐ¯CñÕÎ?'$øñÑž÷^9% V½ìÖh8�ü,àx»‡p¶øÒÜ]*íŽÜudÚ’íÀî§x/bâ—™X—zí²f§Œ‰ÒÜIà‘ècÿ´g2jR"tŽ¿˜e)â÷Þ‘÷`\Z™×Œ©<2T—L®^²ûÈÜwø˜�@®�á#zhLüê�¿¢½ˆhIûÉO0-Ö4ñxVLb­;zaÕÁHÃÚüfûñ;Íƒ¾˜Àä­œØ?âääG”Œs‘¾µ†$?³zÛßÆ}w{N‘à{¸
«Eâf[y*ÞNæŽ²)¹ŽhEI”³4tÎßmý_ezMÕKq¹°XM'•ªÔ`�3ášrnqoc˜çJŸ@ ç»ñzfÁí­¥ç=ò›ç<0@ÆÉJl¦ÃµV2³¯¡~M¡Ni;p"™Å>øSEþ€û"öÒ0JxÂÞAsätqP˜Â­	äÉAé'¡Šy�ˆóáëÆ‹ºÀf¹½#½7¼,¬ŠÑóuËFÑsóaÔ³ÑÖ�F
¾œò…œ¬ø~
÷«UÿŠö""£¤´„1Âp²mŸ#-‚”JïŽ ”�}làñ´q/…vþtì£÷ æ)âÎªæàÉ4ÏYï¬ºaªàíŠ•«e=" ²ýÜÇe‘T¾Wÿíõ°Iï�"gj¿™¢ºX^žšÐžrV¹ÖùržçJ»ô³™¾Ý’pê¢*™ß%öÌÉ‘·FF"$�¥(’ ùLää"}9u¸u¼1÷Øé‡ñá7ïæïó j,gÌ-¸”{òu'ýÆ×vWl#¡[ãrg¥)`C?bŠ°ýëbžönbi³9ÕaûC�A?6xOÆîí´ø=7y®ñå˜¹Ê*Ãæ²áú6ÏXäÃ"cëfX* E.Ô°òÚ•ðK˜¯Ý–Òà{N×�ëìD:^L”5vãäâÜÒè„œâû>Ÿ·O„õò¨gµ$/Ê±xÇ~—x´@Íï/¾|¾vd™Î)žÎ93^ò(Ðæ)¨!?ã¶é‰ê×@_6.ø°{*_æõ®`ÄâFŸq7Í�H‹%«ù*²n™{þsìr©‹ÇÐûô2Êåiä&•0q¹×Š]L9¢9¡‹"°ï=«Þs<!ï¡¿�üZž�oÀ:ÑXø2 ºnê—ˆ€½î
,ºÐ’>±=+dïñ!úBÐÞ»  Ç¤¯Ÿ6öÍº~¸!ïVJ{[wh¡Ì}J’s›%,n†÷«œNÊäKÝ€Ç~	óEÜ»õ3]Y9)-—woWw�í­´pƒ z‹#Ûþ¦ºuü:S^DÝØÿžyÔãòa\¡w átÆÇõ –÷ã²mÄìÜºûÉ�,¿Gc'¬^T[A¾®�¿B~m/NÄ˜îú¶aˆ³-Gø[Ä©Ø—NÌ÷TC@?é;æ@˜‡>ù\Ì»ºh‚‡](ÚÁ‰÷nºç’h!ÓNf˜ „/¾Šß¥3�ºáf-ÇQ;É¨–‡F-æ_[»íÒb¡ï3}"’4”ÐpI,�C4>Ô6wM
ÓÁÚŒü<�ePAý¬"}|ù)¢ øaŸ`ûAA8‚'ÓøóTi¸©]ApLtuîeº.€r•®²ÁLg–@Ôk9¥¡ãäÜÿ„/ŠºCº�MiKÚåï—ËIÂ•”�ê‡_iÕ51mq|Øc·µ¾´|Wu°L}7ÍÏc2ýÞ÷ô¬¯€ù[¥†K ÇÕÕ‹;œî†ÓWÖ V9äôÃZvÁ�]DúUÔ×|q;e.ºã!Iht«y=œK
Ìç¼D·¾†N¾ËnÕxùlkªL§÷ ô¹Iå©ŠºHtÆkËÛT@jxîá¥–ß�UFÌxvóÞÏjêOq^[:ÅÖ?3«Ó‡[I&.¼ø²âlY'h]]ÄŽCŽ›™}…ëßŠ§ÑÙÓÝ§µÉcº¾yŒÝW}îDð1šsSI”	¼¥´b³EUá/±^ÄË;aÊ=çÜ´	�öõá‚ûST¤ßÕãvRü = ·ê+äœÜæ7À�Ø×o�@þà!{æ q¾ÏËx”`Thœé#'�Ù¼¿ Œ÷H/¢%j2’b%UqX´ªˆµö„Õ�diŽ¶Ý°U#ÙøEÛ½Nê¤éÚ·ã8	—÷ö’w)Í“>>òZ‰—ˆ’™.-‹Ú�ë‡A¿„0kB·¥½°)tùÒÝô§ ¯Å.ÍÃM?F]�‡ùìîz"¤Æ…’%·Â)Hõ!ÈÐd>ç,u‚ýðÄûˆënpÖ ŠRuHx™BRƒºÎ™iXBõ.øØ_ç£ÝýÒ‹h`Pú%4¢íÎ¹ÚU¬³=¶¶(³FÔ¸(ÚÄ#l1÷Ÿ-bÝóü0‰-8?lâ»ÊÏZÊqÅ„àè^Ï#p°ÄÖ°1GI){ef?k!gQ,à mbm ÒÏ_Û~¤š]vrÐ·Wxg–Ì2£ÈÖØÃÙžOD$û�žjÛçI¯—[Z<ÛŠêw_þô]Õt,¦Ç¡¥Ù†Û�íÇŠv£¹j8V*	|ˆIqÒ¨Ÿ ½žòú<ÚÝÒ´ÐÉ•õÎSa¶q`Ä¶ãÕš‹žå>4ÌüVÄ÷‹L‡,³�n>¢ê8póAÞ±óâtÖ:ít¶ˆ~Ä`uZsþK¬Wñ�¹eëx½E;‰€±æ`âºü"Òs"«zÄ].Æ•6=úò£P÷¯£bî� óÆKtÅ #£¬X`”GÈe%Qö9bŸ
Sé'h¯¦º#eëæûÇ^±PØË!X{<ð�[ÓÙŒ“bÎªçø|tìï@ÿåÞ<ÚÓ±`³`7\SàpÁm£L=nÆE+Žä¾WRÖN&²©li?|í‰àýÆ#}z×Þ@9ð›ª¤)ÀíõQXï~Ž(e^¤ïm£e“v~G~nœDÞW8Í*5UÛà|!I,²›ž<(@·hÙªrCË‘Qô+�¯©e	Š&®?,dÄ"'LŸQ`¾Ó@ì Ú.v|`_ÊmÀóí{äiÚ�¾†wYÃh°jÞ4#?º\¦ƒfX¦îR;§pµÏÑ�¯¦)0Gî†e¿€øGé•#pÔðBµ�O|¡(Ø9o­Ö¥”<=B½>c'bš?„9(Ú·Öcè÷S&öš‹âîéàœð¦¬õh$ç$FíZCL�U± ã€_—ïWÀgÄ×óUôÒ7šˆÇØL’šµGïj›}š$Ëc*ÃDÙC�Tß�{?wMbÏïÌG¢Zý‘J®­Ž§*n¼dÈ_jïŠ�-ÔW9–�ßÎÒ'¼Wg™<â¸‚2“(ìC¨t,Þ÷|v„µf•Ò�,ùèƒm­?rÅo`ßn¬†Áô~ù�ü{ižV=‘Ón‰tXøK�hšìXûÌmr�XqMþÎ�üòEXBØñ• GxoR-Åâ`/� 3B(e2‹¡ÄÌÌÎÔý»u•%sþD%~#èç.=ÄÍò@‹¹¢±yÂ–Ý1å Ô�jÞ\«”{|8õwSô	îEH*8wñ) Œ#â$œp§Ö¼#”ÌÞÄ•;±œ{R'‰¾E};·|Ïi/7ãÚÖü!½!V9ø!?áj®ttÈÉòxI‚�’,k˜ïVü+Ú‹ˆaŠâ÷ÀFõŸ¹„cüÒù'Š÷<¹ÆÎéQd4<|-‘ï&½x¿”€üöD`/ãüêÒt<¶1â×��ì©
= ¨‹‚p8Ëg¬ËŽÁfgèÿ�õ"^•Ë†§[Œ {•‘3¶áfø :[Ü#±låî:Îzßé\´qrK>ÎŸr#Œ	WP !­¬ÄãJtvëˆÇ¶{–GÚ(`É¯ß®ñÏx¯!0n£s&Ó5+¾+£þy‡;Óöeô´¶mf½Â‰¤ ¼ŸÁâï•'â‘Ta0„0òƒÀJßJòÑûÕÛ`9íû0]YqÏªˆOn²V¬<"7‘LÌj:(?aû "
=>ÿ‚8Áâ˜Ncìy¨ap*·Ì¦òQ™%VÛ.C53rJ)@P;WÎ)$„&ßý‡lÿ/T|ü�ü€"(ÿEâg`¹f3pÆ –ïª*-Â#Ý,3¤CE@·¹óyúŠÁZgvÕwìï°ý×T¬»ìãfÁs/I©hvqH—#n¤ì�]AF<Üop>ÇŠ h2WšTöÝ^ú7Ö«UÇ$½™šõÕykOv*ohÏ—äfC•y!YŸÞ4Ò$‘ò;ƒÙó‡…{îõÂ¹“”Y§Û6ˆDªEev@iê=‚<³ÇJ¶I®l¢Ä×Ÿ€½FÆ ÓÐÞ*N/�¸ïRÚI �+w=,ÕU/t�lxú¢Ünþwã÷†	†o·ìþU*pîX™“ïœbI9MÇSÆÈ\~M´‡q÷hÚwR­ÔÐ¥ï¢÷/€/‚²Ëí² ˆ4zcÕ
‚oëXì»¶“IôÆ‹wl‘Ñãò­òqûZÏpX´9G‚Ñ%~ñàQ3™8/‡Kwá³“×Þ.€¡¸Ú·Bþì5�Dã<QœÒ®�½¸€]t„%þž¨WCJ€ªw&a”¼áÇÅ·˜É<Ñ{­þ¹‰Ù	Æ«Öê&z½/1%zö±œ¯AhÕ5z”@CÐœ©Lô�ïù÷Z­xŒÇäî°ìÊœÚÕpÒ½jÌè›2Ê‰îøbì‚øue�ùµ‹ªwÅŸ"BGa–Ê–	Î½’½Ÿˆq:ºkEÍ­ÛËYÊ~aÌíÛµóo°×žÅƒªF�9¦ÔðÎÏåÈPÙ\œ=cLØ‚ÈˆÏ•ï²‹)húúÝˆ|ÔÄ+ê’ÝìRè†õç*¯*d¨–Knäe¸ž'·Ñ-Ãù.`ûŒ÷ÚoB`w¯KzS¿‚J�Çc‹9ýRŸƒ|cí|ÎûDJªïVã2:ßsŠ�–E&Â†>RüÝ¸ß×ä*ÁŒ_/ “\ú£nŽTá}'ä+ÚëÁÈTE¹_xžÉŠÓe÷2TwÕÓ†Àðbî7iBÁ�¹�éEb¾ñôïÕ§çLã‹�ú©/ ¨Gêà%Š�±P
@>/’4ˆh -š:ƒßèÿÆzM#»p2BÓbðñ­E+GØˆ3w]Z+(‡O²?·¾�qøÑûIt²80?†þ0û“¿Žî~Úpžzˆ¿Ý)�åp#ºð–uße;_ _ÏØ«Ÿx;05†Ž1þÄ‰Š;žqŠ»æ;êêÂéÐäº|^‘M�%iQ'Ï/žÓóQß ÎüÍˆ;ì­2ÖÕ§Ü‚·j²£K :P ³@/‡$š²åÞýðEÐ;nû!X\ÐCg\ZHÎÜ¥ØÈøV/£ãY –ÇëpÎ(êKó[ŒÕ25]X,ÏYJÒãôq�õÝ÷ 	u•»{ ˜Îí°MfF"QWà°Š…7H¶TU¬Fþ%òÿ)øë’`›áåEÓs¹ß5Bò—éÄ|V‚¡µ£{ë=|™éKæ}á˜çggÔÕÝ¼õ½Û•§ä4C«ÇË ‚d¼;Î•È ›Ž¶†îhu¢³Ë=Z¤Óñ4_
ìŽúêßÕæ| Îªh`üg§Õîµ�ÉÈ&Óœ�ü#llOÔ÷àÅ|Ô ß"žg�3å¡‚_¥å«B¬.X�Àá¦
³
–!vsä‹ë
2
Ÿ!_0 ‡:Õ³eiÆª<l¼|$¥e‰.4vA8¢L�'c‹³:ôO�§¡.æýX{¯g#úîf¸ëË¡$ÏôØ{­¿‰éQ�ƒ>Æ€ÊÙL+"üºI¾B¿�„¡hôÊ®Ç)ÝûShþ¨„<Ž@ôÙ¡ËÐ¶d¥ÜMÜåÑ£w¾ÄÀßÇ=˜8í„2ÜyÂ’Bš,åÙEmŠºW·Nò&c¨Â¾�¦/•ð¦Hª|†Ï–û×UÑmÞ^¦œé¶[ž¯ÁÝ°rEnyêÙ}ækÑ}ò¦ý&ù<Å[óf,aÐyéz¥ò¢Bß”õäŠeº��RUx—þìE@@X„S�é1?n…[ÂÝ§É�‡ªó”�Àyx(«“>Ÿ 7ã�yìZð­¢�S/y½y™Í1Ÿ;…6Jƒ@„Žæ*@‹Âžk!�³D©26X÷sÀWK\´ZNçv®%NÇscŠ'»;1°_�”cfl\ºÒèÛ—ÙYÚ6ï¤`™»6Éþ}îÓ0CõÈ1ˆGTHÀ…<›C7ú4HƒÆ Gpk3EiÆ1:st�7R}Ñ˜Þ×Æ#óó½ÎGKZT£PÇålEâ&$÷¶yé¦…IïR,qz:«Ì¯ŸÉ6˜§·+oŸûx;çÃåœâŒ¼ç`ÅŸ8ƒ2æ @ƒ(ý8ø
n€‰ÌìL{D´_�|ÍLW:u•#‰¡S”·E;ØNÏsÀZZ]Ä¸öS��Í¾G®’}z/~††ç1D•Ó.Â­ÀVú}mÝÛl^Î$C±Ý`ŽQ@?|tºóE3gMjt‹àrŸJñaƒ‘Ù½ë�neîÜ–óèßã~ô»}Ü8À†â-Ê!ÊUá­HÄz ŽÖãX‰ÛÓ†YÜã‡ÛÏà^{Lé-:ÝÑSŽ�îÅ0ÅP•œ¼ÉÐø,Ÿn‰
ÀÆÅÐ‘Ï+¼�Æ%z?hšÇôýÍ ÅÀGMì©Få¬SÿXwè·6)é×ShE@Sø%§"³‘Û/¢¾šÌ-ÂóÙ?8N5%EÝ¦™á„t£ìÄ·�Ü´4—ä·H‰êÏÖ£/’ñí&O}Ç>Â®Çvn»H7b‘y ³tÃÙˆW!YÅvbÄx’,˜JÌY-IçËÃ÷ˆ/¢f¾Ë÷ Ÿ€’|=\S­ç®E34–}vè .æyg‹/�þôÓŽÝ6ý^ÿ¸ëC/„À(ô"($Æàgµhâ²:(ññ|@Qg¼&^aIL�/lD5¶„³m¨äü0½¨FŽ›Šq«¶¥z³IÖ[Î-„xDÃ–�l¯€ñP¯Û�ãì?>F­§÷ÇŸk[Xõß/Œu[_îä0±Iqm³Zò3³�
noêÁÚæ?ŽÓ'°×²ìl·ôÈŠº¨Hn÷ðÃjòKê ’6·¼§ðˆë’.ÔÐ0§tî?®Î?¯°Ë¢oa×Ð°Éš$o‡KoÄ.Ðo’6œ “öòz%ÄÃ.(b÷—X/âÍwxÜuÛT’4Uà°p÷¦“½Û9º«è¦êzrƒ,Î°óçð¶o’{0ÆÏÈ¶HÓº?6õÇ¥;AÂ1ù�ÇJaI‚¥ás™ËA³fx?™RM¤xÐ­Z2µÊ¢¶lîñ„»Æl=äÞAýòŠÉ?¤|µÙÊ‘$«G`íÝ,î#³­”jH­.›ÂŒÌ|Ýñ°Éä=ü2¿c×$sž<"ù¨.’vþ?ï]LOOýÑf$'ô#ÉŠrÜä�y­�;{*™ü€q�Ñ9dÊ®Y€Ôý-ì×ãÕ=²(x9Iw®ø«„I”}TÑÎ°Ç� tØ‡/WþHñ |;Ì†~º¯c§lëY8÷B[ÇC.àhFÕ~„CÇvÐ·á¤Þ6¯þòk3.Ú âÛµ×t'ì<°“=X®?pöª9÷Kª©e´³õÂŸtMÓµOhòý ·—2…Æ\·>Ï°«n‡—ÈÞªkòEÑvLHGÿhÙéÏ§óè‹À—R !íÎ³�ñˆ†ˆ8 ÓU"È&àn÷ÀB8å—ü/Ø�/£�& ú}˜ï¶–‚KÂ�4®RñÈsjt®é öã¢¶�	ÿºaÎ6ýèk+?tó^$Í)g9ºãEV*‚p·4:B¸ß{õ„˜L6_ž‚6íÛê#QXÓéÓSÁ0BÂ$L£Ø,A±Bš†ƒ÷~YÏ­¡Å1B.dÅµ„�Iª÷ã=ó7ß8®Þ#0ƒsy¿ÛÑ�ñu»XtI6üÄ„ð¤ù…%TŽÌH¬>>2l–(-¯LNéÎÒg“:&ÍÞ-ñ#ÿÉ“6{«¶¦éüB�B0BCo"S?,Æ"¥â y:6¢%GŽh¬ˆL/=4ªCR­(dŠ�YA¤îPÚÐ—þ£Êù¢î0„Ç=§Vâs†ä:WÁ;“ÔQ;š‡îæq—-”Ì�>[–±Ëú¤ïŸW[>=úQõ·Ï,v<œX¢›èr¶Ô½‰XÌÊÖÝ]¯‹À…f	ìfáüË¸¯20•ÊnpÊºkŸp©EÚ¾D¤¨’*9upÍ¹3Šû—ìóòx^Žò·F°i.ÒßŸj}ŠLNHq`ï=.l&	’?RA‡ñ [qÞ•²—5sÎÌþ%Ìq7K]tŠÔÌT Í?ûIªô�ô½MïÄÄÓ¬7K|…^â¢û>±Ø’0Ö÷÷ªßCÐ!<ßÌ¾Ü<„ò½é Û©�—&p«Ä9j7÷�Ôc~ã/Ã¾G†œIò
žEá'LËßbU4«Ÿ‰Ø¸bZZlå—öòié“1­w°žoï7³žÃ»ì„Í••U|dB¬É�K�ß�:M[:.Ïð€Ä…çýîµD’âÐ™‘®6(XØ ÌrFÒ…µ0ÓÏ>CŽ ïÝc"ÉÏVoNæ·ÇAë œÀ-xk„iE’±+’�‡»¼ÎÀ6Ôó²F—òx&—ò|cè,$N‰6aCm»¿úº¼’éfF�n‘Ëé…¿1°%©Ô8T©ê5@(¥aX/´óyclÁT<¢’÷×L“×WCU1gõ{w™5tÓŠ„udªy8Z¼#ÂŠc¿ê*jÏÑ¤m¿€øÚæ¤#wuÖƒ)Ë†ÔhA™ÉˆFŽNãîÅä(ä$Nyxk�/Àü¸íæ¹h÷,zy;‡-Y+r�ÃÚD'üp•^½e1}ãDm#Z°ÔÍC’›ü'ú}¸ã#©›§jñt­4ñ‘‹�>µÜùGÎ¹×£B•É\³èQz3fÝo]Ÿ´QÒNËô[ñ¼öûk(;‰.›�aœ<k'¤j‘…ÖÍ…ÄE\q»p‡–qnIÀCô"Øª+Ô�îÉ€)Ö*lº1ÅV€<f7ÀŸ Ý Ô˜36-ÏÊ¿›ßº@j{ÿû­äñöhñË"-¯S+cjÓ1W[Å:ˆš©�¨ùJq¹Ou,úÀ'°_Â|-®™V	F Ô�Z µ+ä}óºU"Æá·S($¨_H¹NzßC¿u9�EíÃ8.ÍãoŸïÜ®úí=}þ�lì£‡HêìyCè1£Ä»z—þìè´Â¦õ©Ó†ƒ¡ñÌÃ0íN”)PöÝòá>¾ÿWdxÌî4OðÚup:Ž$¡œªÊ&ïY!¸ž«ÒëÖ„­%Ÿþ¦(m2?x’¿}ñ: �(»Ü kz)èuT'ËbTæt¤�Úm•½Dé5’„ø‚üÉüþmÖ×ºŽp¸ºÌøLÑÙ	€¤jçI'p¾qB'q²¶«ôEÿdÁ¾a?[ÕËúr>Ü­Ì1Q`Êdž±AôµR§}`µ‹ž¬“``•›ò_ |=�“õ)6µgKî¢ÕŽìÞ“è¢-�Q#cÃÛÝz0®n\ôç¸/»¿Ë‹”×€v¾ª_…Sª7mSøÄŸD	ï0hïÄÍ®1Á?ýop_Ä6k[ÉÕ²I¦ãG¬.ôç¾½£ �ÈtÕA8ûöÒl€\.Ò_À?¼M”¼JmØ\&�f ¹m`[½qŽØBOê4z²ˆS“VmïO6îa_�gQ…¾ ’8´KÃVJnÊZø‰^ÑI¿ç-×œsPØ§3¿�õ&ãûëÓ[½¸,DAïŒ×´–>8$Z4;-Ãêr@ÖpÉé†äÂ¡*š$>ÌÝ_ }:÷<òX¬´,ë£ºj�’Çƒ*ã�–ˆ1ÏÇ¤ºÃW">þ~^þqo*ßE:N›0“®¸�q0êäòÄmèu¿¾†µÊ‚|Øn+êý)Î«!-c_´›ÖòQƒû†é²gP¾ÞŸZ¿…}€}¾#Ýý¶Mý[Ñ‚oŸÿõ”Íó–ÈVãRž;‹E×®ÎæË^€G£L�ÒTeÌÛ<ÿ°™ôH¯çš—›5_÷Óµ/kË
™Iéª¡âuÆ™}r­°hèyÆöocyûWgè¿«ï
QòÈ«¨BahÁ1¼&�q©g‘0Â áWep¡¦…lãBOÀg»:¸ÔÐÇÓ$AÀöS6‚áÇGÅPoµ~:…`ÿt;·ÈD›^‡ãÝ+~¢Y•$
*A.ãÖ-Ñ²ÄhºnÅ¨J9éOÙ°�.Z@Ï[uVõØÊ /
¦ÄƒQZŽcYê—«~¿e�‡ÙØ_b½ö$�W,¶sÄÖ NôŠ«oZ‘Ö‘N… œÝ„eE[ƒï¼™c¾@&·þó|yŒ‚?È „1ì‘ß…ô{hÁç®›pd™ºƒIGàÒ×š1ÈÐêµî®�]Ýh½ÂÚ?ãxQç&ÀGa%B ¾p>.S¤™¥Í*³tÛ�B$ ØÒú™?¡zx?ý}¯²Ñ0áo/v ø�ˆ†’4IHO^ÏÏÃÉbÖ}˜„0Jwö®g™xmÇA„¹ÀŽÖ¬ŠMÜ˜IÅÏ¾'C?B¼›tÌtÛn]bòªHÃå:É!œ_UcÞ"Â,æÃ1ä+,¨Ì¯käèuÍUÙG
ÎOHºº«ª–ïœ}Ïµ¨ Ús	@×*¼öº7;¥÷¯MæO›	ƒ0ä1Þ0þ#	ñ0
q8E‚×­;ÓÇÃœRGC¿á‰…ŒsKDbç¶Ó*:çr‡žûúë|¥"3üö¬ù!Œ€“$¢ð×ÆÚ;Î™ŽeY#$i=ô:Ø –'R«ÅPÄ³ìNàô*ú¡�PotAÃ–`Iý÷©úÀ0ö0Œþ ‚ô1|4žàAðß¤Â?üí1-®¡k=GÍB®Aˆ2%ªÒCæfóÐ‘(Çç˜?zÛñN.šs´%¨÷Ó5œ`8\T•{¨ D˜Ñ%._ÍäÛ¡hŽ<×å‡+ë„ï™tn"ÇøÁq{÷€×[™² É²òZúªäg¬ñ<žµeÍªøV¾ Æž°ÄgKn˜»êaÒsƒ_NåVNùu§O{}^ä
!C?`y+TÑHôé^¨yÓ²dÃùv_,eKÀ—¤ìK§6°¸�,	É]J"�ö'\ÈÇA]�¬>$ûÒ ÌlMH|&J/ÀY×«µå
a$u`/ÍÆWþŠôzûî6ê[¬¦7¬¬”P_ZìUbt2Y[Cu]—Ïˆ*¢À×YŸöéÓ(<,+Œ!4Fÿˆ¡†Ã8"êÓ»nÖ‰âW~�ÀXÂCÊÂó)Ÿ«òbµû¤sö).£â�µ*_©°Ç0ÃèÃ¶þ IEH…húSç6¬—n”ÐWS=œ…É%·^Ñ¹ò@ó³^sµ³ˆªàÇ¼
_×ËWª·×êpìaÈ J¦!–"Xšü7©>/ËîTå=¬ÉcN¶vÒslTá@ÃÜSƒ–Ç�éu=¬øÀKÖW{ø÷{øÜ5cŒ´L½[¸p1cNõâR-ÕEÌyé,5¦ü|âO€^Çƒ
¸dç@>+†-A‘¦ùÈ$Ã>¥Î¡èyïÎ°Ã#rþº�¿§ú9ÊÕ�lÃ†±¶¬d44è#B�Û-/@©ì·ãLîû²_÷ß¿ð>v–çF¬/>vn0#ô³sW­žOhåÐ—ølbàØCõkô÷ŠôéÆ#nw_FúC[&íR'«¢_a— 9{‡˜ééKx:ü!¦ùýWñÀ¢…áØS3ìD4nÕ¯“æ™ç\´Š¢l~l9Ã{Š²ÿéÕjažœ›ëv8û�8b…y=7c¢zÖ<ÅOè.qÀÓ.í×Y�»®þ´´¡Ð[÷<ù=1:@Ãy=Ú±X$ˆ=.ßc9ïƒ^á­ cM0¿ÖþEíÄí£„æ›�üa`'Ã>’$ac—æõ”À­D1G«©+�
Wöæ˜4$'Ú#O!¿î’OP/Â-ÇJ›:£Ù+ù
˜ý"�®4ï�Üï0ÑÙÒn‡W©új¸nÿ>�þ†¿�˜?LøÃÉbDÇýjÂ%¿45µ´PV"G´¦Œ�m§Ë‹[žUFŒSÚx¿B‡<Lá�8”ã4‚Â1ôŸÓ=’Ø'ÛúV÷h—çƒ›ð‡ïÄ�¥Æ.[�¢Âe�¶ˆO=vðÃ{
57çÜóœgŸÒ�Ÿ¿8âO¡^sç$EƒÓÎ4j"Çc¯´j…�V˜ƒÓ#çx¼MâÔn;öLP¿–¨‚¾x_ÙÑë•<WŽâ³SuŸ›þ¤¸çkVÊæ‰AbÙUb&ïdª%„C?ƒ{=Ê{L£ð<Š®’¼i4wå6–)›HyVc>"7‰Yy{}‡š%íóåº×¨ÇŽy$†=¦�Æc‚DHŽÞß(‘ñž§G‘êèó<×ìô³[u)G‹³ˆ«UØgÇ˜¥núÙ^ëE`EŽÚZE‹=íÀ,^ëUˆÑ;ÀFÝ®Háà±“÷W¤àË;Qƒù¯„éÙX‚Q�†ŽQ*ñ<<o¾9'÷V×Ø±Ö6Ø™Àá:ë$»æþ0]è(fŠyæôÕt¡fþ»ä¯=-g–a+s6©;`	ms}äÃ&«õ}ô'eì†œ=t?¬�ö«2|TMa jhúr�Ð,GðZ›¶ßö`¤¦¾¬=
MÂÒw÷ç³§ÿEòW§|ªœÚ£tƒðúíB­«’£l\Ò$T‘3À;|bf^v³mê[>^²¢>¢ËëH4ˆÛ¸¾*X«­¯’Ý$<n0A¦m¨X(�V[±S¸Óñûñ|�{��:0VÛÛV /3˜Ÿå=Y!BRK…bµAd¸‰Øw¨¯H>bø÷ÀÔ!ü-×»ˆßYU�âH3*©æÈI2–]„®îCë¼³ºó­	úæ‹¸G+¸QiÍðåHÍNÌqd¯ÜTHÅ’%‚^Ó;ßGOÛWÙ[mêùûJ~[á3O¼ýö`˜‚ß* #x‚P�	ütS¾#ŠÐ‘™{ËR5ôT¾X[-Ÿ9ÿñÏË.£PÇ÷ž¿î�\ð¿E~ä(öÖÓCQHSAFDôžØ ÆðiìrûÚÍÖû¡�õ-p£%Ù’)Žõ0˜…Sý3’…äcu’Â1íc¹@Á‰‰fRþ\F’®w&ÄQ%2¢ƒ��_¸Ú`NÒ¢�r°^š0Ëà�ùx"þH±f‡�q¤'ð‘™åËµ×kÀ™¹@¾Šo…A¼X4þÿ7v&KÊ"A ~•¹{`ßŽ%²ƒ,
7Av��§íî?‚6æ7æ¾È “Ü*³jfÖðsƒô``›Ÿ ‘¤€P-ÀJ ²÷ú‚³�½äLÞZ[üW_rÂþ™°?3ŸñìV�°²+8·ÊqœÂAdÙbÒT«ª³^ŸGøb{’E}gŽÿÉÙ¶\ñq)¨•ÚsÀhGì8÷â		MªX©Nô§Ì©´HÀó|u÷ðË¾Ãðg¾¥¾cÎ¼žJ¯‹âŽwQ	 :íòƒ„,†P?îs˜­D×¦ŸõHÛôËBZGnjåœ¶Í¬!ZyLuöÊä_D²*�¬øøº‡ð‰ý”*i.�d BEûLà� ]l2î |Ø™Ñm0Ð¸­su»Ãè>>²¶[˜{n&F3ÄèBÊ¬Up}×ºFB*Lštå„oâ½Ög^ëR›ÛFrÒ_5;\&ð$CiÄ^”üh4$g;TæfpÉAÀ“–ú‘µíš¶7ùÀÏÙ~ig¥ ëUŸõ€·âÐÕ'ˆÀgþiåÎñ6qŽó™„?pò{²ë¾´7—nqRžìÆ/ûIçöq9o¹XJz*�Åœ¹3‹m„D¼¦²4IQaz¥MxŠß›™l)ª‘ìÍg�!fÙë0ã�÷eÁ?¯÷úÏÝÓú~±üh. Öë£D;¹H)	ÉUó¥QÎ%ë,/ ý Úzåƒ‘¬ ,ŽóøR*¡Â=#Cûê5–Z9Ph3§ªŒ_§A¿xõ5ÈàÍ�KÇ	Ô�‚RæÐ¸ÁQKV®Îõšá¤»Ë(©ËlÀÙuÔŠþiëÄÁát¿K©<�m$D$[ÄØ©Ú‰~
}$jƒ¥£#­ÜÕ7”Yð/©Ÿðë´v}\ÚK�4>toó¬qÒ¦¯KT2>Š¾]µâñwÐF0Ý­æƒ` œù¸‚ØÄÔ1x ïÇð¡á%Ï•û0¥7¼vlÿî�8ß.·«F'v»GÔûñ Ôj�XA0ðÜW¹Yw>²¶ÉKCˆI´ã¦)aïÀ\‡óCj_mœ‚4ìj“j'Rò†Œªºûõv�›}UÍµ
œŒ˜3Cþ±zŒ�iŒz„§~—â'ÔvWW2p7p¹�Lôúð3&8>K^•-¸{7 C;BdÉ”š¿é¢n†ïs€ï¥¸BÔSæä]ƒ+áÕ5Ög§BFC±öéÓRy ÌŠþf#ŒÅ-SöZ<žÏ]ì¤>w6
¤¬ð×´ÝnŸBP †äþfº}Ý”ðQwóR1BT*¦©¥Áßm7DL¥u˜PÔpÇ³(Q��¬m:î‹Ø;�àšQÉÊ)a=Œ†åp×wOÍùØBo©­Þ�_ó�_n’þYG)ýE©ÌD«£WâÁ0.,å~WGŠ5‚f÷ŠZüj›…žó“NÚ9Dˆ�Eíl.Atç²ÀštÐth&·‘Ë){³¶×ÀÁïOçÜ#_˜ìŽ¿ §#äP%™Ðê5`1åBWæ™§�£\ê,ü	µ�Ç"Kž~.jsÎÝ›µW1IV©‚TàC  "Zže†íö7q¨‹è¾¹¨íRRlÃÜä+è‰B©u32+ãMñ½€x•hC%ôJ–^zÒû;h#˜#¬z§¹HÅ,°'±R?ÅF;9®:yvH.#b©t±ùgìû¡§ÿÓ¼è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 ""
}
«$²¡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n ‘ÿ100644 icon.svg ^pý”`9«ƒ8(í=ëÙü0¸100644 index.html §Ož;ùÁ@’6‰w4²ÑLÏ100644 logo.svg ¿ÛÃž—ìÿ6Ñ•£„µ_|nÉ%€Ê¸,(¶
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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Û³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¾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
}
«ÿÉàƒŸax  ßÿ¹¹�Æ0605-aade4dd9fd4c“q±êO»•DâƒŸYx íÿ°‡Í…°¡A“ÐB
³ŽB"AOXµáƒ 3x! Þÿ¹¹�Å05612-65220f1104cd“q±êO°H
Hâƒ ,x íÿ°‡êƒ°¾@“ÐB
³ŽB"AR$ìãƒ¡@x3 Ìÿô½�³(d5e65d4b50c127ce8982ef94f448dd17822448f7±¹b´…ânex ñÿêƒ‡‚°Û?³¾@,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ä„ÏYx ëÿ�§°“N“Âš³c:Fäslƒ¥Tx óÿô›�¸4±¹b$5#lô x óÿ¾o•o°C³lR"%yâà�·ex°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‚ªjx óÿŒ=�:°ì³w"r�å�Émx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ƒ«Hx øÿô›°;¦è‰@xH ·ÿêƒ‡‚°A?/1 h1:6NXqd1aDXvhIN13U0O3UvehDjG/9fQ0tb+w8ZpTjWs“�>“Ü?1³u@uAc´á‚­Vx îÿŒ=Ö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«—âà†á	‹ix‘ 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ƒ°vx óÿ‘‘�*1±+f¯ž¦x& Ùÿ100644 docker.yml £³×—87ä:ße³ >£o²C,¡à�8x` ŸÿÕƒ�¦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½ Bÿ�¤°“N“Âš£chas inconsistent caching“xM^Regenerate go.sum from the
# proxy state we actually see at build time and skip sum.golang.org“A	 GOSUMDB=off go mod download

RUN“V	W³¶	çÛFB€¨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„îx øÿ±¨°(
›7¨xH ·ÿ100644 ci.yml Ò5TÁØ·´µ¼¼[¹jx6100644 docker.yml ­3â+	ë«œÛÊ³’Xi¼…ŠÃŠ¯Pìj‚Kx
 õÿÁ	�°N’ÁÆºo�nx ðÿ¤�°Ú“ã5£$1ífì„§Lxìþ»/¾�š³º`“´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þ+è}€¤Fxœ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˜ìƒª9xœ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Ùà…7xœ  ßÿåå°_4Å�|W}³ÍUVôíès÷ó¢¹Þ“sòþofá|ƒ¯=xœ•UMoÓ@• 	j—‚„„¦V‰šÄ!F9 $¨HmH…"ª6ö:¶âØî®�¦‘9sà€ùÜ�8Á�¿À�À•?Áì:I7©Â¡Šú:ïÍÌËÎë§§?7¿~¿qçuHÙ1´Oïè±]k7×¯´ôøãÒ³uVb´mó€²F@\ƒ0£"kr-ÂiZÁ8³¼»¾@]>n,ÿŽs+Oâ+½HöÚ;ö)hePh¯«d3Û0ÛmóRÅs©Ðë¾ª@!X‘‡S,ÊœS‘i�¶`€:œÂ%¾CÓcC•Õ”Œ/SâÜí‡¸¶¢Ñ.éR \ü”À›zE‹ÿÞÒÎ§—pÃ™Ž•J×p¹" A>›dÙ¬ªBÇõŽÜÄèŸƒAh×sS8µž8R8xIðkD·F‹ Ý6#“2êI%‡?ï?ƒë@+z*j]m–²=Â¦º—EÿýÄ ƒý‰¢M¦ÝFG'ì+štø´�à:zbJ#”í°•"Ò"‰Šýµä»+@•š$tÒAaBÅŸ¡R/V†àPdX4EìÏ 6%1š`Š hâG9ñ¼Ôˆ“	‡*ï<jowŸ_(´?©yP­5§%Æ38öah©1pz’m‰ýß0‰N4=ÐPhæHé	L‹^ ¾ÜªÍÙÅ"îIÚQÄ¢“”©[¢n¡M¤¬OO©
hZ³ŽÈÉöÍäÔf‡ïLÂèb0€ŒŽêÈ¢îD Â¡LD“ØN\¼~u¶h\_‚{^G¤×Ä‰íË£>ˆ?dà±ë]ì¹ÁÀ;¶ïãbÇi½£Œ‚ôWæQÄ¥+z)ÉM2TÃývˆ"�Dh`�OÿÂ�ì 3B1oÇ¸Ex#4M»ŸC=j H‹ópv	åòøÎ5äGØZ(—A$7l‚¢Ò~ ¶tµ¢2_Wi!Þf^“I=G«:W+J‹‰÷z¹ØÞ”ØÄZþùZsÉõ1’ÌþÌ¦`Ãç}YáØ¸AòŽ2&^+½0Œœž.�µ2¸¶§ñ—kw×ÄÁ§¢ÁFßÆÈ,Ë'*ƒoñjtõ0¼c�àŒ xœ  ßÿåå°_B+äžŠ&©Ûp<çüä“�§ÿ“sòöÓá …ixœ}Q=JÄ@F¬2¢¢��±Ü,ÓRÈXKp-"2;3‰�ü1I$K9D."xàk¯áŒd7+f-¿÷½ïçñÞfï—/«ƒþóèâœ�øÀŸX”¨,i&dz,s%ƒ¨ÇŒQ¿‡ÎºaâÂR±TÀð#«EÈ*J=`Jfýþ�Gš¥A6È²šE\…r-ã?‘Ø†–Hg´:§sëQM°Bš9‘uI–'”¨œkön•k–Ï‰‹¬RÜÀD¤…Æ×2`U\®}]}@\Õ¸µ§ªä»ªxªt[]8ñÆ�ßM¦ë]�þ·sâã«M `ádZ‹¬ Sðh7�ÐÖ¡„ñ7Íë×ÉÇþ7C2›KàŽkxœ  ßÿåå°_ìÿ³õR¸Ë;õ‘’ÈÌ,ÕV]_“sò›oì�Kxœ]�Í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úÿ®ÉÝÔ†ö¥©Ž<á’xœ{ÊÒÏ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,�©:óñí·ôâ›rxœû(°‚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ä)€’Oxœ]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(ïå%€¡gxœ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@&«®ôn7k2kÁîf§djv| ÷