Commit Graph
131 Commits
Author SHA1 Message Date
zeekay 62a5d63bf6 Name Zen in the hero card's font stack
The social card asked for Inter first. That is not a face this estate
licenses, ships or ever drew, so a renderer with Inter installed painted
the wrong face and one without it silently fell through to system-ui.
Zen leads now; the same tail follows it.
2026-08-28 11:41:46 -07:00
zeekay 0d4a268c3a style(docs): swap the geist-named font tokens for Zen 2026-08-23 15:02:00 -07:00
zeekayandHanzo Dev 984494b3a8 Take the sqlite C under the name nothing else claims
The wrapper this reached for registers the driver as "sqlite", and so does
modernc.org/sqlite, which arrives here with hanzoai/replicate. Two packages
under one name is a panic in init, before main runs.

csqlite is the same house C under "sqlite3" — the name every sql.Open here
already names, and one nothing else in this graph takes.

Co-authored-by: Hanzo Dev <[email protected]>
v1.5.6
2026-08-13 15:12:22 -07:00
zeekayandHanzo Dev 6920479c85 One sqlite in the binary
Two packages compiled the same amalgamation: upstream mattn/go-sqlite3 here,
and hanzoai/csqlite by way of hanzoai/sqlite, which luxfi/graph moved to. A
binary linking both — the explorer links all three of us — defines every
sqlite3_* symbol and every cgo trampoline twice and fails at link. It fails on
darwin; linux is laxer about duplicates, which is the only reason a build ever
got through.

hanzoai/sqlite is the house driver and csqlite registers both "sqlite" and
"sqlite3", so nothing but the import line changes: every sql.Open here still
names the driver it always named.

Co-authored-by: Hanzo Dev <[email protected]>
v1.5.5
2026-08-13 15:08:06 -07:00
zeekayandHanzo Dev fe9cf8d4f7 evm: notice when the chain under us has been rebuilt from genesis
The zoo chain was relaunched and its index never followed. It is serving
24,607 blocks of a chain that no longer exists: at height 5000 the index
holds 0xd798ca91 where the chain now has 0xa6c56d04, and its newest block
is dated two days ago while the live chain sits at 9,564.

indexNewBlocks resumes from MAX(number)+1 and loops while that is <= the
head. A chain rebuilt from genesis reports a head far below everything
already stored, so the resume point lands past it, the loop body never
runs, and the cursor cannot move again. There is no recovery from this by
waiting: the further the new chain gets, the longer it stays behind a
cursor left on the old one.

Deciding this on height alone would be wrong in the other direction. One
backend of a load-balanced set lagging its peers reports the same shape as
a relaunch, and erasing a chain because a node was slow is worse than
being stale. So the height is only used to decide when to ask a question
that has a real answer: block 0's hash. Every node knows which chain it is
on however far behind it is, so a changed genesis means a different chain
and an unchanged one settles it. The streak before the probe keeps the
common case free of extra RPCs.

Each chain is indexed into its own database, so forgetting one reaches
nothing else. The hash is recorded on the first poll that can reach the
chain, because a relaunch cannot be recognised without knowing what came
before it, and an unreadable or unrecorded genesis is not evidence of
anything, so both keep the index.

The graph indexer has resolved this for its own cursor since v1.2.x. This
gives the block indexer the same footing.

Co-authored-by: Hanzo Dev <[email protected]>
v1.5.4
2026-08-11 12:46:30 -07:00
zeekayandHanzo Dev 7c85b79a62 explorer: name the block's transaction count what the SPA reads
Block rows and block details rendered the Txn label with nothing after it.
Lux block 1106999 has 4 transactions by RPC and the page showed no number.

formatBlock emitted the column's own name, tx_count. The SPA's Block type
declares transactions_count and every render site reads it — BlocksTableItem,
BlocksListItem, LatestBlocksItem, BlockDetails — so the value arrived under a
key nobody looked up and each cell fell back to undefined. Rows that arrived
over the realtime channel DID show a count, which is what made this look like a
data problem rather than a naming one: that path already renames the field
(lib/socket/sse.ts toApiBlock), so the same block read correctly when pushed
and blank when fetched.

Renamed rather than aliased: nothing reads a block's tx_count, so a second
spelling would only be a second thing to keep in step. formatPlatformBlock gets
the same name — P-Chain rows go through the same block widgets.

The suite asserted the old key by PRESENCE, in three places, which is why a
rename is what it takes to notice. TestBlockFieldValues now pins the value:
want 3, got <nil> on the old name.

Co-authored-by: Hanzo Dev <[email protected]>
v1.5.3
2026-08-08 06:17:16 -07:00
zeekayandHanzo Dev d8b8f8fa23 explorer: fold address case at the lookup boundary, and emit the token contract the SPA reads
Two defects that both fail silently, which is worse than erroring.

Every wallet emits EIP-55 checksummed addresses. The hex pattern accepts
mixed case, so a checksummed address passed validation and then matched no
row — the route answered 404, and the page rendered WLUX, a contract holding
149M LUX, as an empty EOA. The stored hash is lowercase, so the two spellings
have to fold to one key. That fold now happens once, in j()/csvHandler(),
which every route already passes through; handlers keep doing lookups and
stay in their lane. Only {hash} and {addr} fold — {id}, {pair}, {prefix} and
{item} are not hex and are case-significant.

formatToken emitted address/holders/trust_score while the SPA's TokenInfo
(luxfi/explore types/api/token.ts) reads address_hash/holders_count, so
TokensTable keyed every row on undefined and the page crashed. Renamed to
the contract rather than shimming the client, and fixed the second producer
of the same object in formatTokenTransfer, which had drifted to its own
spelling. trust_score had no consumer anywhere: TokenReputation is
'ok' | 'scam', a fraud flag, not the 0-100 completeness heuristic that
computed it — deriving one from the other would have called any token
lacking an icon a scam. reputation is null until there is a real oracle, and
the dead scorer is gone. Its toBool/toInt helpers were generic coercion
braided into a scoring file; they move to convert.go with their tests.

Co-authored-by: Hanzo Dev <[email protected]>
v1.5.2
2026-08-07 09:00:19 -07:00
zeekayandHanzo Dev a6fa1e8268 Merge remote-tracking branch 'origin/main'
Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 18:37:52 -07:00
zeekayandHanzo Dev f4d8b7a114 rpc: finish the /v1/ sweep, and stop sending Avalanche to /v1/
luxd serves exactly one HTTP prefix -- server/http/server.go:33 sets
baseURL = "/v1" and every route derives from it -- so /ext/* 404s on any
luxd. Avalanche is the mirror image: api.avax.network really does serve
/ext/bc/C/rpc, and pointing it at /v1/ breaks it.

31a1ee7 converted README.md only. This finishes the job and corrects the
overshoot in the same pass:

  config/chains.yaml     sparklepony, testnet.sparklepony, pars,
                         testnet.pars  ->  /v1/   (luxd hosts)
                         avax stays on /ext/
  chains.example.yaml    avax stays on /ext/
  docs/configuration.mdx LUX_{C,P,X}_RPC and the c/p/x_chain block, all
                         127.0.0.1:9630 and :9650 luxd  ->  /v1/
  docs/index.mdx         localhost:9650 and api.lux.network  ->  /v1/

Measured, positive and negative control per host:
  api.lux.network/v1/bc/C/rpc  -> 200 0x17871    /ext -> 404
  api.pars.network/v1/bc/C/rpc -> 200 0x78d65    /ext -> 404
  api.lux-test.network/v1/...  -> 200 0x17870    /ext -> 404

evm/transport/transport_test.go is deliberately left at HEAD. Its
isLuxdEndpoint gate is literally strings.Contains(endpoint, "/v1/"), so
the table is /v1/ throughout by design.

go test ./evm/transport/... ok 15.429s. Both YAML files parse. This repo
does NOT build before this change -- cmd/indexerd fails at link with
duplicate sqlite3 symbols, verified identical with these changes stashed
-- so nothing here is tagged.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-06 18:35:56 -07:00
zeekayandHanzo Dev 31a1ee7d6e rpc: /ext/ -> /v1/ on our own hosts
luxd serves /v1/*; /ext/* 404s. Third-party chains keep their own paths.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-03 21:02:00 -07:00
zeekayandHanzo Dev f816a9e7fa 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:31 -07:00
zooqueenandHanzo Dev de500be239 ci: one CI path — .hanzo/workflows
Both workflows here named labels no runner on this forge carries. docker.yml
used the bare `[self-hosted, linux, amd64]` triple and docs.yml the same; the
pool is named `lux-build-linux-amd64`, nothing answers to the triple, and a job
naming a label nothing carries does not fail — it waits. Note that
.github/actionlint.yaml in this repo has registered `lux-build-linux-amd64` as
the self-hosted label for some time; the workflows had simply never followed it.

The image build moves to .hanzo/workflows/release.yml. Two further things had
to change or it would have failed on the first runner that claimed it:

  * ghcr login used the instance GITHUB_TOKEN, which on this forge is a Hanzo
    Git token with no rights at ghcr.io. It now uses GHCR_USER and GHCR_TOKEN,
    the org Actions secrets sourced from KMS.

  * the layer cache was `type=gha`, the GitHub-hosted Actions cache service,
    now the backend-independent registry cache.

Dropped with it: the linux/arm64 half of the image. It was produced by QEMU
emulation on an amd64 runner, which is hours rather than minutes. This image is
amd64-only until we own an arm64 runner and it can return as a native matrix
entry.

docs.yml's build survives as .hanzo/workflows/docs.yml; its publish does not.
configure-pages, upload-pages-artifact and deploy-pages all speak to the GitHub
Pages API, which this forge does not have. Building still fails on the commit
that breaks the docs, and it stays path-filtered to docs/**, but the site is no
longer published from CI — that returns when it is served from the PaaS.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-30 17:14:21 -07:00
zeekayandHanzo Dev e9498c93fb 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:27 -07:00
zeekayandHanzo Dev 5ce7dca138 fix(explorer): a zero that means "unavailable" is a lie
Every figure below was rendered as 0 on explore.lux.network while the
chain said otherwise. None of them was missing data — the queries asked
for columns that are not there, or read columns nothing writes.

  /addresses answered {"items":[]} while /stats reported 72 addresses:
  the ORDER BY named `transactions_count`, which is the Blockscout-legacy
  spelling; luxfi/indexer's evm_addresses spells it `tx_count`. SQLite
  raised "no such column", the handler swallowed it into an empty page,
  and it stayed empty forever. Same guess broke evm_tokens' address
  column. detectColumn now asks the database instead.

  Token "Holders 0" sat directly above a populated holder list:
  evm_tokens.holder_count is never written by the indexer. Holders and
  transfers are now counted from evm_token_balances / evm_token_transfers
  in the same statement that selects the token — one query, not N+1 — and
  the token list is ranked by that count instead of by a column of zeros.

  Address balance read 0 for every address, WLUX's 159,126,795.518 LUX
  included: evm_addresses.balance is written as the literal '0' on first
  sight and never updated. Current state belongs to the chain, not to an
  index of it, so balances now come from the node — one batched
  eth_getBalance per page. No node configured, or no answer, means the
  balance is unavailable, and unavailable is null.

  "Burnt fees 100%" on every block: burnt_fees was gas_used x base_fee,
  which is the Ethereum EIP-1559 burn. Lux does not burn it. creditTxFee
  credits the whole fee to the coinbase (0x0100..0000, no code, no key)
  unless the fee split is active, and it has never fired on mainnet.

  average_block_time was structurally 0 — subtracting two TEXT datetimes
  in SQLite yields 0 — and gas_used_today compared those TEXT timestamps
  lexically against datetime('now'), matching nothing. Both go through
  strftime('%s') now, and transactions_today is counted rather than
  hardcoded nil.

Co-authored-by: Hanzo Dev <[email protected]>
v1.4.7
2026-07-27 14:25:21 -07:00
zeekay 69cc8f9adf chore: sync working tree
Commits 1 outstanding change(s) that were sitting uncommitted.
No build artifacts and no secrets in the changeset (both checked).
2026-07-26 10:13:40 -07:00
zeekay 0b0a861363 fix(deps): reconcile go.sum against what the registry actually serves
Chasing these one at a time was whack-a-mole — every repair surfaced the next
moved tag (age, pq, threshold, zap, consensus, constants, sdk, keys…). This is
the systematic pass instead: every distinct luxfi/hanzoai module@version in the
workspace (692 of them) was resolved against proxy.golang.org + a direct fetch,
and any go.sum line disagreeing with what is actually served was corrected.

Not an attack, and checked rather than assumed: for each case the proxy and a
direct fetch AGREE with each other and differ from the recorded hash. Two
independent transports agreeing means nothing is rewriting bytes in flight —
the tags moved at source. sum.golang.org still holds the original, but
GOPRIVATE covers github.com/luxfi/* with GOSUMDB=off, so our own modules never
consult the checksum DB and a moved tag splits consumers silently instead of
failing at publish time.

Only the recorded hash changes. No selected version moves, and `go mod tidy`
was deliberately NOT run, so nothing is upgraded as a side effect.

Verified: `go list -m all` resolves with no checksum error.

The durable fix is upstream: treat a published version as immutable. Cut
x.y.z+1 instead of moving a tag — with GOSUMDB off, no consumer can detect it.
2026-07-26 04:16:27 -07:00
zeekay 01d674241a fix(deps): correct moved-tag hashes for luxfi modules in go.sum
Several luxfi versions carry TWO different contents across this workspace,
because published tags were moved instead of a new patch being cut:
[email protected], [email protected], [email protected], [email protected], [email protected].

Adjudicated before editing, since "checksum mismatch / SECURITY ERROR" is also
what a real supply-chain attack looks like. It is not one here: sum.golang.org
holds the OLD hash while proxy.golang.org and a direct fetch BOTH serve the same
NEW bytes. Two independent transports agreeing means nothing is rewriting
content in flight — the tag moved at source. The sumdb entry is a fossil:
GOPRIVATE covers github.com/luxfi/* with GOSUMDB=off, so our own modules never
consult the checksum DB and a moved tag splits consumers silently.

The old bytes are served by nothing now, so a stale pin can never build.
Corrected to the only content that exists, in BOTH line forms (h1: and
/go.mod h1:) — Go reports these one at a time, so a partial fix just relocates
the error.

Deliberately no `go mod tidy`: this changes no selected version, only the
recorded hash of versions already chosen.

Verified: `go list -m all` resolves with no checksum error.

The durable fix is upstream: never move a published tag, cut x.y.z+1 instead.
2026-07-26 02:42:49 -07:00
zeekayandHanzo Dev 755319c47c feat(platform): wire P-Chain block + validator indexing (additive)
Standalone explorer API now serves a P-Chain variant when a pchain_blocks
table is present: GET /blocks, /blocks/{id}, /main-page/blocks read the
linear block table, and a new GET /validators returns the synced validator
set (weight/stake, start/end time, uptime, delegation fee, BLS signer
pubkey+PoP, potential reward, connected, per-node delegator_count).

Adapter fixes required for the default SQLite query backend:
- SyncValidators no longer uses Postgres-only to_timestamp(); timestamps are
  bound as time.Time, which both the SQLite and Postgres drivers accept for a
  TIMESTAMP column (regression covered by a real RPC->SQLite test).
- Capture the validator BLS signer (publicKey + proofOfPossession); new
  bls_public_key / bls_proof_of_possession columns on pchain_validators.
- Fall back to weight when the RPC omits the deprecated stakeAmount.

EVM path untouched: block/main-page routes branch on the platform flag; all
other routes are unchanged.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-08 07:12:16 -07:00
zeekayandHanzo Dev e616aa286e chore: migrate luxd HTTP routes /ext -> /v1
Drop the Avalanche-heritage /ext prefix; /v1 is the single canonical route
surface (one way, no backward compat). The node's baseURL is the source of
truth; clients, SDKs, CLI, indexer, maker, genesis, netrunner, and the
k8s/compose/gateway/explorer configs are updated to match.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-01 11:40:21 -07:00
zeekayandHanzo Dev 8d5a0db0eb fix(evm/defi): rename duplicate TestEmptyLogs to subject-scoped names
Two distinct tests (ParseGovernanceEvents vs ParseTeleportEvents empty-log
handling) collided on the name TestEmptyLogs, breaking compilation of the
evm/defi test package. Rename to TestGovernanceEmptyLogs / TestTeleportEmptyLogs
— no coverage change, package compiles + both pass.

Co-authored-by: Hanzo Dev <[email protected]>
v1.5.1
2026-06-30 16:01:01 -07:00
zeekayandHanzo Dev 984a761b64 fix(deps): repair vanished pins / go.sum drift ( core-bump@latest [email protected]) + build green
Co-authored-by: Hanzo Dev <[email protected]>
v1.5.0
2026-06-30 14:11:06 -07:00
zeekay 2303cb398a Merge branch 'bump/luxdb-v1.19.3' 2026-06-30 14:10:29 -07:00
zeekay bd887f0e2a docs(brand): add hero banner 2026-06-28 20:37:40 -07:00
zeekay 535a3f89dd chore(brand): dynamic hero banner 2026-06-28 20:37:39 -07:00
zeekayandGitHub 637b5c65db chore: bump luxfi/database v1.19.3 (#21)
Co-authored-by: zeekay <[email protected]>
2026-06-25 15:24:11 -07:00
zeekay d909d7f168 chore: bump luxfi/database v1.19.3 2026-06-25 13:06:36 -07:00
zooqueen 5f7a20b00c fix(explorer): real tx fee + block burnt_fees/rewards; value as decimal string (fixes SPA NaN/null on fee-less txs) v1.4.6 2026-06-23 14:41:27 -07:00
zeekay a534262058 scrub branding + corona→corona (OSS brand hygiene) v1.4.5 2026-06-10 23:41:10 -07:00
zeekay 22723298b0 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:29 -07:00
zeekay a84761fa68 chore: replace default coin symbol with LUX in explorer SPA
The 4 explorer page components hard-coded '' as the fallback
when VITE_COIN is unset. Per workspace rule the lux tree must not
default to the  token symbol — switch the fallback to LUX
(the actual Lux Network coin).
2026-06-10 12:19:33 -07:00
zeekay 41b923eb60 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:46:12 -07:00
zeekay 6967813390 go: 1.26.3 → 1.26.4 (security: crypto/x509, mime, net/textproto) 2026-06-06 22:09:29 -07:00
zeekay ea02705ced WIP: session checkpoint 2026-06-05 (codec rip in flight) 2026-06-05 15:33:55 -07:00
zeekay bda50ca355 fix: gofmt -s across repo (CI format check) 2026-06-02 11:40:11 -07:00
zeekay f9f573e4e4 chore(deps): go get -u ./...
Updated dependencies to latest minor versions via go get -u + go mod tidy.
2026-06-02 11:21:43 -07:00
zeekay 4c1d22ed57 chore: brand-neutral cleanup — remove cross-tenant references 2026-05-26 11:42:03 -07:00
zeekay a21cf1b615 docs: scrub "subnet" language → L2 EVM / VMID terminology (Hanzo policy 2026-05-25) 2026-05-25 16:23:41 -07:00
zeekay 2457a33d9e chore: brand-neutral cleanup — remove cross-tenant references 2026-05-25 15:13:53 -07:00
Darkhorse7starsandGitHub d7152bbe13 evm: composite PK on evm_token_balances (hotfix v1.4.20)
Mark PK columns Primary.
2026-05-24 18:59:41 -07:00
Darkhorse7starsandGitHub 32533b5c84 evm + explorer: server-side per-(token, holder) balance state
evm_token_balances now maintained on every Transfer event.
2026-05-24 18:53:38 -07:00
Darkhorse7starsandGitHub 00bca2bb7a explorer: alias log + token-transfer column names
Try modern names first, fall back to legacy.
2026-05-24 18:43:49 -07:00
Darkhorse7starsandGitHub 4dab4b4bb8 evm/adapter: capture tx.input via second eth_getTransactionByHash hop
Best-effort second hop.
2026-05-24 18:17:37 -07:00
Darkhorse7starsandGitHub 8b74fec760 explorer: return raw input field on tx detail response (#16)
1-line change to formatTx — exposes tx.input as 0x-hex so SPA can decode against ABI.
2026-05-24 18:12:00 -07:00
Darkhorse7starsandGitHub 413ed03355 explorer: search by token name/symbol substring (#15)
Adds tokens LIKE fallback to /search.
2026-05-24 18:02:11 -07:00
Darkhorse7starsandGitHub ffe4328379 explorer: return secondary_sources + file_path + verified_via (#14)
Three fields already accepted + persisted by verify endpoint, now wired into the GET response shape. Unblocks multi-file source rendering.
2026-05-24 17:40:43 -07:00
Darkhorse7starsandGitHub 3eb7788893 explorer: implement standalone verifyContract endpoint (replace no-op stub)
Real verifier + table-init + RW DB open. Replaces the previous no-op stub that left evm_smart_contracts empty on every chain. See PR for the JSON submission shape.
2026-05-24 17:05:53 -07:00
f6ad1e62f8 explorer: SPA-compat fixes + log/token-transfer indexing (#12)
Six fixes for the  Blockscout-derived SPA against the luxfi/indexer + luxfi/explorer backend, batched together as one squash.

* `formatAddress` aliases `tx_count`⇆`transactions_count` and `balance`⇆`coin_balance` so the SPA's `i.tx_count.toLocaleString()` and `Number(i.balance)/1e18` stop crashing.
* `/smart-contracts/{addr}` returns 200 `{is_verified:false}` on no-row instead of 404 — the SPA fetches that endpoint unconditionally on every address page, including EOAs, and the 404 was console noise.
* `evm/indexer.go` now indexes the `receipt.Logs` slice into `evm_logs`, detects ERC-20 (3-topic) and ERC-721 (4-topic) Transfer events, writes `evm_token_transfers`, and best-effort resolves token metadata (`name`/`symbol`/`decimals`/`totalSupply` via `eth_call`) into `evm_tokens`. Previously these tables stayed empty.
* `formatToken` + `formatTokenTransfer` fall through both column-spelling variants — luxfi schema (`address`, `token_type`, `tx_hash`, `value`) and Blockscout-legacy (`contract_address`, `type`, `transaction_hash`, `amount`).
* `fmtNum` detects `0x`-prefixed strings (Transfer event `data`, RPC `gas_price`/`value`/`base_fee`) and converts to decimal via `math/big`. Pure decimal strings pass through unchanged.
* `getToken` + `tokenCounters` use a per-schema `tokenAddrCol` (`address` for evm_*, `contract_address` for Blockscout-legacy) instead of hard-coding `WHERE contract_addr = ?` — fixes `/v1/indexer/tokens/{addr}` 404 on the luxfi schema.

Verified live against devnet 8675312 (anvil) with 10010 blocks indexed: 9 tokens, 33+ transfers, address-detail + token-detail pages render.

Co-authored-by: Darkhorse7stars <[email protected]>
2026-05-24 09:06:04 -07:00
Darkhorse7starsandGitHub 826db07e81 explorer: read is_contract directly from the column instead of deriving (#11)
formatAddress was inferring is_contract from `length(code) > 0`, but
the SQLite address schema in evm/indexer.go writes `code` as the
default empty string and tracks contract status in a separate
boolean `is_contract` column. Net result: every address — including
real deployed contracts — returned is_contract: false in the API,
because empty code → derived isContract=false.

Switch to reading the boolean column directly. Fall back to the
code-length heuristic only when the boolean is absent (legacy
Postgres shape).

Verified against live devnet 8675312 — DB has:
  pool 0x58a86a…    is_contract=1, code_len=0
  USDL 0xc65ea8…    is_contract=1, code_len=0
The API was returning false for both; with this change, true.
2026-05-24 06:41:32 -07:00
Darkhorse7starsandGitHub 0d2c6c4495 evm: upsert addresses with tx_count increment + eth_getCode lookup (#10)
* evm: honor Config.StartBlock when DB is fresh (skip historical backfill)

When running against a mainnet-fork devnet (e.g. an anvil
--fork-url=... mainnet snapshot), the chain's reported latest block is
in the tens of millions. The existing behavior was to backfill from
block 0, which is wasteful (~14 days at ~21 blocks/sec) AND surfaces
millions of irrelevant pre-fork txs in the explorer UI.

Add StartBlock to evm.Config. When the indexer's database has no
existing rows (MAX(number) == 0), the indexer now honors StartBlock as
the lowest block to index. Once any block is stored, StartBlock is
ignored and the indexer resumes from MAX+1 as before, so this is
backward-compatible for fresh-genesis chains where StartBlock is left
zero.

Wire-up in luxfi/explorer is a separate change — supervisor.go reads
the value from `chains[].indexer.start_block` in chains.yaml.

* explorer: never emit null for address counters (FE crash fix)

The Blockscout-derived SPA renders the address page with:

    <span>{address.transactions_count.toLocaleString()}</span>

— no null guard. When the indexer returned JSON null for
`transactions_count` (or `token_transfers_count`), the SPA crashed
with "Cannot read properties of undefined (reading 'toLocaleString')"
and rendered its global ErrorBoundary instead of the address page.

Two reasons the indexer was returning null:

1. Schema drift. The SQLite path in evm/indexer.go writes addresses
   with the trimmed columns `tx_count` and `code`. formatAddress and
   addrCounters were reading the legacy column names
   `transactions_count` and `contract_code`, so SQLite-backed installs
   always returned null + is_contract=false even for real deployed
   contracts.

2. No fallback. Even on the Postgres path, an address that hasn't
   sent or received a tracked event yet has NULL counters in some
   schemas; we returned that NULL as JSON null instead of zero.

Fix both:
  - formatAddress and addrCounters now check both column-name
    variants and coerce nil to int64(0).
  - is_contract now also honors the `code` column (treats "" / "0x"
    as no code, anything else as a contract).

Repros locally against the live cluster: GET /v1/indexer/evm/addresses/
0x58a86aafb6cdd0989c799353c891e420fb530e0a returned
  transactions_count: null, token_transfers_count: null, is_contract: false
for a real deployed AMM pool contract → after this change → integer
counters + is_contract: true.

* evm: upsert addresses with tx_count increment + eth_getCode lookup

Two bugs the previous fix (#9) couldn't reach from the format layer:

1. `transactions_count` was always 0 for every address. The address
   upsert SQL was `INSERT OR IGNORE` / `ON CONFLICT DO NOTHING`, so
   the first time we saw an address we inserted tx_count=0 and every
   subsequent tx referencing that address was a no-op. The counter
   never moved.

2. `is_contract` was wrong for contracts not created via top-level
   tx (e.g. a SimplePool deployed via `new SimplePool(...)` inside
   a foundry script): the indexer only flagged is_contract=true when
   tx.ContractAddress matched, which is empty for inner CREATEs.

Fix:

  • Change the upsert to ON CONFLICT DO UPDATE with
    `tx_count = old + delta` and `is_contract = OR-merge`. Each
    block contributes a per-address delta (count of distinct txs
    in that block touching the address; self-sends dedup to 1).

  • After collecting per-block addresses, call eth_getCode on every
    address that wasn't already pinned as a contract by
    tx.ContractAddress. If the chain returns non-empty code,
    mark is_contract=true at upsert time. Cost is bounded by
    unique addresses per block (~5 typical).

  • Same logic in backfillAddresses for one-shot bootstrap.

Manual repro on the live cluster after deploying this fix:
  GET /v1/indexer/evm/addresses/<pool>
    transactions_count: 4    is_contract: true
  (was: 0 / false despite indexer having logged the deploy + 3 swaps)
2026-05-24 06:06:05 -07:00
Darkhorse7starsandGitHub 26e21716f6 explorer: never emit null for address counters (FE crash fix) (#9)
* evm: honor Config.StartBlock when DB is fresh (skip historical backfill)

When running against a mainnet-fork devnet (e.g. an anvil
--fork-url=... mainnet snapshot), the chain's reported latest block is
in the tens of millions. The existing behavior was to backfill from
block 0, which is wasteful (~14 days at ~21 blocks/sec) AND surfaces
millions of irrelevant pre-fork txs in the explorer UI.

Add StartBlock to evm.Config. When the indexer's database has no
existing rows (MAX(number) == 0), the indexer now honors StartBlock as
the lowest block to index. Once any block is stored, StartBlock is
ignored and the indexer resumes from MAX+1 as before, so this is
backward-compatible for fresh-genesis chains where StartBlock is left
zero.

Wire-up in luxfi/explorer is a separate change — supervisor.go reads
the value from `chains[].indexer.start_block` in chains.yaml.

* explorer: never emit null for address counters (FE crash fix)

The Blockscout-derived SPA renders the address page with:

    <span>{address.transactions_count.toLocaleString()}</span>

— no null guard. When the indexer returned JSON null for
`transactions_count` (or `token_transfers_count`), the SPA crashed
with "Cannot read properties of undefined (reading 'toLocaleString')"
and rendered its global ErrorBoundary instead of the address page.

Two reasons the indexer was returning null:

1. Schema drift. The SQLite path in evm/indexer.go writes addresses
   with the trimmed columns `tx_count` and `code`. formatAddress and
   addrCounters were reading the legacy column names
   `transactions_count` and `contract_code`, so SQLite-backed installs
   always returned null + is_contract=false even for real deployed
   contracts.

2. No fallback. Even on the Postgres path, an address that hasn't
   sent or received a tracked event yet has NULL counters in some
   schemas; we returned that NULL as JSON null instead of zero.

Fix both:
  - formatAddress and addrCounters now check both column-name
    variants and coerce nil to int64(0).
  - is_contract now also honors the `code` column (treats "" / "0x"
    as no code, anything else as a contract).

Repros locally against the live cluster: GET /v1/indexer/evm/addresses/
0x58a86aafb6cdd0989c799353c891e420fb530e0a returned
  transactions_count: null, token_transfers_count: null, is_contract: false
for a real deployed AMM pool contract → after this change → integer
counters + is_contract: true.
2026-05-24 03:56:12 -07:00