121 Commits
Author SHA1 Message Date
zeekayandHanzo Dev 1dec0e83f2 Give a chart something to draw: a day series over the trades already indexed
Every price chart on the site read "Missing chart data". Not because the
trades were missing — they were on disk, and the swaps query serves them —
but because tokenDayDatas, poolDayDatas and uniswapDayDatas each answered
[] from a hardcoded stub, and a client draws nothing under three points.

Underneath that was a clock problem. eth_getLogs says which block a log was
in and never when, so a swap's `timestamp` held the block NUMBER — trade
history dated every fill to January 1970, and a day series was not merely
empty but impossible. The poll now reads the header of each block it saw an
event in, and declines to advance past one it could not read: a block is
indexed when it has been read whole. Rows an older build wrote are dated
from the same headers, which is what lets the series cover the history
already indexed rather than starting the day it ships.

The series itself is a second fold over the trade stream the valuation pass
already builds — one read, one valuation, two questions. A pool's candle is
its own execution ratio, exact for every day. 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. Value locked is not in the trades at all, so
each day records it while it is today.

Recomputing the whole history every thirty seconds must not rewrite it, so a
day is written only when it moves: in the steady state that is one day per
subject that traded.

Filtering learned two things a chart needs. `where: {token: "0x…"}` names a
reference, and the row carries the token itself so the client can select
through it — so a filter now compares against the referenced id. And an
address arrives checksummed as often as lower-cased, so exact matches on text
ignore case.

pairDayDatas and the hour series stay stubs. Nothing asks for them.

Tests: 18 deliberate mutations — an off-by-one day boundary, high taking the
smallest price, the where filter dropped, the limit applied before the sort,
trades ordered by id, a log index read as decimal, a candle priced off its own
leg, and the call sites that write the series and read the block times removed
outright — each one turns a test red.

Co-authored-by: Hanzo Dev <[email protected]>
v1.2.17
2026-08-11 11:17:16 -07:00
zeekayandHanzo Dev 502f1496e9 a chain's factories come from its config, never from a default
One process indexes six chains, and the AMM factories were resolved by
falling back to Lux mainnet's when a Config left them blank. Every chain
that did not name its own therefore matched PoolCreated against Lux's
factory and denominated prices in WLUX. The zoo subgraph registered no
Zoo pool and quoted Lux's coin under Zoo's slug.

A default is only honest when one answer is right everywhere. 0x9999 is;
a factory address is not. FactoryV2, FactoryV3 and Native now come from
Config alone, so a chain that names none indexes none — the failure is
visible instead of wrong. The hardcoded per-network tables go with them:
they had no readers, and Zoo's entry had drifted to Lux's addresses.

ZUSD joins the stable anchors, so Zoo's book prices in dollars.

Co-authored-by: Hanzo Dev <[email protected]>
v1.2.16
2026-08-11 11:08:20 -07:00
zeekayandHanzo Dev 80a4098363 Let a page ask for as many tables as it needs
The Explore transactions view requests 23 tables in one query and got
"too many top-level fields (23), maximum 20" — the whole tab died on it.

20 was a bare literal at the check while every neighbouring limit lives on
Config. Asking for several tables at once is ordinary, not abusive: the cap
exists to bound work per request, so it belongs with the other limits and
starts high enough for real pages. A zero value means unset, not zero
allowed, so configs written before this field keep working.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-11 10:56:23 -07:00
zeekayandHanzo Dev cb4dc0c059 price every token on the wire, not just in the pass
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]>
v1.2.15
2026-08-10 20:53:24 -07:00
zeekayandHanzo Dev 3732f0045b count swaps: a pool, its tokens and the factory all bump on every swap
The indexer stored every swap via SeedSwap but nothing incremented txCount, so
each aggregate the explorer served read 0 — pools, tokens and all-time trades
alike. lux.exchange rendered a TXNS column of zeros beside real volume, and
"ALL-TIME TRADES" showed the factory's create count rather than trades.

bumpSwapCounts is the one place a swap is counted: pool, both tokens, factory.
All three handlers (V2/V3/V4) call it and nothing else counts swaps, so the four
aggregates cannot drift apart. An unregistered pool is a no-op — the handlers
already drop rogue emitters, and inventing an entity here would reopen the
phantom-pool bypass those gates exist to close.

Tests cover the increment, accumulation across swaps, and the unknown-pool no-op.
They are NOT verified on this machine: every test in this package, including
pre-existing ones, fails to link against two sqlite drivers (284 duplicate
symbols) under the local darwin toolchain. go vet and the package build are
clean; CI builds on golang-alpine, where the link succeeds.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-10 19:37:21 -07:00
zeekayandHanzo Dev f77f1a329e test(indexer): one DEXFill on the chain must reach the wire
The D-Chain cannot settle yet, so no DEXFill has ever been emitted on any
network and the dex pipe cannot be validated by waiting for data. The existing
tests cover the handler and the schema separately; nothing covered the seams
between them, which is where this pipe actually fails — a topic0 the log filter
never asks for, a field name the frontend does not read, an empty result that
encodes as null.

Drives the real poll loop against a JSON-RPC server serving exactly one DEXFill
at 0x9999, then asserts the frontend's own query
(`fills(first:25){ id market taker amountOut timestamp txHash }`) returns it
with those exact field names, and that it rolls into the market the page's
headline stats read. Asserts the poller actually filtered on the DEXFill topic0
rather than trusting that the stub served it, and asserts the pre-fill answer is
an empty list rather than null.

Co-authored-by: Hanzo Dev <[email protected]>
v1.2.14
2026-08-06 14:12:54 -07:00
zeekayandHanzo Dev 843678ae8b fix(storage): an empty list is [], not null
A store getter that answers with a list built it from `var result
[]interface{}` — a nil slice — so "nothing matched" marshalled to JSON
`null` instead of `[]`.

On a GraphQL wire those are opposite diagnoses. `{"data":{"markets":[]}}`
says "deployed, healthy, nothing has happened yet"; `{"data":{"markets":
null}}` is the signature of a subgraph that is absent, unsubscribed or
erroring behind a 200. The `dex` subgraph shipped the second while being
the first: mounted at /v1/graph/cchain/dex, subscribed to the 0x9999
DEXFill topic and caught up to the chain head, it reported itself dead on
every query purely because no fill had been emitted yet — leaving no way
to tell a quiet DEX from a broken pipe.

Six getters were affected (ListByType, GetTokens, GetFactories, GetPools,
GetPairs via GetPools, GetSwaps) plus the shared FilterResults tail. Every
other list getter in both backends already returned []interface{}{}; this
restores the convention rather than introducing one.

The regression test is reflective — it walks every (interface{}, error)
getter on *Store and fails any that answers with a nil slice — so a getter
added later cannot reintroduce the bug by being forgotten in a hand-written
list. 26 getters are covered.

Co-authored-by: Hanzo Dev <[email protected]>
v1.2.13
2026-08-06 14:03:37 -07:00
zeekayandHanzo Dev 3b21ff252e build: bump Go builder images to 1.26.5, set GOTOOLCHAIN=auto
Currency bump: golang builder images -> 1.26.5 (latest stable).

Adds ENV GOTOOLCHAIN=auto to each Go builder stage. The official golang
images set GOTOOLCHAIN=local, so a builder image older than the module's
go directive is a hard mid-build failure. The pin stays the hermetic fast
path; auto is the safety net for the next go.mod bump.

No go.mod go directive was changed. Builder images move; directives stay.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 16:26:22 -07:00
zooqueenandHanzo Dev fdb2455bcc ci: one CI path — .hanzo/workflows
The only thing that ever pushed ghcr.io/luxfi/graph was
.github/workflows/docker.yml on `runs-on: lux-build` — an ARC autoscaling
runner set that no longer exists. The cluster has no ARC CRDs, no ARC namespace
and no such pool, so every v* tag queued a job nothing could pick up. An
unclaimable job does not fail; it waits, and they accumulate.

It moves to .hanzo/workflows/release.yml on `lux-build-linux-amd64`, a label
the fleet actually carries. Two other things had to change or the job would
have failed on the first runner that did claim it:

  * ghcr login used the instance GITHUB_TOKEN. On this forge that is a Hanzo
    Git token and carries no rights at ghcr.io. It now uses GHCR_USER and
    GHCR_TOKEN, the org Actions secrets sourced from KMS that luxfi/node and
    luxfi/kms authenticate with.

  * the layer cache was `type=gha`, the GitHub-hosted Actions cache service.
    It is now the registry cache, which is backend-independent and lives
    beside the image it caches.

docker.yml was the whole of .github/workflows, so nothing is stranded. That
matters generally: the forge stops at the first workflow directory that
exists, so the moment a repo gains .hanzo/workflows its entire
.github/workflows suite goes dark at once — a migration has to land whole or
it silently deletes coverage.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-30 17:14:01 -07:00
zeekayandHanzo Dev f8beb04753 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:22 -07:00
zeekayandHanzo Dev 2c785b0eb0 rpc: /ext/ -> /v1/ in graphd + CLOB indexer endpoints
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]>
2026-07-27 17:58:17 -07:00
zeekayandHanzo Dev 84a4bdf91e perf(indexer): bound the swap history one valuation pass reads
The previous commit stopped the pass WRITING a row per swap. It still READ
every swap row in the table, every 30 seconds, into a map.

That is the same shape of mistake one layer down. A live chain's swap table
grows without limit — the Lux C-Chain's already spans blocks 53..1,096,496 in
a 732 MB store — so the cost of a pass grows with chain age forever, and the
allocation churn lands on a process sharing 4Gi with six other chains.

storage.SwapsRaw is replaced by RecentSwapsRaw(limit), which uses the indexed
timestamp column to take the newest `limit` rows in SQL rather than filtering
in Go. The pass takes the newest 50,000.

Where that bound binds, the volume figure is the volume of those 50,000 trades
and the log line says so outright. A partial sum silently presented as a total
is worse than a smaller number that is honest about its window.

Co-authored-by: Hanzo Dev <[email protected]>
v1.2.12
2026-07-26 18:42:27 -07:00
zeekayandHanzo Dev 01aa717953 fix(indexer): valuation must be O(pools) writes, and V3 amounts are signed
Deployed v1.2.10 to a six-chain explorer and watched one chain produce
aggregates while two others produced nothing at all — no result line, no error
line. Two distinct defects, both found by that silence.

1. The pass wrote O(swaps) rows per interval.

   valueSwaps rewrote every swap's own amountUSD, one serialized INSERT OR
   REPLACE each, every 30s. A chain with a real trade history never finished a
   pass, so it never wrote its pool/factory aggregates either — while a quiet
   chain beside it looked perfectly healthy. Volume is a per-pool rollup and
   belongs on the pool: valueSwaps now READS the swap rows and writes none.
   (Per-swap amountUSD stays "0". It is a separate gap, and the honest fix
   values a swap at the price of its OWN block, not at today's.)

2. A V3 Swap emits int256, and the handler read it unsigned.

   One leg of every V3 swap is negative — the token leaving the pool. Reading
   it unsigned persisted ~2^256 in place of a small negative number. Nothing
   had ever added those amounts up, so the corruption was invisible until the
   volume rollup landed and reported $4.07e72 of trading on Zoo.

   handleSwapV3 now uses decodeInt256, the same decoder the V4 handler already
   used for the same reason. Rows the old build already wrote are healed on
   read rather than by a re-index: a stored leg at or above 2^255 IS the
   two's-complement encoding of a negative int256, and the V2 handler's
   difference of two uint112 reserves can never reach that magnitude, so the
   boundary is unambiguous.

Both were invisible for the same reason, so the third change is observability:

  - Config.Label names the indexer ("cchain/amm") and every indexer log line
    now carries it. One process runs seven of these; a line that does not say
    whose it is cannot be acted on.
  - Every revalue exit logs, including "read no balances", and the result line
    reports valued/total pools and elapsed time. An indexer that reports
    nothing was indistinguishable from one that was never scheduled.
  - Each pass runs under a 60s budget, so a pass that cannot finish is
    cancelled and says so instead of wedging its chain forever.

Tests pin all three: the wrapped-negative leg valuing identically to the signed
one, handleSwapV3 persisting "-2500", and revalue leaving swap rows untouched.

Co-authored-by: Hanzo Dev <[email protected]>
v1.2.11
2026-07-26 18:38:23 -07:00
zeekayandHanzo Dev 1015359a85 feat(indexer): valuation pass — derive AMM TVL, prices and volume from chain state
Every USD field the exchange reads was an empty string. `pools`, `tokens` and
`factories` all resolved — with real addresses, real symbols, real fee tiers —
and then reported totalValueLockedUSD:"", volumeUSD:"", token0Price:"". The
landing page's TVL / 24h-volume / active-pool header had nothing to render, so
it rendered nothing.

Nothing was ever writing those fields. processLog's Sync/Mint/Burn arm was an
explicit no-op ("storage for these types not yet wired"), and bumpFactory's
comment deferred the USD aggregates to a "price oracle / day data" that does not
exist. Sync is the only event carrying V2 reserves, so with it inert there was
no path — at any block height — from the chain to a TVL number.

Fixed by adding the missing concern rather than by decoding one more event:

  indexer/valuation.go is the ONE owner of the derived USD aggregates. It reads
  balanceOf(pool) for both legs of every registered pool in a single JSON-RPC
  batch, anchors stablecoins at $1 by symbol, and relaxes prices outward through
  the pool graph — each unknown token taking its quote from the DEEPEST pool
  that already has a priced side, which is both deterministic and the least
  manipulable choice when pools disagree. A token no path connects to a stable
  stays unpriced and contributes nothing: unknown is reported as unknown.

Reserves are chain STATE, not chain HISTORY, so this is a pass beside the log
poller, not a handler inside it. That distinction is the whole point:

  - Sync exists only on V2. balanceOf answers the same question for V2, V3 and
    V4 identically.
  - An event-sourced TVL only converges by replaying all history, and a pool
    whose last Sync is a million blocks back reads empty until it trades again.
    On an idle chain it never trades again.
  - It is one fact, so it gets one writer. The Sync/Mint/Burn arm stays inert on
    purpose and now says why.

poolCount moves to the same pass for the same reason: it is derived from the
store, not accumulated by the create handlers, so it can no longer drift across
a restart, a re-index or a duplicate create. bumpFactory keeps the one field
that genuinely is a running total of events — txCount.

Fails closed: an unreachable RPC aborts the pass rather than overwriting good
aggregates with zeros.

Tests: valuation_test.go drives a stub RPC shaped like the live mainnet book —
a stablecoin pair anchoring a WLUX pair — and asserts derived TVL, spot prices,
per-token TVL, factory poolCount, the deeper-quote rule, non-clobbering of
fields the pass does not own, and the RPC-blip guard. Full suite green.

Co-authored-by: Hanzo Dev <[email protected]>
v1.2.10
2026-07-26 18:20:23 -07:00
zeekay 1e934c363d fix(indexer): bind clean BASE/QUOTE pair symbol + assetsBound on 0x9999 Initialize
handleInitializeV4 now sets the DEX Market symbol to the human BASE/QUOTE pair
(from the two currencies' ERC20 symbols) and marks assetsBound=true when both
resolve to a clean symbol. This is the exact shape the public exchange-api
real-asset gate (dexMarkets.ts isStructurallyReal) requires: without it every
native 0x9999 market was stripped (poolId-hex symbol failed the clean-pair check)
and the markets surface was empty despite real on-chain DEXFills. A token with no
identifiable symbol yields no pair -> the market stays unbound and hidden.

One place computes a market's display identity, read by every downstream surface
(explorer /dex, exchange-api gate, FE). New TDD: BindsCleanPairSymbol + UnnamedTokenStaysUnbound.
v1.2.9
2026-06-28 22:07:41 -07:00
zeekay 61ec197a34 fix(indexer): clob_get_* -> dex_get_* to match D-Chain read RPC (red H1)
The D-Chain read surface (luxfi/dex pkg/dchain/read.go:50-53) serves
dex_get_{markets,trades,orders,book}; the CLOB source still called the old
clob_get_* names -> 404 -> empty dex subgraph while real fills matched
on-chain (lux.exchange showed no markets though the chain was correct).

Proven: GOWORK=off go test ./indexer/ -run TestCLOB green.
v1.2.8
2026-06-25 15:52:07 -07:00
zeekay 9e363a5913 chore(deps): fix phantom go-sqlite3 v2.0.3 pin -> v1.14.47 + regen go.sum
go-sqlite3 v2.0.3+incompatible is a deleted upstream tag (unknown revision),
breaking every standalone graph build. Pin to v1.14.47 (latest v1.14 patch;
the cgo driver store_sqlite.go imports). Regenerate go.sum first-party hashes
stale from the org tag re-move (luxfi/age etc.).
2026-06-25 15:52:07 -07:00
zeekay 27d1b84235 feat(indexer): native D-Chain CLOB source for the dex subgraph
The dex subgraph indexer only consumed EVM 0x9999 settlement logs (the
cross-chain settlement view). On a native-DEX deployment the trade engine
IS the D-Chain (dexvm): a trade is a consensus state transition matched at
Block.Verify, not an EVM event, so native fills never appear on the EVM RPC
and the dex subgraph stays empty (markets:null).

Add CLOBSource (indexer/clob.go): an orthogonal, READ-ONLY poller of the
D-Chain committed-state read RPC clob_get_{markets,trades,orders} that writes
the same Market/Fill/Order entities the existing dex resolvers serve. Enabled
by Config.DexRPC; empty preserves EVM-only behaviour. The two sources share
only the store and the entity shapes, so this is purely additive — the amm
(EVM) subgraph is untouched.

- markets <- clob_get_markets (per-market authoritative book summary; bound-
  market hex symbol decoded to its human pair, e.g. LUX/LUSD)
- fills   <- clob_get_trades?since=<h> (global height-ordered log; keyed
  height:seq -> idempotent; incremental by accepted height)
- orders  <- clob_get_orders?market=<id> (resting book per market)

Tests (httptest doubles serving the exact read.go JSON shapes captured live
from lux-devnet): markets/trades/orders ingest + symbol decode, incremental
cursor + idempotent re-read, and EVM-AMM no-regression with DexRPC set.
v1.2.7
2026-06-22 15:48:53 -07:00
zeekay 8c24d384da feat(indexer): enrich AMM token symbol/name/decimals from ERC20
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.
v1.2.6
2026-06-21 23:52:42 -07:00
zeekay 705e38c45e fix(indexer): gate V2/V3 AMM handlers to canonical factory; floor parseLimit everywhere
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.
v1.2.5
2026-06-21 20:27:48 -07:00
zeekay c2bd4165e6 fix(indexer): scope V4 Initialize/Swap to 0x9999, guard poller, cap DEX first
Three confirmed findings in the explorer/graph indexer:

- MEDIUM: handleInitializeV4/handleSwapV4 seeded AMM pools/tokens/swaps from
  ANY emitting contract, so any contract could emit lookalike V4 events to
  create phantom pools/tokens and inflate the explorer's public TVL/volume
  aggregates. Gate both to the canonical 0x9999 PoolManager (idx.isPoolManager),
  mirroring handleDEXFill. The now-always-true inner Market guard in
  handleInitializeV4 is de-nested (one path).

- LOW (DoS): unguarded fixed-offset slices (token0[:8], l.Address[:8]) panic on
  a short/malformed RPC log, and the poll goroutine had no recover() -> one bad
  log crashed graphd. Route those symbol slices through the length-checked
  shortAddr helper, and wrap each poll in pollGuarded() which recovers any panic
  into an error so the loop continues.

- LOW (query-DoS): resolvers/dex parseLimit had no max cap. Clamp <1 -> 100 and
  cap min(limit, 1000), matching every other resolver namespace.

Tests:
- TestHandleV4_RejectsNonPoolManager: rogue Initialize/Swap seeds nothing; the
  same event from 0x9999 is honored.
- TestPoll_MalformedLogDoesNotCrash: malformed logs don't panic processLog, and
  pollGuarded converts a poll panic into an error.
- TestParseLimit_Caps: default 100, floor 1->100, hard cap 1000.
2026-06-21 18:25:55 -07:00
zeekay 600ee3dca3 ci: route docker.yml to lux-build ARC + drop QEMU arm64 (amd64-only)
The classic [self-hosted, linux, amd64] triple is serviced by fleet arcd hosts
that are not reliably available (v1.2.3 queued 30+ min). Use the in-cluster
lux-build autoscalingrunnerset (same as node + exchange-api). amd64-only — QEMU
arm64 pushed builds to ~2h. Artifact identical to v1.2.3; CI-plumbing only.
v1.2.4
2026-06-21 15:01:20 -07:00
zeekay 223316ec3b docs(LLM.md): native 0x9999 DEX indexing + two-exchange-api-image topology 2026-06-21 14:34:02 -07:00
zeekay b87f7805bb Merge fix/native-dex-indexing: native 0x9999 V4/DEXFill indexing + dex/amm subgraphs + re-genesis self-heal v1.2.3 2026-06-21 14:29:34 -07:00
zeekay d24bd62198 graph/indexer: gate re-genesis rewind on a deterministic genesis-hash change
Item 4: the 3-consecutive-poll hysteresis alone could be satisfied by a deeply-stale
load-balanced RPC backend held >reorgDepth behind for the full window (~6s), wiping a
still-canonical chain and re-indexing from genesis (DoS amplification).

Keep the streak as a CHEAP PRE-FILTER, but gate the actual rewind on a deterministic
signal: the chain's genesis (block 0) hash. Every node serves block 0 regardless of
how far behind its head is, so an unchanged genesis hash deterministically rules out
a re-genesis even on a deeply-lagging backend (unlike a cursor by-number/by-hash probe,
which a backend whose head is below the cursor cannot serve and would false-trip).
Honors the no-eth_getCode rule (only eth_getBlockByNumber 0x0 header is read). The
probe is a struct field (genesisHashFn) so tests drive canonicality without a live RPC;
baseline captured lazily before any reset decision. Fail-closed: a probe error or
missing baseline never wipes.

3 existing regenesis tests updated to the new signature/semantics + 3 new
(deeply-behind-but-canonical => NO wipe; changed genesis => wipe; probe-fails => NO
wipe). Full indexer+engine suites green.
2026-06-21 13:51:33 -07:00
zeekay 305636ac07 graph: re-genesis hysteresis + preserve factory TVL/volume + rich-market merge
Red MEDIUM (FIX C): maybeResetForRegenesis rewound to StartBlock on the FIRST poll
where head << cursor, so an idle chain (eth_blockNumber=0x0, 'Building block') or a
briefly-lagging RPC backend triggered a full re-index from genesis (DoS amplification).
Add hysteresis: require the low head to persist across regenesisConfirmations
consecutive polls; a recovered/normal head clears the streak. A genuine relaunch holds
low for many polls; a transient dip recovers in one or two.

Red MEDIUM: bumpFactory wrote SeedFactoryData with empty TVL/volume, and SeedFactory is
INSERT OR REPLACE, so every pool-create/swap clobbered totalVolumeUSD/totalValueLockedUSD
(emptying the landing-page header). Carry both USD aggregates through the
read-modify-write (asString helper).

Red LOW: handleInitializeV4 now MERGES with any volume stub a prior fill accumulated
(don't reset volume24h/tradeCount to 0 when enriching with the token pair/fee).
decodeUint256 guards the SetString ok bool (malformed hex -> clean 0, not a half-parsed
amount).

Tests: regenesis requires sustained low head; idle-low-head-that-recovers does NOT wipe;
shallow reorg no reset; Initialize builds a rich Market (pair+fee), merges stub volume;
bumpFactory preserves TVL/volume; decodeInt256 signed. New engine end-to-end: the dex
schema resolves the FE markets/fills query shapes against indexed entities, amm does NOT
(the exchange-api routing rationale, mirroring Red's live finding). Full graph suite
green (CGO_ENABLED=1).
2026-06-21 13:15:52 -07:00
zeekay ed9e132af8 fix(dex): wire native 0x9999 V4/DEXFill indexing + uniswapFactories alias + re-genesis self-heal
- indexer: handleSwapV4 (AMM swap/pool), handleInitializeV4 (pool/Market/factory),
  handleDEXFill (sole CLOB Fill source, scoped to 0x9999), bumpFactory, signed int128 decode
- indexer: poll() re-genesis self-heal (head<<cursor by >reorgDepth => rewind to StartBlock)
- indexer: correct SigInitializeV4/SigModifyLiquidity hashes; add SigDEXFill + LXSettleAddress
- engine: uniswapFactory(ies) alias resolvers (single-source schema-delta fix)
- tests: dex_v4_test (V4/Fill/regenesis/scoping), e2e dex_native_test (dex schema + alias)
- fix pre-existing e2e/wasm_test.go missing os import
2026-06-21 12:26:11 -07:00
zooqueenandHanzo Dev 5783d1167f docs: tidy LLM.md indexes; CLAUDE.md -> LLM.md symlink convention
Co-authored-by: Hanzo Dev <[email protected]>
2026-06-17 09:54:03 -07:00
zeekay 3e916c59b5 corona→corona: academic Corona now only in lp-220-p3q-corona 2026-06-11 10:28:03 -07:00
zeekay 920dea4d2d scrub Liquidity branding + corona→corona (OSS brand hygiene) 2026-06-10 23:36:13 -07:00
zeekay f16ffb8082 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:22:19 -07:00
zeekay 26463e3afd deps: bump Go to 1.26.4 across go.mod, Dockerfiles, GH Actions
Workspace-wide sync. luxfi/node already shipped on 1.26.4 in v1.30.6
(commit 121aca1fa9); this is the cross-repo catch-up.
2026-06-07 10:45:10 -07:00
zeekay 1c5bbea6f4 go: 1.26.3 → 1.26.4 (security: crypto/x509, mime, net/textproto) 2026-06-06 22:09:24 -07:00
zeekay a211263b30 WIP: session checkpoint 2026-06-05 (codec rip in flight) 2026-06-05 15:33:54 -07:00
zeekay f04061d5fa fix: gofmt -s across repo (CI format check) 2026-06-02 11:38:29 -07:00
zeekay 21683d2393 merge: feat-ql-alias 2026-06-01 16:34:48 -07:00
zooqueenandHanzo Dev 050748ea4e Add agent project docs (CLAUDE.md, LLM.md)
Co-authored-by: Hanzo Dev <[email protected]>
2026-05-31 21:57:56 -07:00
zeekay a146c834aa chore: brand-neutral cleanup — remove cross-tenant references 2026-05-25 15:13:54 -07:00
zeekay 738544d4da drop hardcoded /Users/z paths — use $HOME-relative 2026-05-21 15:01:37 -07:00
zeekay 65bf5c2f83 ci: switch workflows to lux-build (luxfi-scoped ARC pool)
Cross-org runs-on dispatch failure: hanzo-build-linux-amd64 scale set is
registered to github.com/hanzoai and silently rejects luxfi/* jobs.
Switch to lux-build (githubConfigUrl=github.com/luxfi, max 20).
2026-05-19 13:21:06 -07:00
zeekay 1270e76233 ci: fix runner label (hanzo-build pool)
Imaginary runner label `lux-build-linux-amd64` does not exist. The live
amd64 pool is `hanzo-build-linux-amd64`.

- docker.yml: docker job
2026-05-19 08:03:38 -07:00
zeekayandGitHub 449a3c0478 chore: bump Go toolchain to 1.26.3 (#4)
Pin Go version to 1.26.3 across go.mod, CI workflows, and Dockerfiles
for canonical alignment with the rest of the luxfi/* stack.
2026-05-16 16:43:20 -07:00
zeekayandGitHub c8e3c07730 chore: add LICENSE (Lux Ecosystem License v1.2) + LICENSING.md pointer (#3)
* chore: add Lux Ecosystem License v1.2

* docs: add LICENSING.md pointer to canonical IP/licensing strategy
2026-05-16 10:47:36 -07:00
zeekay e5143ccf54 go.mod: bump go directive to 1.26.3 (security advisory) 2026-05-12 21:29:51 -07:00
zeekay 007cb9d837 ci(docker): go build -mod=mod populates go.sum on the fly 2026-05-05 21:53:54 -07:00
zeekay b821f244b2 ci: drop ci.yml — Docker workflow handles cgo build via alpine 2026-05-05 21:51:17 -07:00
zeekay 19a7c28927 ci: ARC has no_new_privs (no sudo); copy . . before mod download 2026-05-05 21:44:09 -07:00
zeekay ce9276fce5 ci: regenerate go.sum at build time (proxy cache inconsistency) 2026-05-05 21:34:15 -07:00
zeekay d98d95c40b ci: refresh go.sum (proxy hash drift), install gcc on ARC runner 2026-05-05 21:13:22 -07:00
zeekay 4e30001132 ci: align with luxfi org's actual ARC pool (lux-build-linux-amd64) 2026-05-05 21:04:27 -07:00