Files
zeekayandHanzo Dev 3d6388d6ab An NFT transfer says which NFT
The indexer had the item id all along and threw it away at the edge.
formatTokenTransfer wrote every row in the ERC-20 shape — an amount and a
null decimals — so the three Lux Genesis mints, which moved ids 0, 1 and 2,
came back as three identical rows and no page could name what had moved.
The id now rides in `total`, whose shape follows the token type the way
luxfi/explore's Erc721TotalPayload / Erc1155TotalPayload already read it.

/tokens/{addr}/instances was a hardcoded empty list and /instances/{id} was
an unregistered route, so a collection had no items and an item had no page.
Both are real now. The item list comes from the transfer table rather than
the balance table: balances hold only what someone holds now, so a burned
item would vanish from a collection that still minted it, while its last
transfer always exists and names its current owner.

ERC-1155 was not indexed at all. It is now, single and batch — a batch log
yields one row per (id, amount) pair, so no id is folded away.

Underneath, one decoder. There were two, and they disagreed: the daemon's
path read ERC-20 and ERC-721 only, spelled types "ERC-721", and stored the
raw amount hex; the adapter's path also read ERC-1155 and ERC-404 but
spelled types "ERC721" and stored ids two different ways depending on which
event carried them. decodeTransfers is now the only place a log becomes a
transfer, with one spelling, decimal amounts, and one stored id shape.
applyERC20BalanceDelta and applyERC721BalanceDelta collapse into move(),
which is one function because the three token kinds differ only in what the
id column holds.

An item's tokenURI is read on chain and recorded; the document behind it is
not fetched. An indexer that waits on an IPFS gateway stops indexing, and
the document is not ours and can change. uri_state distinguishes the three
answers that were previously one blank: the chain gave a URI, the chain
names none, or we have not asked. image_url and metadata stay null rather
than assert media nobody read.

Verified against C-Chain 96369: Lux Genesis
(0x9e04fc57c20b2ee45627c4aa280eb471f2ca6ea5, totalSupply 3) indexed from the
live RPC now reports ids 0, 1, 2 on its transfers and lists three items with
their owners, matching eth_getLogs topics[3] and ownerOf.

migrations/004_nft_marketplace.sql stays unapplied and now says why: no
marketplace contract is deployed on any Lux chain, so its tables could only
ever be empty. LLM.md's EVM parity list claimed Seaport/LooksRare/Blur/
X2Y2/Rarible/Zora coverage for a package nothing imports; it now says what
runs.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-29 13:35:20 -07:00

7.7 KiB

Explorer

Overview

Single-binary block explorer. Indexes EVM and multi-chain networks, serves explorer API v2, embeds the frontend. SQLite by default, PQ encrypted streaming backups to S3. No PostgreSQL, no Elixir, no external services.

Package Information

  • Module: github.com/luxfi/explorer
  • Binary: explorer (NOT lux-explorer)
  • Go: 1.26.1
  • Image: ghcr.io/luxfi/explorer

Architecture

explorer (single binary)
│
├── Indexer goroutines      Write → SQLite (WAL, sole writer)
├── Base HTTP server        Read → SQLite (RO, 8 conns)
│   ├── /v1/explorer/*      Explorer API v2 REST
│   ├── /v1/explorer/graphql GraphQL
│   ├── /v1/explorer/ws     WebSocket/SSE realtime
│   └── /*                  Embedded Next.js frontend (go:embed)
├── Replicate sidecar       WAL → S3 (age PQ encrypted)
└── ZapDB KV             Fast hash/height lookups

Storage

  • SQLite WAL: default, zero-config, concurrent readers
  • ZapDB: KV layer for O(1) hash lookups
  • PostgreSQL: optional via -tags postgres
  • No Redis: ZapDB replaces it

PQ Encrypted Streaming Backups

Every SQLite WAL write is streamed to S3, encrypted with post-quantum cryptography:

  • Algorithm: ML-KEM-768 + X25519 hybrid (NIST FIPS 203)
  • Library: luxfi/age v1.4.0
  • Transport: hanzoai/replicate WAL streaming
  • Chunk size: 64KB ChaCha20-Poly1305 AEAD per chunk
  • Forward secrecy: fresh ephemeral keys per WAL segment
  • Restore: point-in-time to any TXID from S3
SQLite WAL → Replicate → age (ML-KEM-768 ∥ X25519) → S3

Enable: REPLICATE_S3_ENDPOINT + REPLICATE_AGE_RECIPIENT env vars.

Rust Services (Optional)

Pure Go implementations exist for everything. Rust originals available as static library via CGO:

explorer-rs/ffi/ → libexplorer_ffi.a (staticlib)
  ├── smart-contract-verifier  C ABI: explorer_verify_solidity()
  ├── sig-provider             C ABI: explorer_lookup_signature()
  └── memory mgmt              C ABI: explorer_free()

rustffi/verifier.go            Go CGO bindings (build tag: rustffi)

Build without Rust: go build ./cmd/explorer/ (default, pure Go) Build with Rust: CGO_ENABLED=1 go build -tags rustffi ./cmd/explorer/

Directory Structure

cmd/explorer/           Unified binary entry point
cmd/indexer/            Per-chain indexer (legacy)
cmd/evmchains/          Multi-EVM (shared DB)
cmd/multichain/         100+ chain parallel indexer

explorer/               API layer (imports hanzoai/base as library)
  explorer.go           Plugin registration, read-only SQLite conn
  handlers.go           /v1/explorer/* route handlers (30 endpoints)
  format.go             Explorer v2 response formatters
  collections.go        User data: watchlists, tags, custom ABIs
  testutil/factory.go   Test data factory

evm/                    EVM indexer
  adapter.go            Main indexer (2330 lines)
  api/server.go         REST + GraphQL + WebSocket + JSON-RPC
  api/repository.go     Database queries
  api/graphql.go        GraphQL schema
  contracts/            Verification, proxy detection, ABI decode
  defi/                 DeFi protocol indexing
  account/              User accounts, API keys
  erc4337.go            Account Abstraction
  mev.go                MEV detection
  blob.go               EIP-4844

storage/                Dual-layer storage
  unified.go            KV + Query combined
  kv/                   ZapDB
  query/sqlite.go       SQLite (default)
  query/postgres.go     PostgreSQL (build tag)

rustffi/                Optional CGO bindings to Rust static lib
{x,a,b,q,t,z,k}chain/  DAG chain adapters
pchain/                 Platform chain adapter
chain/, dag/            Shared libraries
migrations/             SQL schema (4 files)
e2e/                    End-to-end tests

API Routes

All under /v1/explorer/:

Endpoint Description
GET /blocks List blocks (cursor pagination)
GET /blocks/{id} Block by hash or number
GET /blocks/{id}/transactions Block's transactions
GET /transactions List transactions
GET /transactions/{hash} Transaction details
GET /transactions/{hash}/token-transfers Token transfers in tx
GET /transactions/{hash}/internal-transactions Internal calls
GET /transactions/{hash}/logs Event logs
GET /addresses/{hash} Address details + balance
GET /addresses/{hash}/transactions Address transactions
GET /addresses/{hash}/tokens Token balances
GET /tokens List tokens
GET /tokens/{addr}/holders Token holders
GET /smart-contracts/{addr} Verified contract + ABI
POST /smart-contracts/{addr}/verify Submit verification
GET /search Full-text search
GET /stats Chain statistics
POST /graphql GraphQL queries
WS /ws Realtime subscriptions

White-Label

Zero branding in code. Configure at runtime:

explorer --chain-name="My Chain" --coin=MYC --chain-id=12345

EVM Feature Parity

What the Go indexer covers, and where it stops:

  • Block indexing (consensus, uncle, EIP-4844 blobs)
  • Transaction types 0-3 (legacy, access list, EIP-1559, EIP-4844)
  • Internal transaction tracing (3 tracers: callTracer, jsTracer, parityTracer)
  • Token tracking: ERC-20, ERC-721, ERC-1155 (single + batch), ERC-404. One decoder reads them all — evm/decodeTransfers — so a movement has one reading whichever indexing path saw the block. ERC-20 and ERC-721 share the Transfer signature and are told apart by topic arity: the ERC-721 id is indexed and rides in topics[3].
  • NFT items: /tokens/{addr}/instances and /instances/{id} — id, current owner (the destination of the item's last transfer), and the URI the contract gives it. The URI is an eth_call; the document behind it is not fetched, so uri_state says whether the chain answered ("ok"), named nothing ("absent"), or has not been asked ("unread"). image_url and metadata stay null rather than assert media nobody read.
  • Contract verification: Solidity (all versions), Vyper, Standard JSON
  • Proxy detection: EIP-1967, EIP-1822, EIP-1167, Gnosis Safe, OpenZeppelin
  • Account Abstraction: ERC-4337 UserOps, bundlers, paymasters
  • MEV detection: sandwich, frontrun, backrun, liquidation
  • DeFi: AMM V2/V3, perps, lending, staking, bridges, CLOB orderbook
  • evm/defi additionally carries event signatures for Seaport, LooksRare, Blur, X2Y2, Rarible and Zora. Nothing imports the package and no Lux chain has a marketplace contract deployed, so no marketplace is indexed today. migrations/004_nft_marketplace.sql is that same unbuilt venue's schema and is not applied — the live API reads the SQLite tables evm/indexer.go creates.
  • Charts, statistics, gas oracle
  • User accounts, API keys, watchlists, address tags

Testing

go test ./...                    # All tests (431+ across 22 packages)
go test ./explorer/...           # API layer (35 tests)
go test ./evm/...                # EVM indexer
go test ./storage/...            # Storage backends

Dependencies

Package Purpose
hanzoai/base HTTP server, collections, SSE, IAM
hanzoai/replicate SQLite WAL streaming to S3
luxfi/age PQ encryption (ML-KEM-768 + X25519)
luxfi/database ZapDB KV wrapper
luxfi/zap Zero-copy wire protocol

What This Replaced

Before (6 containers) After (1 binary)
Elixir backend evm/ Go package
Rust verifier evm/contracts/verifier.go
Rust sig-provider evm/contracts/abi.go
PostgreSQL SQLite WAL
Redis ZapDB KV
Graph Node + subgraphs G-Chain native GraphQL