158 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]>
v1.5.55
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]>
v1.5.54
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]>
v1.5.53
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
v1.5.52
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]>
v1.5.51
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]>
v1.5.50
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]>
v1.5.49
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]>
v1.5.48
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]>
v1.5.47
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]>
v1.5.46
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]>
v1.5.45
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]>
v1.5.44
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]>
v1.5.43
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]>
v1.5.42
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]>
v1.5.41
2026-08-13 21:58:19 -07:00
zooqueen f50ec8c9e5 Warm one chain at a time, the front page's first
Every chain this process serves shares one disk. Warming them together made
each finish late and made a caller arriving during startup wait behind the
contention rather than behind its own chain's scan. In turn, each finishes as
fast as the disk allows, and the chain the front page asks about is ready
first.
v1.5.40
2026-08-13 21:42:01 -07:00
zooqueen b5dc1e19b4 Count a chain's tables once, not once per homepage
Every number the stats surfaces report is a whole-table aggregate: SQLite
keeps no row count, and both the day bucket and the epoch bound are
expressions over a TEXT datetime, so no index answers them. Computing them
per request put five full scans on the request path, and the handlers share
a two-connection pool, so concurrent callers queued behind each other and
waited long enough for a browser to abandon the fetch and leave the
homepage's headline cards empty.

A chain is append-only, so within a few seconds every caller wants the same
numbers. They are now counted once per chain, held, and refreshed in the
background when they age, so a caller reads them from memory instead of
starting its own scan. The trailing year is computed once and shorter
windows are cut from it. Warm at startup, so the first caller after a start
reads a value rather than waiting for one.
v1.5.39
2026-08-13 21:14:05 -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]>
v1.5.38
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]>
v1.5.37
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]>
v1.5.36
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]>
v1.5.35
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]>
v1.5.34
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]>
v1.5.33
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]>
v1.5.32
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]>
v1.5.31
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]>
v1.5.30
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]>
v1.5.29
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]>
v1.5.28
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]>
v1.5.27
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]>
v1.5.26
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]>
v1.5.25
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]>
v1.5.24
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]>
v1.5.23
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]>
v1.5.22
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]>
v1.5.21
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]>
v1.5.20
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]>
v1.5.19
2026-08-11 12:20:57 -07:00