Commit Graph
100 Commits
Author SHA1 Message Date
zeekayandHanzo Dev 6819fb510c indexer v1.5.11: no shared SQLite cache between the index and its readers
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 05:40:00 -07:00
zeekayandHanzo Dev ce51f72a4d graph v1.2.57: no valuation behind the indexed cursor, stake from the P-Chain op
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-23 00:35:59 -07:00
zeekayandHanzo Dev b0fed15c5f The live stream answers under the page's API base, and block time reads in seconds
The page builds its socket URL from its API base, so a brand page whose base is
/v1/indexer/<slug> opened /v1/indexer/<slug>/v1/base/realtime and got a 404.
The stream now answers there, scoped to <slug>.

indexer v1.5.10 reports average_block_time in milliseconds, which the page
divides by 1000. The image builds on Go 1.27, the version go.mod requires.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-22 23:41:28 -07:00
zeekayandClaude Opus 5.5 a13033fc40 graph v1.2.56, so the AMM subgraph reads through the public RPC door
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_016dDw5Lp9sZFctwvZLn4nVz
2026-09-22 23:21:30 -07:00
zeekayandHanzo Dev e3cee27b5c Build on Go 1.27
Bumped the go directive to 1.27.1. Build, vet and tests are clean.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-17 11:16:37 -07:00
zeekayandHanzo Dev 1f94c45090 set the docker config in a step, not the job's env
The runner context does not exist at job level, so naming it there is a syntax
error and the workflow fails before a step runs. RUNNER_TEMP is the same value
and is an ordinary variable once a step is running.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-09 07:58:03 -07:00
zeekayandHanzo Dev ddcf55fd87 one docker config per runner, not one per machine
Five runner services share this user, so they share ~/.docker/config.json, and
docker/login-action logs out in its post step. A job that finishes takes the
credential out from under a job still pushing, which surfaces as
'unauthenticated' or a request for an anonymous token — intermittently, and
never in the job that caused it. RUNNER_TEMP is per runner.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-09 07:55:13 -07:00
zeekayandHanzo Dev c36a1c5d16 publish with the credential this fleet already authenticates with
GITHUB_TOKEN is scoped to the repository and GHCR authorises against the
package, and the GHCR_USER/GHCR_TOKEN pair failed the same way. GH_PAT is what
the private checkouts in this pipeline use and it belongs to a user who can
create the package.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-09 06:38:51 -07:00
zeekayandHanzo Dev 9d9326f8c8 push with the credential provisioned for pushing
GITHUB_TOKEN is scoped to the repository that mints it and GHCR authorises
against the PACKAGE, so publishing a package the repository does not already
own is refused as an authentication failure. GHCR_USER/GHCR_TOKEN exist
org-wide for this.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-09 05:25:40 -07:00
zeekayandHanzo Dev b901c7fa32 the explorer ships as an image, for both architectures
There was no image workflow here, so the published tags predate the frontend
this binary embeds: 1.5.49 answers /v1 and 404s on /, which is why five
separate frontend pods exist to serve what //go:embed already carries.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-09 01:48:42 -07:00
zeekayandHanzo Dev 6347c100e6 go 1.26.8
One toolchain across the estate. The tree carried 1.26.0, .1, .3, .4 and .5
at once, so which compiler built a binary depended on which repo it came
from.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-09 00:00:21 -07:00
zeekayandHanzo Dev 9c49512095 run on nothing but the binary
alpine carried a shell, a package manager, libc and sqlite-libs so that a
dynamically linked binary could find them. Linking statically removes the
reason for all four: the image is the binary and the TLS roots, on
distroless static, as nonroot.

The explorer serves its frontend from //go:embed, so nothing else was ever
needed at runtime.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 23:21:12 -07:00
zeekayandHanzo Dev 2d8e8c756e A subgraph's health reports the store's cursor, not this process's
Status() counts what the running indexer has ingested since it started. On a
chain already at its head a restart ingests nothing, so /v1/graph/{slug}/{sub}/
health answered {"block":0,"indexed":0} with every event still in the store —
measured on cchain, which reported block 19 before a restart and block 0 after,
while the same subgraph's GraphQL kept answering with its token.

That is the one reply a reader checks this endpoint to avoid. Read `block` from
store.GetLastBlock(), which is the cursor that persists, and keep the process
count where it is higher — a fresh store mid-first-pass has moved further than
it has committed. `indexed` stays the process count: nothing persists an event
total, and a second number meaning something else under the same name is how
this went wrong once.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 19:55:00 -07:00
zeekayandHanzo Dev e44378c65e Read a block's transactions from the block, not one receipt at a time
luxfi/indexer v1.5.6 asked eth_getTransactionReceipt once per transaction and
dropped the transaction on any error — `if err != nil || tx == nil { continue }`,
twice over, with nothing logged. Measured on lux mainnet 96369: block 2 carries
571 transactions, the node serves all 571 and all 571 receipts, and the store
holds 562. The nine that vanished are transactionIndex 407..415, one contiguous
run in the middle of 571 back-to-back calls against one node.

v1.5.8 reads eth_getBlockByNumber(full) plus eth_getBlockReceipts — two round
trips per block where this cost two per transaction — so a receipt read that
fails takes the execution result with it and leaves the transaction, and a
transaction can no longer disappear because the node was busy.

The release job also asked for a runner label nothing carries. Every runner this
forge spawns registers `linux-amd64`; `lux-build-linux-amd64` has never existed,
and a job naming a label no runner has does not fail, it waits — which is why
1.5.49 is the newest image.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 19:48:59 -07:00
zeekayandHanzo Dev dbb11a2593 chain routes: /v1/bc -> /v1/chain
luxd serves chain routes under /v1/chain, and the node, cli and sdk releases
already ship it. The deployed gateways still answer the old prefix, so remote
reads break until they are redeployed; the estate moves together rather than
sitting half converted.

Lines that record an observation - a probe, a status code, a past release -
keep the prefix they were written against, as do URLs on hosts that are not
ours. Avalanche endpoints move to /ext/bc, which is the path that host serves.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 08:17:18 -07:00
zeekayandHanzo Dev d5f79de648 Restore the chain prefix the running nodes answer
The rename to /v1/chain is real in luxd source, but the deployed fleet has not
taken it yet: probed just now, api.lux.network, api.lux-test.network,
api.zoo.network and api.hanzo.network each answer 200 on /v1/bc/C/rpc and 404 on
/v1/chain/C/rpc. Composing /v1/chain against those hosts reaches nothing, which
is the failure the rename set out to prevent.

The move belongs with the node redeploy, not ahead of it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-30 21:24:29 -07:00
zeekayandHanzo Dev d9814b1765 chain routes: /v1/bc -> /v1/chain
luxd serves chain routes under /v1/chain; the /v1/bc prefix it replaced is
gone, so anything composing it reaches nothing. Lines that record an
observation - a probe, a status code, a past release - keep the prefix they
were written against.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-30 20:32:29 -07:00
zeekay c78e72033c docs: chain routes are /v1/chain
luxd serves chain APIs under one segment from constants.ChainAliasPrefix
(constants v1.6.4, node v1.36.181):

  /v1/bc/<alias>/...   -> /v1/chain/<alias>/...
  /ext/bc/<alias>/...  -> /v1/chain/<alias>/...

Observations keep their original addresses: a dated note or a recorded status
code is a fact about when it was written, not an instruction to follow.
2026-08-30 17:13:55 -07:00
zeekay 38370cc0d3 build: name only the private namespace in GOPRIVATE
luxfi/* is public, so it resolves through the module proxy — which serves each
version immutably — and verifies against the checksum database under the h1:
hashes in go.sum. A tag that moves upstream therefore cannot change the bytes
this image compiles. GOPRIVATE names github.com/hanzoai/* and nothing else.
2026-08-20 08:21:59 -07:00
zeekayandHanzo Dev bc27c44c92 build: graph v1.2.52 -> v1.2.53 (a page before a cursor is bounded in SQL)
Co-authored-by: Hanzo Dev <[email protected]>
2026-08-16 12:55:01 -07:00
zeekayandHanzo Dev 901f415a37 build: graph v1.2.51 -> v1.2.52 (a pool's history is read through its index)
The swaps index on pool was BINARY and the read compares NOCASE, so a pool's
trades were a scan of every swap on the chain — seconds per pool. The index now
matches the comparison and carries the timestamp.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-16 12:38:36 -07:00
zeekayandHanzo Dev 40710e7f4e build: graph v1.2.50 -> v1.2.51 (counts and volumes come from one read)
A token showed $74,100 of volume over zero transactions, and the protocol claimed
16,062 trades above pools holding 14,827. The counts were tallied as events
arrived while the volume had become a fact about the whole table; they are folds
of the same read now.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-14 07:56:33 -07:00
zeekayandHanzo Dev 3855a10c88 build: graph v1.2.49 -> v1.2.50 (pools and protocol agree on volume)
Valuing the history fixed the protocol's total and left every pool reporting the
window a pass had just read — $206,718 above a list of pools summing to $4,159.
One grouped read of the swap table now answers both.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-14 07:44:33 -07:00
zeekayandHanzo Dev b01ace4336 build: graph v1.2.48 -> v1.2.49 (the history gets valued, not just the window)
This chain holds 1,018,429 trades and had priced the newest 50,000. The rest
were stored at zero, so the all-time total summed them as worthless and every
day before that window was missing from every chart. A pass now walks forward
from a mark as well as reading the head, and covers the table a batch at a time.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-14 07:07:59 -07:00
zeekayandHanzo Dev 6fd5eeceb8 build: graph v1.2.47 -> v1.2.48 (all-time volume covers every trade)
v1.2.47 summed the day series, whose rows are folded from the same bounded
window a pass reads — so the total was that window again. The value of a trade
is a column now and the total is one scan of it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-14 06:49:29 -07:00
zeekayandHanzo Dev e13eb7033c build: graph v1.2.46 -> v1.2.47 (total volume means all of it)
The protocol reported the volume of its newest 50,000 swaps as its total. With a
million swaps behind this chain that was the last twelve percent. It now sums
the day series, which is written once per day and kept.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-14 06:35:25 -07:00
zeekayandHanzo Dev d131150258 A preflight is answered with the headers it asked about
The allow-list named Content-Type and Authorization, and a browser refuses the
whole request over one header missing from it. The exchange sends x-api-key, so
/v1/quote and /v1/swappable_tokens both failed their preflight and the swap
panel had neither a token list nor a price. A literal "*" would not have helped:
Safari does not honour the wildcard here.

Carries graph v1.2.46, which does the same at the graph's own server.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 22:40:38 -07:00
zeekayandHanzo Dev 7f6d589927 build: graph v1.2.44 -> v1.2.45 (a coin's supply is what was minted)
The valuation pass reads every token into memory at the top and writes those
copies back near the end; the native supply publish landed in between and was
overwritten every pass. Zoo's two trillion reported as the fifteen billion
sitting in its wrapper, so its market cap and fully diluted value were both
wrong by a factor of 133 while the log said "2000000000000 minted" every thirty
seconds.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 21:58:19 -07:00
zeekayandHanzo Dev 211b1166c0 build: graph v1.2.43 -> v1.2.44 (a pool's trades are its own)
Asking for one pool's last N trades read the N newest on the whole chain and
filtered what the limit had already discarded, so every pool but the busiest
answered with nothing.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 20:46:23 -07:00
zeekayandHanzo Dev 10e064b566 build: graph v1.2.42 -> v1.2.43 (the heal drains every window)
v1.2.42 dated the first 50,000 rows and stopped, reading "this window has no
failures left" as "there is nothing left". 862,492 trades stayed dated to 1970.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 20:29:13 -07:00
zeekayandHanzo Dev c0db563975 build: graph v1.2.41 -> v1.2.42 (the heal reaches the rows it exists to fix)
Every pool's trade history is in this store already; the pass that dates it
looked for undated rows among the newest, where a row holding a block number
never sorts. Seventeen pools' worth of swaps become visible on the token pages
without reindexing anything.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 19:58:27 -07:00
zeekayandHanzo Dev 945eeb822d Give the token list the chain's coin and wrapped token
graph v1.2.41 offers only what a pool holds, plus the chain's own coin under the
zero address. Both facts are per chain and already declared here, so pass them
rather than let the list guess.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 18:02:33 -07:00
zeekayandHanzo Dev f6d2b66733 build: graph v1.2.39 -> v1.2.40 (a refusal names the venue it cannot build)
Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 17:37:30 -07:00
zeekayandHanzo Dev 186c8f0613 Route a swap on the chain its quote was priced on
A quote states the chain it came from, and the dispatcher read that last. A
query parameter, or a chainId sitting beside the quote rather than inside it,
could send someone's quote to another chain's router — and these chains share
deployment addresses, so the same numbers name real pools on the destination.
The narrowest statement wins now: the quote's own chain, then the body, then
the URL. The handler refuses a foreign quote as well, so neither layer is load
bearing alone.

Whether a chain can build a swap is one question asked in one place. Both the
router and the wrapped native become a twenty-byte argument, so a blank or
malformed one produced a call whose arguments had slid; a chain missing either
serves no swap route rather than an unsignable one.

A chain may declare more than one subgraph holding a book, each in its own
goroutine, so the book behind the unprefixed routes was whichever registered
last — a different one across restarts. The config names it now.

The dispatcher buffered ten times what the handlers will read, and called an
oversized body malformed JSON. It reads their limit and says which failure it
was.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 17:34:58 -07:00
zeekayandHanzo Dev ffbf00c524 build: graph v1.2.37 -> v1.2.38 (a hop fee must be a uint24)
Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 17:19:01 -07:00
zeekayandHanzo Dev 8f5f807904 build: graph v1.2.36 -> v1.2.37 (an amount must fit the word it is rendered into)
Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 17:15:36 -07:00
zeekayandHanzo Dev 8993b4773c build: graph v1.2.35 -> v1.2.36 (native input read from the hop that names it)
Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 17:07:36 -07:00
zeekayandHanzo Dev 41e5e20e5f Answer the swap form on the paths it actually calls
The swap form calls /v1/quote and /v1/swappable_tokens, not
/v1/graph/{chain}/{subgraph}/quote. Only the prefixed form existed here, so a
TypeScript service stayed in front of this one to serve the unprefixed paths,
and the two answered the same questions from two copies of the book.

A swap request already says which chain it is for — a quote names the chain of
each token, an approval names its chain, a swap carries the quote it came from.
So the chain comes out of the request rather than the path, and the request is
handed to the mux that chain's subgraph already registered. The handler values
are the ones the prefixed routes use; nothing is built twice.

Swap and approval need this chain's router. Without one configured there is no
address to address a transaction to, so those two routes are absent rather than
serving calldata signed against nothing.

The subgraph that backs a chain's swap routes is the one holding an AMM book.
Picking whichever subgraph started first would have been a race, and a CLOB
schema has no pools to price against.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 17:02:43 -07:00
zeekayandHanzo Dev e22f8ece93 Answer the swap form from the same book the explorer already indexes
Two questions a swap form asks that GraphQL does not answer in a shape a form
can use: what can I trade here, and what do I get for this. Both read the store
each subgraph already fills, so they mount beside its /graphql rather than
behind a second service keeping a second copy of the book.

quoter_v2 is per chain because the periphery quoter is deployed per chain. With
it set the chain prices a V3 route directly; without it the quote falls back to
the closed form, which is exact for V2 and an estimate for V3.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 16:27:30 -07:00
zeekayandHanzo Dev 4a38efb267 Take the graph that reads a token's supply, and one sqlite C
Market cap and fully diluted value printed a dash on every token page. The
supply behind both was never read for a token already indexed: a real symbol
was taken as proof the row was finished. graph v1.2.31 asks what is actually
missing, keeps every field it already had, and reports a supply in whole
tokens whatever the source. Asking is what a start does now, so the flag that
used to gate it is gone.

The same pin brings the sqlite the whole process opens under one name.
csqlite is the house amalgamation, registered "sqlite3"; upstream mattn
compiled a second copy of that same C and the link failed on darwin, while
the hanzoai/sqlite wrapper takes "sqlite" — already modernc's, which arrives
with hanzoai/replicate — and panics in init. One C, three packages, no
argument.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 15:13:29 -07:00
zeekayandHanzo Dev 9101f05419 Carry each chain's mint and treasury to the indexer
Supply cannot be read from a node — the genesis alloc is not exposed and summing
every account is not a query — so the mint is declared per chain beside the
factory addresses. The treasury address is declared too, and its balance read
live, because that is the number that falls as tokens reach holders.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-13 06:48:07 -07:00
zeekayandHanzo Dev 396fd6773c Take the graph that reads native supply and stake
Co-authored-by: Hanzo Dev <[email protected]>
2026-08-12 21:28:10 -07:00
zeekayandHanzo Dev f4d904d86e Take the graph that prices trades without the JSON extension
Co-authored-by: Hanzo Dev <[email protected]>
2026-08-11 17:32:39 -07:00
zeekayandHanzo Dev 939827e096 Take the graph that reports its swap writes
The valuation line now says how many trades it priced, so a write that changes
no rows is visible instead of silent.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-11 17:25:48 -07:00
zeekayandHanzo Dev bdd6f665df Take the graph that values its swaps
Pool pages showed volume over trades that each reported nothing. The indexer
computed each trade's worth and dropped it; it now writes it, batched, so a
swap list adds up to the total printed above it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-11 16:57:15 -07:00
zeekayandHanzo Dev 94a136ca7d graph v1.2.20 — an unparseable query is refused, not held open
Co-authored-by: Hanzo Dev <[email protected]>
2026-08-11 12:53:57 -07:00
zeekayandHanzo Dev 4f9994fa4a indexer v1.5.4 — an index follows the chain it was pointed at
Zoo relaunched and its index stayed on the chain that was replaced, serving
24,607 blocks of it while the live chain sat at 9,564. The block indexer
resumed from a cursor left above the new head, so it had no way back on its
own. v1.5.4 reads block 0's hash to tell a rebuilt chain from a slow node,
and starts over only for the former.

Every chain in chains.yaml is indexed by this binary, so each of them was
exposed to it; zoo is the one it happened to.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-11 12:48:38 -07:00
zeekayandHanzo Dev 9482004bbc graph v1.2.19 — every field a document asks for, and a row's token is the token
parseTopFields split top-level fields on newlines, so a minified document was
answered with its first field alone and no error. The explore page asks for its
bundle, factory, tokens, pools and day series in one document.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-11 12:20:57 -07:00
zeekayandHanzo Dev 82f46085bd graph v1.2.18 — a day nobody observed reports no value locked
Co-authored-by: Hanzo Dev <[email protected]>
2026-08-11 11:26:05 -07:00
zeekayandHanzo Dev b61d5fe7b7 graph v1.2.17 — token, pool and protocol day series
Every price chart read "Missing chart data": tokenDayDatas, poolDayDatas
and uniswapDayDatas each answered [] from a stub, and the client draws
nothing under three points. They now carry a day per subject rolled out of
the trades already indexed, so the history appears at once rather than
starting today. Swap timestamps become the block's time — they held the
block number, which is also why no day could be computed from them.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-11 11:21:29 -07:00
zeekayandHanzo Dev 63ad47a6f4 graph v1.2.16 — a chain's factories come only from its config
Removes the indexer's Lux-mainnet fallback for FactoryV2/FactoryV3/
Native, which every chain but Lux was silently inheriting, and adds ZUSD
to the stable anchors so Zoo's book prices in dollars instead of
reporting 0.00.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-11 11:11:58 -07:00
zeekayandHanzo Dev 93b0e4c5bd each chain declares its own AMM factories and native token
One process indexes six chains. FactoryV2, FactoryV3 and Native never
reached the graph indexer from a chain's config, so every chain but Lux
fell through to the indexer's Lux-mainnet defaults: the zoo subgraph
matched PoolCreated against Lux's factory, registered no Zoo pool, and
priced what it did see in WLUX. zoo.exchange read that endpoint.

These are identities of the chain being indexed, so they travel with the
chain: three fields on ChainConfig, passed straight through. A chain that
declares none now indexes no AMM, which is the honest answer.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-11 10:57:51 -07:00
zeekayandHanzo Dev e57e831570 graph v1.2.15 — tokens carry a price again
v1.2.15 publishes what the valuation pass had always computed: bundle
ethPriceUSD and per-token derivedETH. Without them every market on
lux.exchange and the explorer showed real TVL and volume beside a blank price
and no market cap.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-10 20:54:25 -07:00
zeekayandHanzo Dev ae873d5974 stats: measure the span between the first and last block, not the oldest and newest timestamp
1.5.12 stopped a block with no time from anchoring the span at 1970, which
fixed Hanzo — 58,370 seconds per block became 10.3, its real cadence. It did
nothing for Zoo, which still read 2,387 for a chain building one every twelve:
Zoo's stray row is not timeless, it carries a plausible timestamp dated 1.76
years before Zoo's own first block, and no filter on the VALUE can tell that
from a legitimately old chain.

Nothing about the value distinguishes them. The ORDER does. Timestamps are
monotonic in block order by consensus rule, so the chain's elapsed time is the
last block's time minus the first block's — by number. On a sound index that
is identical to MIN and MAX of the column; on a damaged one it is the answer
and MIN/MAX is not. Zoo's first block is three days old, so three days is what
its span should have been all along.

This subsumes 1.5.12: a stray row only moves the answer now if it is an
endpoint, and a timeless one is still skipped because a block that does not
say when it happened cannot bound how far apart blocks are.

The suite covers both shapes at once, numbered mid-run the way a damaged row
actually sits: epoch 0, and two years early. The old query reports 1.26e7
seconds per block on the second.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-08 08:19:05 -07:00
zeekayandHanzo Dev 16a96ed0a3 stats: a block with no time cannot bound the chain's history
1.5.11 made the average block time readable, and it read 58,370 seconds on
Hanzo — 30,602 blocks over an apparent 56 years, on a chain that builds one
every 11 seconds. Zoo read 2,389 for the same 11-second cadence. Lux read
50.3, which is right, so nothing looked systematically broken.

Both have a single row whose timestamp parses to epoch 0. The span is
MAX-MIN, so that one row anchors it at 1970 and the other thirty thousand
contribute nothing: the answer stops being about the chain and becomes a
restatement of the current unix time divided by the block count. 1.786e9 over
30,601 intervals is exactly the number that came out.

The span now counts only blocks that carry a time. That is not a threshold to
tune — a block with no timestamp holds no information about how far apart
blocks are, and including it invents the interval it appears to measure.

Reproduced in the suite: five blocks a minute apart plus one row at
1970-01-01 reports 3.57e8 seconds per block on the old query, 60 on this one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-08 08:07:36 -07:00
zeekayandHanzo Dev 216ef5d8bf explorer: answer every request from the chain its host is served
One binary answers explore.lux.network, explore.hanzo.network and
explore.zoo.network. Only /envs.js and the slug-prefixed /v1/indexer/{slug}
routes knew that. Everything else read whichever chain the config called
default, so Hanzo's home page printed 1,107,383 total blocks above its own
Latest blocks list topping out at 29,805 — Lux's number, on Hanzo's page,
overstating Hanzo's chain 37x and Zoo's 49x. Measured on all three brands.

Config.Serves(host) is now the one thing that answers "which chain is this
request about", and the three surfaces that were guessing ask it:

  * StatsService keeps a read handle per chain instead of one pinned at
    startup, so /v1/counters, /v1/lines, /v1/pages/* and /v1/stats report the
    host's chain: 1107259 / 29805 / 22728 on the three brand hosts, where all
    three read 1107259 before.

  * The realtime stream carries the host's chain. It carried all of them: a
    45s read of /v1/base/realtime returned the same 6 cchain + 14 hanzo + 5 zoo
    events on every brand host, and the SPA appends what it is sent, so a
    50-block page rendered 63 rows with 2 Lux on top and 8 Zoo at the bottom.
    ?chain= still overrides; an empty filter no longer means "all chains",
    because no brand host wants another brand's blocks.

  * A slug-less /v1/indexer/* request follows the host, falling back to the
    default only when the host names no chain (api-explore.lux.network) or
    names one that is not up.

Also here, because they were all the same mistake about the same column: the
timestamp in evm_blocks is a Go time.Time bound into a column declared
TIMESTAMP, so SQLite stores a datetime STRING, and everything that read it as
a unix epoch got nothing and said nothing.

  * /stats' daily charts were empty on all three brands. strftime with the
    'unixepoch' modifier returns NULL on a datetime string, the scan failed,
    and handlePagesMain discarded the error into `daily, _ :=` — a nil slice
    marshals to `null`, which the page renders as "No data". Now 22 points.

  * Average block time read 0.0s on a chain producing a block every 10s: the
    three-value Scan failed and its error was never checked, so the zero went
    out as the answer. Now 60.0 on a one-block-per-minute fixture.

  * /v1/blocks reported `"timestamp": 2026` for every block — CAST of a
    datetime string takes its leading digits, and 2026 looks like a number
    rather than a failure. Now a real epoch.

  * Transactions (24h) was the lifetime total. SQLite orders every INTEGER
    below every TEXT, so `timestamp > <epoch>` was true for all 1.1M rows.

blockEpoch and blockDay are the only two spellings allowed to touch that
column now. The fixtures were the reason none of this failed a test: they
declared the column TIMESTAMP and then inserted integers into it, so the suite
exercised a database the indexer never writes. They bind a time.Time now, and
on the old queries they fail with exactly what production showed —
average_block_time "0.0", empty charts, timestamp 2026.

Deleted: the eight per-channel SSE routes at bare /blocks, /transactions,
/tokens, /gas-tracker, /stats, /validators, /token-transfers and /internal-txs.
Every one of those is also an SPA page path, and one mux cannot serve both; the
SPA opens only the multiplexed stream, so they had no subscriber to lose. With
them goes the second envelope, the per-channel framing built on every
broadcast, and the client's channel field — SSE has one endpoint and one shape.

indexer v1.5.3 for the block transaction-count name.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-08 06:37:03 -07:00
zeekayandHanzo Dev 6156f554f7 release: retry the ghcr.io login instead of losing the release
v1.5.8 was tagged and produced no image. The job died at step 2 of 5:

    ::error::Error response from daemon:
        Get "https://ghcr.io/v2/": denied: denied

The credential is fine. Measured on the forge over 7 days, luxfi ran 365
ghcr logins with the same org secret and 27 failed — 7.4% — across 23 repos
and a dozen runners, and the same secret succeeded on either side of every
burst, including two hours after one. Three readings were tested and all
three are wrong:

  expired token   the same secret succeeded at 16:27 today, hours after
                  failures at 20:53 and 21:18 yesterday
  sick runner     failures spread over 12 runners; the worst has 3 of 7,
                  a clean one has 12 of 12
  throttling      no correlation with concurrency — 7.4% at one login per
                  minute, 10.9% at two, 0% at four and five

So it is an intermittent rejection from ghcr, and the only defect on our
side is that nothing retries it. docker/login-action does not, and the job
dies before the build, so each occurrence costs a release silently: a tag
with no image and no failed push to show for it. 27 of those in a week.

Five attempts over 75s, backing off. Exhausting all five is a different
failure than the one this covers, and says so rather than pretending.

--password-stdin, so the token is never an argument and never lands in a
process list or a log — docker/login-action's own behaviour, kept.

Same shape belongs in the other 22 luxfi repos that publish this way;
this one is the repo with a release waiting on it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 12:23:00 -07:00
zeekayandHanzo Dev 8558c7d562 honour chains[].enabled — it was declared and read nowhere
`ChainConfig.Enabled` existed on the struct, was written in every chains.yaml
we ship, and was consulted by nothing. Only `Graph.Enabled` and the subgraph
flag were ever read (supervisor.go). So `enabled: false` registered the chain,
spawned its indexer and polled its RPC exactly like `enabled: true` — a switch
that looked like a switch and was wired to nothing.

Found by using it. lux-mainnet's config lists spc, which is a sovereign L1 and
has never been a chain on that network. Setting `enabled: false` and rolling
the config changed nothing: the pod still logged

    [registry] added chain spc (…/v1/bc/spc/rpc) source=config
    [spc/amm] [indexer] starting
    [evm] Failed to get latest block: http status 404

at 1,800 errors an hour, against a chain that was already switched off.

Honoured in ChainRegistry.Add, which is the single door — config, env, mDNS
and the admin API all arrive there — so this is the whole of what the field
means, in one place. A disabled chain is not registered, so it does not index,
mounts no routes, and does not reach the SPA's chain list (which reads
registry.List()).

Enabled is now *bool because absent must not mean off. Every config written
while the field was inert says `enabled: true` out of convention; a plain bool
would read a future config that simply omits the key as false and make that
chain vanish with no error and no log. Nil is no opinion; only an explicit
`enabled: false` disables.

Tests: registration gated in all three states (true / false / absent), a
disabled chain proven not to reach the supervisor or the chain list, and
LoadConfig proven to decode into the pointer — a wrong yaml tag would leave it
nil and silently re-enable everything it disables. Verified by defeating the
check: the false and supervisor cases fail, the absent case still passes.

Also: the Dockerfile now runs vet + tests in the builder stage. This repo has
exactly one workflow, release.yml, which builds on a v* tag; nothing ran the
test files at all. Putting them where cgo and sqlite-dev already exist costs
no new job, no new runner dependency, and means a release that fails them
never becomes an image.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 12:06:16 -07:00
zeekayandHanzo Dev d1ddc08aae deps: indexer v1.5.2 — checksummed addresses resolve, /tokens matches the SPA contract
Picks up the two API fixes. A checksummed address (what every wallet emits)
404'd and rendered a 149M-LUX contract as an empty EOA; /tokens emitted
address/holders where the SPA reads address_hash/holders_count, crashing the
page on an undefined row key.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-07 09:01:57 -07:00
zeekayandHanzo Dev 23b0301b4d Merge the 2026-08-04 publication snapshot into the working history
origin/main was force-pushed on 2026-08-04 to a single squashed commit
("Lux Explorer", BSD-3-Clause), which replaced the repository's history and
removed every tag. That snapshot is not buildable-current: its go.mod pins
luxfi/graph v1.2.0 against the working tree's v1.2.13, and it dropped
stats.go, realtime_sse.go and the whole test suite. Building main as it stood
would have shipped a binary thirteen graph versions behind, without the dex
subgraph fix.

Merged with -s ours: the tree is the working source, and the snapshot is kept
as a parent so nothing is rewritten and main fast-forwards. The one file the
snapshot contributed, static/index.html, is carried over. The one change it
made that was clearly deliberate — dropping the agent notes — is kept:
LLM.md/CLAUDE.md are now gitignored rather than restored.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 14:09:08 -07:00
zeekayandHanzo Dev 393a4454b0 build: graph v1.2.13, and one CI path that a runner actually serves
The dex subgraph answered `{"data":{"markets":null}}` on a 200 while being
mounted, subscribed to the 0x9999 DEXFill topic and caught up to the chain
head. A GraphQL list field answering null reads as "not deployed / erroring",
so a DEX that had simply seen no fills was indistinguishable from a dead pipe.
luxfi/graph v1.2.13 makes an empty result an empty list.

CI moves to .hanzo/workflows/release.yml on the native git.hanzo.ai runners,
matching luxfi/graph. .github/workflows/docker.yml ran on `runs-on: lux-build`,
an ARC pool that no longer exists — the clusters have no ARC CRDs and
github.com/luxfi reports zero runners at org and repo scope, so every v* tag
queued a job nothing could pick up and waited instead of failing. That is why
v1.5.4 is a tag with no image. One CI path, one that builds.

LLM.md/CLAUDE.md become local-only, per the org convention.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 14:08:55 -07:00
zeekayandHanzo Dev a83aceaadb Lux Explorer
A single-binary indexer and GraphQL query layer for the Lux network.

Copyright (c) 2024-2026 Lux Industries Inc.
Licensed under BSD-3-Clause. See LICENSE.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 20:02:39 -07:00
zeekayandHanzo Dev b53f579f6a fix(explorer): never hand the browser the address the server dials
envs.js was emitting ChainConfig.RPC verbatim, so every browser loading
explore.lux.network was told the default chain lives at

    http://luxd-headless.lux-mainnet.svc.cluster.local:9630/v1/bc/C/rpc

which no browser can resolve. One field was serving two consumers with
opposite requirements: the indexer must dial the service-mesh name, the
SPA must get a public URL. Split them — RPC stays the dial address,
public_rpc is what the SPA is told, and browserRPC() falls back to RPC
only when it is actually reachable from outside the cluster. Emitting ""
beats emitting a URL that always fails: the SPA can hide the control
instead of offering one that silently errors.

Also adds the canonical /v1/stats and /v1/blocks. Everything under /v1,
nothing under /api — TestStatsRoutesAreV1Only pins that /api/v2 is not
mounted and never becomes so by accident.

/v1/blocks CASTs timestamp to INTEGER because the real schema declares it
TIMESTAMP and go-sqlite3 turns declared TIMESTAMP columns into time.Time,
which will not Scan into int64 — without the cast every row is skipped and
a fully populated index answers an empty list. That failure was invisible
because the handler swallowed the error; it now logs and reports it, and
the bound test asserts rows came back rather than just `<= 200`, which
passes trivially on an empty result.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-04 18:27:25 -07:00
zeekayandHanzo Dev 7d827e2b85 rpc: /ext/ -> /v1/ on our own hosts
luxd serves /v1/*; /ext/* 404s. Third-party chains keep their own paths.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-03 21:01:59 -07:00
zeekayandHanzo Dev ef9cc4e869 ci: the PAT step falls back to the one credential luxfi provisions
The ghcr login was fixed, and the very next step still failed:

    Resolve cross-repo PAT for private luxfi/* deps -> failure
    Build & push (amd64)                            -> skipped

because it reads GH_TOKEN then UNIVERSE_PAT, and orgs/luxfi provisions neither.
Measured: repos/luxfi/<name> has no secrets at all, and orgs/luxfi holds exactly
GHCR_USER + GHCR_TOKEN. Both names therefore evaluate to "" and the step exits 1.

GHCR_TOKEN is a real GitHub PAT -- it has to be, because ghcr.io accepts nothing
else -- so it can resolve private modules as well as pull images. Added as a
third rung that REPORTS which secret it used (`Using secret: $src`), rather than
a `||` chain that swaps credentials silently. Same fix already proven green in
luxfi/trader.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 17:39:35 -07:00
zeekayandHanzo Dev f12932e2c1 build: drop the luxfi exceptions — graph and indexer are public now
The list named github.com/luxfi/graph and github.com/luxfi/indexer because they
were private and 404 on proxy.golang.org. Both are open source now and both
serve from the proxy, so the exceptions have nothing left to do — and every name
on a GOPRIVATE list is a module Go fetches from git and trusts unverified.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 17:02:22 -07:00
zeekayandHanzo Dev 7571f94150 build: mark lux-private private, not all of luxfi
GOPRIVATE switches off the module proxy AND checksum verification together.
Covering github.com/luxfi/* meant every luxfi module was fetched straight from
git and trusted without verification — which is how moved tags got captured into
module caches and served from them indefinitely, making the same commit build on
one machine and fail on another.

Nearly all of luxfi is public and on proxy.golang.org, so it belongs under
normal verification. The private org is lux-private.

Twelve luxfi repos are still private AND still 404 on the proxy, so they are
listed individually rather than by wildcard — coverage now matches what actually
needs a direct fetch, and every other luxfi module is verified against
sum.golang.org again:

  bridge broker compliance erc20-go fasthttp graph hsm indexer
  mpc ordering password pool

The right end state is those repos moving to lux-private; until then the
explicit list keeps the exemption honest instead of blanket.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 16:05:25 -07:00
zeekayandHanzo Dev 849178ea59 deps: drop go.sum lines that disagree with the checksum log
These hashes were recorded from direct VCS fetches made while GOPRIVATE covered
github.com/luxfi/*, which switches off the module proxy and checksum
verification together. Tags moved afterwards, so what is written down here no
longer matches what sum.golang.org holds and the build refuses to verify.

proxy.golang.org still serves the originally-published bytes and those verify
against the log, so dropping only the disagreeing lines and re-tidying restores
a verified module graph with no version change. Lines that agree are untouched;
this is not a regenerated go.sum.

The failure followed the machine rather than the repository — the same commit
built wherever the module cache was clean. Any machine or CI runner that
fetched luxfi modules under the old GOPRIVATE holds the same poisoned entries.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 09:38:02 -07:00
zeekayandHanzo Dev e5bf01acea docs: publish /v1/bc/C/rpc, drop dead /ext/ URLs
/ext/ is gone from api.lux.network, api.zoo.network and api.hanzo.network
(all 404 today), so every published /ext/ URL was already broken. Convert
to the one canonical path and fix the URL forms that were dead for other
reasons at the same time:

  /mainnet|/testnet|/devnet path prefix -> the per-network host (404 today)
  api.testnet.lux.network, testnet-api.lux.network -> api.lux-test.network
  /ext/C/rpc (missing bc/) -> /v1/bc/C/rpc
  chain aliases + blockchain IDs under api.lux.network -> the L1s own host

Third-party RPC hosts (Avalanche, Flare, blastapi) are left untouched.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 18:22:24 -07:00
zeekayandHanzo Dev 8d136c6843 docs: publish /v1/bc/C/rpc, drop dead /ext/ URLs
/ext/ is gone from api.lux.network, api.zoo.network and api.hanzo.network
(all 404 today), so every published /ext/ URL was already broken. Convert
to the one canonical path and fix the URL forms that were dead for other
reasons at the same time:

  /mainnet|/testnet|/devnet path prefix -> the per-network host (404 today)
  api.testnet.lux.network, testnet-api.lux.network -> api.lux-test.network
  /ext/C/rpc (missing bc/) -> /v1/bc/C/rpc
  chain aliases + blockchain IDs under api.lux.network -> the L1s own host

Third-party RPC hosts (Avalanche, Flare, blastapi) are left untouched.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 18:00:43 -07:00
zeekayandHanzo Dev 066916145d fix(explorer): balances from the chain; one source for sibling hostnames
indexer v1.4.6 -> v1.4.7 and pass the chain's RPC to the standalone API, so
native-coin balances are read from the node rather than from
evm_addresses.balance — a column the indexer writes as the literal '0' on
first sight and never updates. Every address page reported a balance of
zero, WLUX's 159,126,795.518 LUX included. v1.4.7 also counts token holders
and transfers from the rows that hold them, stops calling the base fee
"burnt" on a chain that credits the whole fee to the coinbase, and returns
addresses on the evm_* schema instead of an empty page.

The network switcher advertised testnet.explore.lux.network and
devnet.explore.lux.network. Neither resolves; the live hosts are
explorer.lux-{test,dev}.network. networksForHost already derives sibling
hostnames from the request host and derives them correctly, so the
switcher now takes its domains from there and config keeps only what
config knows — the label and the chain ID. Devnet's was wrong too:
96370, where the devnet genesis cChainGenesis.config.chainId and a live
eth_chainId both say 96367.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 14:28:41 -07:00
zeekayandHanzo Dev 34a292b8e8 build: bump luxfi/graph v1.2.11 -> v1.2.12 (bounded swap window in the valuation pass)
v1.2.11 stopped the pass writing a row per swap; v1.2.12 stops it reading the
whole table too. The C-Chain's AMM swap history spans a million blocks in a
732 MB store, so an unbounded rescan every 30s grows with chain age forever.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 18:42:47 -07:00
zeekayandHanzo Dev a67f2a6644 build: graph v1.2.11 + label every graph indexer with its chain/subgraph
v1.5.1 in the cluster showed one chain's valuation pass reporting and two
others silent — and the log gave no way to tell which chain any line belonged
to, because the graph indexer was the one component here that did not prefix
its output. Config.Label (new in graph v1.2.11) fixes that: pass the same
"slug/name" this supervisor already uses everywhere else.

v1.2.11 also stops the valuation pass writing a row per swap (which is what
wedged the busy chains) and reads V3 swap amounts as the int256 they are.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 18:38:53 -07:00
zeekayandHanzo Dev 6837310ff0 build: bump luxfi/graph v1.2.9 -> v1.2.10 (AMM valuation pass)
v1.2.10 adds the valuation pass that derives the AMM graph's USD aggregates
from on-chain balances. Without it the explorer's /v1/graph/<chain>/amm/graphql
served pools and tokens whose totalValueLockedUSD, volumeUSD and token0Price
were empty strings — which is what left every TVL / 24h-volume / active-pool
figure on the exchange blank.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 18:21:43 -07:00
zeekayandHanzo Dev 4f1395b269 merge: P-Chain indexing + /v1-only luxd RPC + chain stats
Integrates feat/pchain-indexing into main so nothing is left behind a branch.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 18:20:57 -07:00
zeekay 48f3ebc0ab chore: sync working tree
Commits 3 outstanding change(s) that were sitting uncommitted.
No build artifacts and no secrets in the changeset (both checked).
2026-07-26 10:13:18 -07:00
zeekayandHanzo Dev 69a42bc965 feat(supervisor): run P-Chain (platform) indexing for linear/platform chains
runChain handles cfg.Type "linear"/"platform": build the platform adapter,
create pchain_blocks via the generic chain indexer's Init (which also runs
the adapter's validator/delegator schema), then poll — fetch+parse+store
recent blocks and SyncValidators each interval. Blocks are written via
store.StoreBlock (not the chain indexer's broadcasting StoreBlock, whose
subscriber channel nothing drains here). mountIndexerAPI gates readiness on
pchain_blocks for platform chains (evm_blocks for EVM).

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-08 07:12:16 -07:00
zeekayandHanzo Dev 7b2a74df96 fix: direct-luxd /ext/bc/ -> /v1/bc/ (luxd is /v1-only)
luxd v1.34.x serves chain RPC at /v1/bc/*; /ext/bc/* 404s. mdns.go
discovers luxd nodes directly (base = http://<host>:<port> from mDNS)
and constructs their per-chain RPC + platform.getBlockchains routes —
these are direct-luxd calls, so they move to /v1/bc/. registry_test.go
fixtures use localhost:9650 direct-luxd URLs, flipped to match.

Public-gateway URLs (api.lux.network in chains.example.yaml / README.md)
are left as /ext — the public gateway still accepts /ext externally.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-03 15:28:11 -07:00
zeekay 0b4ebbd9be build: bump luxfi/graph v1.2.6 -> v1.2.9 (bound BASE/QUOTE pair symbol + assetsBound on 0x9999 markets)
graph v1.2.9 enriches the 0x9999 DEX Market with a human pair symbol + assetsBound
so the exchange-api real-asset gate admits native markets (was stripping all of
them on the poolId-hex symbol). Pulls luxfi/database v1.19.3 transitively. go.sum
regenerated for luxfi modules (upstream re-tag staleness on age/pq).
2026-06-28 22:12:53 -07:00
zeekay a47755267b docs(brand): add hero banner 2026-06-28 20:37:35 -07:00
zeekay 271b9eb27b chore(brand): dynamic hero banner 2026-06-28 20:37:34 -07:00
zeekayandGitHub 2e96949096 chore: bump luxfi/database v1.19.3 (#13)
Co-authored-by: zeekay <[email protected]>
2026-06-25 15:24:22 -07:00
zeekay feaa1442b5 feat: enrich AMM token metadata — bump graph v1.2.3→v1.2.6 + opt-in backfill
graph v1.2.6 reads ERC20 symbol/name/decimals for AMM Token entities
(was address-placeholder). supervisor.go runs graph's BackfillTokens at
startup when BACKFILL_TOKENS=1 to enrich existing placeholder rows in
place (≤3 eth_calls/token, no re-sync), before the live indexer loop.
2026-06-21 23:55:00 -07:00
zeekay 066a8060c6 fix(docker): resolve private luxfi modules + GHCR push via cross-org PAT
The v1.3.1 image build reached the runner (lux-build works) but failed two
ways that the local build now proves fixed:

1. Dockerfile cloned private luxfi/{indexer,graph} over unauthenticated
   https -> 'could not read Username'. Those clones were vestigial: explorer
   has no replace directive, so under -mod=mod graph v1.2.3 + indexer v1.4.5
   resolve from the module cache, not siblings. Drop the clones; add the
   node-style BuildKit-secret git auth (ghtok) + GOPRIVATE/GONOSUMDB/GONOPROXY
   before go mod download so private modules resolve.

2. GHCR push would be denied (repo-scoped GITHUB_TOKEN). Log in with the
   cross-org PAT (GH_TOKEN/UNIVERSE_PAT) and stamp
   org.opencontainers.image.source so the package stays linked.

Verified locally: docker buildx build --secret id=ghtok,... compiles the
unified FE+indexer+graph binary green (go mod download 74s, go build 98s,
image exported).
2026-06-21 15:07:07 -07:00
zeekay b0ad7f49e7 ci: route docker.yml off classic [self-hosted,linux,amd64] to lux-build ARC
The classic-triple pool is serviced by fleet arcd hosts (dbc/evo/spark)
that are not reliably available — the v1.3.0 tag never produced an image and
recent builds sit queued. lux-build is the org-scoped autoscalingrunnerset
in lux-k8s (listener healthy 18d, scales 0->N on demand) that already
publishes the node image. Same one-line pool swap node/docker.yml uses.
2026-06-21 14:58:40 -07:00
zeekay c93423f735 deps: bump graph v1.2.2->v1.2.3 (native 0x9999 V4/DEXFill indexing, dex+amm subgraphs) + pin indexer to real v1.4.5 tag
graph v1.2.3 lands the native 0x9999 DEX indexing fix: dex/amm builtin
subgraphs, 0x9999-scoped Fill source (DEXFill log), keccak topic-hash
fixes, and re-genesis self-heal. The supervisor already wires
PoolManager(0x9999)+StartBlock into graphidx.NewWithConfig and mounts the
dex+amm subgraphs at /v1/graph/{slug}/{subgraph}/graphql; this bump makes
the engine those routes resolve against carry the fix.

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

Verified: go mod tidy clean; CGO build of the unified binary green;
go test ./... ok; graph dex_route_test confirms markets/fills resolve on
the dex schema from 0x9999 entities.
2026-06-21 14:52:43 -07:00
zeekay 5a664afa21 feat(explorer): pass PoolManager(0x9999)+StartBlock to graph indexer; mount dex subgraph
- config: add ChainConfig.PoolManager (DEX settlement precompile, default 0x9999)
- supervisor: runSubgraph uses graphidx.NewWithConfig (PoolManager + StartBlock)
- chains.example: cchain mounts dex (CLOB) subgraph + pool_manager
2026-06-21 12:26:11 -07:00
zeekay 2f0af7d7ca ci: route to canonical native arcd labels [self-hosted, linux, <arch>]
Replaces non-canonical scale-set / org-prefixed labels with the
existing labels every arcd host registers with. Matches evo for
amd64 and spark for arm64. No new labels added.
2026-06-10 20:20:27 -07:00
zeekay 3959357be3 feat(v1.3.0): drop embedded SPA — per-brand FE Deployment owns the UI
The Go binary is now a data-layer service only. The `/` catch-all that
served an embedded SPA shell is gone; v1.3.0 keeps just the endpoints
the FE depends on:

  - /v1/*       — indexer + blockscout-compat REST APIs
  - /health     — health probe
  - /envs.js    — runtime brand/chain config (window.ENV)
  - /icon.svg, /logo.svg — per-host brand SVGs (overlay-friendly)
  - /icon-{slug}.svg, /logo-{slug}.svg — extra named brand assets

Every other path returns 404 so the IngressRoute hands it off to the
per-brand Blockscout FE Deployment (ghcr.io/{brand-org}/explore-{env}).

Concrete deltas:
  - frontend.go: drop handleSPA, writeIndex, f.index; replace with
    handleNamedBrandAsset + brandAssetNames discovery
  - frontend_test.go: TestFrontendNoSPA + TestFrontendBrandAssetsServed
    replace the index-html overlay tests
  - Dockerfile: drop Next.js frontend build stage; runtime image is just
    the Go binary + brand SVGs (no embedded HTML)
  - static/index.html: removed (60-byte placeholder)

The per-brand FE Deployments + IngressRoute patches that complete this
split live in the cluster (lux-mainnet + lux-testnet namespaces). Source
of the FE images: luxfi/explore via Dockerfile.branded.
2026-06-10 13:21:01 -07:00
zeekay 73749c70f4 chore: scrub default from Dockerfile NETWORK_CURRENCY_SYMBOL
Replace the default  currency symbol +  network ID
(8675312) defaults with Lux mainnet defaults (LUX, 96369). The
explorer binary white-labels per-tenant via runtime env, not via
-specific build defaults.
2026-06-10 12:17:24 -07:00
zeekay 366e4c57d2 fix(spa): brand merging + env-tag canonicalisation
- BRAND is now a merge of SERVER_BRAND (chains.yaml-supplied) and
  INFERRED_BRAND (host parser table). Server wins on name/coin/accent
  when it's non-default; inferred fills in iconUrl/logoUrl when the
  chain config doesn't carry per-brand SVG paths. Stops Zoo rendering
  with the default Lux favicon when chains.yaml omits icon_url.
- networkFromHost now canonicalises "test"→"testnet", "dev"→"devnet"
  so the dropdown label matches the server-side VITE_CURRENT_NETWORK.
2026-06-10 11:21:02 -07:00
zeekay c75790d446 fix(spa): client-side network derivation fallback for older images
The Go-side networksForHost was added in v1.2.20 but the deployed
image is still v1.2.18 (CI pool gap — lux-build AutoscalingRunnerSet
is missing from arc-system). The SPA now also parses location.host
client-side so the network switcher works when /envs.js doesn't ship
VITE_CURRENT_NETWORK / VITE_NETWORK_HOSTS.

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

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

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

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

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

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

Test coverage: TestBrandStructHostMatching, 18 cases incl. the
explore-spcfoo cross-contamination guard ensuring partial-slug-
suffix matches don't bleed.
2026-06-05 15:05:22 -07:00
zeekay 1e602d24e9 feat(frontend): runtime static-dir overlay + substantive SPA shell
Two related changes that get the explorer SPA rendering real chain
data without waiting for upstream luxfi/explore to ship a Next.js
static export.

1. Runtime SPA overlay (config.go, frontend.go)

   New cfg.StaticDir (also EXPLORER_STATIC_DIR env var). When set,
   the frontend prefers the on-disk copy of any static asset over the
   embedded FS — so a ConfigMap-mounted SPA shell replaces the
   embedded 60-byte placeholder without rebuilding the binary. The
   embedded FS remains the source of truth at build time and the
   fallback when no overlay is configured.

   handleSPA + serveBrandFile both check overlay > embedded. Tests
   cover overlay-present, overlay-missing-file fallback, no-overlay,
   and env-var-only configuration.

2. Substantive SPA shell (static/index.html)

   Replaced the 60-byte stub (<div id="root"></div>) with a vanilla
   JS SPA — ~7KB, zero dependencies, no React/Next.js. Reads
   window.ENV from /envs.js, fetches /v1/indexer/{slug}/{stats,blocks,
   transactions}, renders KPIs + Latest Blocks + Latest Transactions
   with per-chain switching and brand color/title overrides. Polls
   every 15s. Stores selected chain in URL hash + localStorage.

   This is the explorer's default UI until upstream luxfi/explore
   ships output: 'export' and the Dockerfile's optional pnpm-build
   overlay activates.

Dockerfile: drop the leftover merge-conflict markers around the SPA
build stage, keep the explanatory comment.

Tests: 4 new in frontend_test.go covering overlay precedence + env-var
fallback. All existing tests (registry, realtime) still pass.
2026-06-05 11:05:53 -07:00
zeekay ebfcc0dc16 chore(deps): go get -u ./...
Updated dependencies to latest minor versions via go get -u + go mod tidy.
2026-06-02 11:11:38 -07:00
zeekay 4e7bb24121 chore: brand-neutral cleanup — remove cross-tenant references 2026-05-25 15:13:23 -07:00