7.8 KiB
graph
Org: luxfi · Ecosystem: lux · Path: /Users/a/work/lux/luxfi/graph
Origin: https://github.com/luxfi/graph.git
Discovery
This file (CLAUDE.md) is the canonical agent-facing readme; LLM.md is a symlink to it. Update either name and both stay in sync.
Where to look first
README.md— human-facing overview (if present)package.json/Cargo.toml/pyproject.toml/go.mod— language & deps.github/workflows/— CI surfacedocs/— extended docs (if present)
A swap's timestamp is a TIME, and the poll is what makes it one
eth_getLogs says which block a log was in and never when it was mined. Until
v1.2.17 the swap handlers wrote the block NUMBER into timestamp, so trade
history dated every fill to 1970 and no day could be computed from a swap at
all. stampTimes (blocks.go) now reads one batch of headers per poll and hangs
the time on the logEntry, and poll returns an error rather than advancing
past a block whose header did not answer — a block is indexed when it has been
read whole.
SeedSwapData.Block is what tells an old row from a new one: no swap is mined
in block 0, so Block == 0 means "the number in Timestamp is a block number",
which is exactly what healSwapTimes needs to look the real time up. That is
Dated(), and both the heal and the day series ask it. The heal runs once at
Run and stops when every row has a time.
The day series (series.go) is a SECOND FOLD over the valuation pass's trades
tokenDayDatas / poolDayDatas / uniswapDayDatas are rolled out of the swaps
already on disk, which is why history appears the moment a build ships instead
of starting that day. valuedTrades reads and values the swap window ONCE;
valueSwaps folds it into running totals and writeSeries folds the same
stream into days. Do not add a second read or a second pricing path —
valuation.go owns pricing (see its header).
- What is exact. A pool's candle is its own execution ratio
amount0/amount1. A token's is that ratio carried to USD through the OTHER leg, so it is exact against a stablecoin and exact in shape against a volatile one. Pricing a token off its OWN leg yields a flat line at today's price — that mutation is covered by a test. - What is not. Value locked is not in the trades. Each day records it while
it is today; a day that passed before the series existed reports
"", never"0.00"(fmtHeld) — zero would draw a cliff to no liquidity. - Write only what moved. A pass recomputes the whole history every 30s;
idx.writtenremembers each cell as persisted so the steady state is one write per subject that traded. Rewriting everything is the O(rows) storm that once stopped this pass from finishing. - Rows are entities (
storage.TokenDay/PoolDay/FactoryDay) built from theenginestructs, so the json tags ARE the wire.Store.Entitiesreads a whole type then filters/orders/limits — a SQL LIMIT before the filter would answer with the first N rows rather than the first N matches. - The client draws nothing under three points; fewer renders "Missing chart
data", the same as an error.
pairDayDatasand the hour series stay stubs — no client asks for them.
where learned two things a chart needs: a filter naming a reference
({token: "0x…"}) compares against the referenced entity's id, and an exact
text match ignores case, because an address arrives checksummed as often as
lower-cased.
DEX indexing has TWO orthogonal sources (EVM 0x9999 logs + native D-Chain CLOB)
The dex subgraph (markets/fills/orders/orderbook) can be fed from either or both of:
- EVM 0x9999 settlement logs — the cross-chain SETTLEMENT view.
handleDEXFill/handleInitializeV4derive Market/Fill fromDEXFill/Initializelogs emitted by the 0x9999 PoolManager on an EVM chain (C-Chain). Source =indexer.goeth_getLogs. - Native D-Chain CLOB read RPC — the TRADING view. On a native-DEX deployment the
trade engine IS the D-Chain (dexvm): a trade is a consensus state transition at
Block.Verify, NOT an EVM event, so it NEVER appears on the EVM RPC.CLOBSource(indexer/clob.go) polls the committed-state read RPC…/v1/bc/<D>/dex/clob_get_{markets,trades,orders}(GET/JSON, READ-ONLY, zero consensus impact — seeluxfi/dex pkg/dchain/read.go) and writes the SAME Market/Fill/Order entities the resolvers serve. Enabled byConfig.DexRPC(graph) /dex_rpc:per-chain (explorerchains.yaml); empty = source 1 only.
The two sources are ORTHOGONAL — distinct loop, distinct protocol, distinct endpoint;
they share only the store (the sink) and the entity shapes (the contract). Running the
CLOB source NEVER regresses AMM/EVM indexing (the amm subgraph never gets dex_rpc).
Both Markets key by poolID, so when both run they converge on one Market per pool:
writeMarket refreshes the live CLOB book summary and MERGES through any EVM-accrued
volume24h/tradeCount/lastPrice it does not own.
Markets serve on the same host under distinct slugs (.../cchain/amm/graphql,
.../cchain/dex/graphql).
Native CLOB source notes (clob.go)
- Fills are a GLOBAL feed. The 80-byte on-chain trade row carries no poolID (frozen
GPU ABI), so
clob_get_tradesis a global height-ordered log.writeTraderecords every fill as aFill(the trade-history view) keyedheight:seq(deterministic → idempotent re-read). Per-market figures (open orders, depth, best bid/ask, remaining) come fromclob_get_markets' authoritative per-market book summary — we do NOT fabricate a per-market volume by mis-attributing the global log. - Incremental.
drainTradespages bysince=lastHeight+1; a steady chain costs one small query per tick. Symbol decode: a bound market's hex symbol (4c55…= ASCIILUX/LUSD) → human pair; an unbound poolID stays hex.
Native DEX indexing (0x9999) — the EVM-settlement source detail
- Single Fill source. A settled fill is recorded ONLY from the authoritative
DEXFillevent inhandleDEXFill.handleSwapV4is the AMM-side view and does NOT write a Fill. Do not add a second fill source. - Emitter-scoped.
handleDEXFilldrops anyDEXFillnot emitted by the configuredPoolManager(0x9999) —!isPoolManager(l.Address)returns early. This is the anti-spoof control; keep it. - Topic0 hashes (
indexer/events.go) are keccak256 of the EXACT signatures theluxfi/precompileemits (see~/work/lux/precompile/dex/events.go). The LuxSwapcarries a trailinguint24 fee— NOT the upstream Uniswap-v4 signature.DEXFill=keccak256("DEXFill(bytes32,address,uint256,uint256)"). If you change a signature in the precompile, recompute the hash here or it matches nothing. - Re-genesis self-heal rewinds the cursor only on a confirmed re-genesis (changed genesis-block hash) after a hysteresis streak — never on a transient low-head blip.
Deployment topology — TWO exchange-api images (do not conflate)
ghcr.io/luxfi/graph(this repo,Dockerfile→graphd) backs the explorer. Built by.github/workflows/docker.ymlonv*tags, self-hosted ARC pool.ghcr.io/luxfi/exchange-apihas TWO independent source/version lines:v2.2.x← repoluxfi/exchange-api(TypeScript,/v1/graphqlwith amm/dex routing +DEX_SUBGRAPH_URL). Deployed in lux-explorer. This is the markets API.v5.x← repoluxfi/exchangeDockerfile.api(server.js, REST/v1/tokens|pools|trades|price+/subgraph/*). Deployed in lux-ns. These are NOT the same codebase. A change to one does not reach the other's image.
Sibling repos
See the org-level LLM.md at /Users/a/work/lux/luxfi/LLM.md for the full inventory of sibling repos and inter-repo dependencies.