mirror of
https://github.com/luxfi/indexer.git
synced 2026-09-24 00:25:37 +00:00
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]>
7.7 KiB
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(NOTlux-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/agev1.4.0 - Transport:
hanzoai/replicateWAL 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}/instancesand/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, souri_statesays whether the chain answered ("ok"), named nothing ("absent"), or has not been asked ("unread").image_urlandmetadatastay 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/defiadditionally 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.sqlis that same unbuilt venue's schema and is not applied — the live API reads the SQLite tablesevm/indexer.gocreates.- 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 |