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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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.
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.
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]>
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]>
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]>
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.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]>
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]>
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]>
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]>
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.
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.
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.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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>
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]>