exchange-api answered this, and exchange-api is a forwarder: four deployments,
one per brand per network, four image tags of a small TypeScript file whose
answer comes from the indexer this binary already is. A second copy of a list is
a second thing that can be wrong about it, and one was — the Lux copy served a
13.27B LUX supply for two days after the indexer had been corrected to the real
two trillion.
The response shape is exchange-api's, unchanged, because the client reads it
today and moving a service is not an occasion to break a client.
The chain identifies itself. eth_chainId is asked once at boot rather than
configured, because a flag or an env keyed by deployment is another copy of a
fact the node already holds — and two copies of "which chain is this" is exactly
how zoo.exchange came to serve Lux's tokens under a ZOO label. If the node will
not say, the route is not registered at all: the client keeps the endpoint it
has rather than being handed a list labelled with a guess.
Quote and the portfolio API are not here. Quote needs routing over pools and is
real work; the portfolio routes the client calls are answered by nothing at all
today — exchange-api returns 404 for every one of them — so that is a gap to
close, not a service to move.
Co-authored-by: Hanzo Dev <[email protected]>
The valuation pass has always priced every token in USD — that is where TVL and
volume come from — but it published none of it. The subgraph format carries a
price only as the product `token.derivedETH * bundle.ethPriceUSD`, and both
sides were left unwritten: mainnet served derivedETH="" for all 8 tokens against
a null bundle. lux.exchange and the explorer therefore rendered real TVL and
real volume beside a blank price and no market cap, on every market.
"ETH" is the wire's name for the chain's own coin, so the bundle now carries the
native coin's USD price and each token its price in native units. The two
multiply back to exactly the USD price the pass computed, and the wrapped native
quotes 1 of itself by construction. No second price source, no oracle: one
relaxation, two projections of it.
Unpriced stays unpriced. A token no pool connects to a stable publishes an empty
derivedETH and no bundle is written at all — a zero there would render every
token free rather than unquoted.
fmtUSD could not carry these numbers: it is a CURRENCY format, and at two
decimals LUX at $0.00041334 renders "0.00". fmtPrice keeps 12 significant
digits.
Wrapped-native address is per-chain (Zoo's differs), so it follows the factories'
existing pattern — Config field, Lux mainnet default, env override.
Tests run under -tags nosqlite: the default store links two sqlite C
implementations (mattn + sqlite3vfs) and 283 duplicate symbols make the test
binary unlinkable, which is why the swap-count tests added earlier had never
once executed. They pass now, along with the new ones. Removing the SeedBundle
call fails TestPricesReachTheWire with "bundle absent", so the test tracks the
publication and not a reimplementation of it.
Co-authored-by: Hanzo Dev <[email protected]>
The CLOB read base is `<node>/v1/bc/D/dex/...`, not /ext/. luxd serves
one prefix (node server/http/server.go baseURL = "/v1").
Measured 2026-07-27 on both fleets, each with a bogus-path control:
mainnet luxd-1:9630 /v1/bc/C/rpc 200 0x17871 | /ext/ 404 | control 404
devnet luxd-2:9650 /v1/bc/C/rpc 200 0x1786f | /ext/ 404 | control 404
Ports come from each net's LuxNetwork CR spec.ports.http — never assume.
go vet ./... clean; go test ./indexer/ -run CLOB -> 3/3 PASS.
Co-authored-by: Hanzo Dev <[email protected]>
Token entities were seeded with placeholder metadata (symbol=addr[:8],
name=full address, decimals=18) — no contract read happened, so the AMM
graph returned every token as its own address. Add erc20.go: read
symbol()/name()/decimals() via the existing eth_call RPC (canonical ABI
string + legacy bytes32 fallback, garbage-decimals guard), memoised once
per token (revert→placeholder cached too, never re-probed). Route all four
seed sites through a cache-first seedToken with a cold-start no-downgrade
guard. BackfillTokens (opt-in BACKFILL_TOKENS=1) enriches existing
placeholder rows in place — ≤3 eth_calls per token, no re-sync.
Red re-verify found the V4 AMM-spoof gate left a parallel bypass.
MEDIUM — V2/V3 AMM handlers were UNGATED: handlePairCreated, handlePoolCreated,
handleSwapV2, handleSwapV3 accepted events from ANY emitter — a parallel path to
seed phantom pairs/pools/swaps and inflate the SAME public Factory/TVL/volume
aggregates the now-gated V4 handlers (0x9999) block. Mirror the V4 gate:
- PairCreated/PoolCreated now require the canonical FactoryV2/FactoryV3 emitter
(the AMM trust root), exactly as V4 requires 0x9999.
- V2/V3 Swaps are emitted by the pair/pool, not the factory, so the gate is
"is the emitter a pool the canonical factory created?" (isKnownPool). The
factory authorizes a pool; only that pool's Swaps are honored. A rogue
factory's PairCreated is rejected, so its pools are never registered, so its
Swaps are rejected — the whole parallel path is closed.
- Pair/pool keys lower-cased at seed so the Swap-side lookup is RPC-casing safe.
- Factory addresses configurable (Config.FactoryV2/V3 + FACTORY_V2/V3 env),
default to canonical Lux mainnet (LuxMainnet.Factory{V2,V3}); main wires env.
INFO — the per-namespace resolver limit lacked the `if limit < 1 { limit = 100 }`
floor that resolvers/dex has, so a negative/zero `first` flowed to ListByType
(`first:-1` = "no limit" in SQLite — an unbounded scan). Brought every pl()/
parseLimit() copy to the dex-canonical body (floor <1->100 + cap 1000); fhe and
platform also gained the previously-missing 1000 cap. All namespaces now identical.
Tests (go test ./...): indexer + resolvers/{dex,exchange,fhe,platform} green.
- TestHandleV2_RejectsNonFactory, TestHandleV3_RejectsNonFactory: rogue
PairCreated/PoolCreated/Swap seeds nothing and does not inflate the factory
aggregate; the canonical-source event IS honored end-to-end.
- TestPL_ClampsFirst / TestParseLimit_ClampsFirst (exchange/fhe/platform):
negative/zero floored to 100, capped at 1000.
Less-typing canonical short path. With GRAPH_PREFIX=/v1/graph the
binary now serves both /v1/graph/graphql and /v1/graph/ql. Same
handlers, same payload shape.