Skip to content

Repository files navigation

Nura Explorer

An open source block explorer for EVM chains. Every block, transaction and transfer is indexed locally, so you can follow where value actually moved.

CI Built with AzerothJS Node >= 24

Nura Explorer overview

What it is

A self-hosted explorer for any EVM-compatible chain. Point it at a JSON-RPC endpoint and it indexes the chain into a local SQLite file, then serves six pages over that index: a live overview, blocks, transactions, block and transaction detail, and an address page with a flow ledger showing value in and out - plus, when the address holds code, what that code can do.

It speaks standard Ethereum JSON-RPC and nothing else - no vendor API, no hosted service, no tracing extensions. It runs against NuraChain, a local Hardhat or Anvil node, or any other EVM network.

It is not a Bitcoin explorer: Bitcoin is UTXO-based and speaks a different RPC.

Why it indexes instead of proxying the node

Ethereum JSON-RPC has no method that answers "what has this address done?". There is eth_getBalance for a number and eth_getTransactionByHash for a hash you already know, but no address history. Every explorer you have used - Etherscan included - answers that question from its own index rather than from the node. So does this one.

The index is a cache, not a source of truth. Delete .data/index.db and it replays from START_BLOCK.

What a contract address shows

A deployed contract keeps no names. eth_getCode returns runtime bytecode - no ABI, no source, no argument names - so an explorer either verifies source or reads the bytes. This one reads the bytes, and says so on the page:

  • Entry points. The dispatcher compares the first four bytes of every call against the selectors it answers to. Walking the opcodes recovers that list, and a table of published signatures gives most of them their names back. A selector no standard claims is printed as four bytes rather than labelled with a plausible guess.
  • Interfaces. ERC-20, 721, 1155, 165, 2612, 4626, Ownable, AccessControl, Pausable - claimed only when every selector of that interface is present, so the badge means the contract answers them, not that it says it does.
  • Current values. The zero-argument getters it actually has (name, symbol, decimals, totalSupply, owner, paused, ...), called live and decoded.
  • Compiler and source metadata. solc appends a CBOR trailer naming its version and an IPFS hash of the metadata. That is what the deployer stamped, not proof of anything - but it says which compiler to point at the source if you want to verify it.
  • Proxies. EIP-1967, beacon, EIP-1822 and EIP-1167 clones are followed to their implementation, and the functions come from there. A proxy's own code answers nothing.
  • Deployment. Who deployed it, in which transaction and block. This half comes from the index - the chain cannot map a contract back to the receipt that created it.

Source verification is not implemented: nothing here compiles source or checks it against the deployed code, and the page says as much above everything it shows.

Calling one

A named function can also be called, and the page splits the two kinds because the EVM does:

  • Read - view and pure. Answered by this server through its own node: arguments are encoded, eth_call runs, and the return is decoded against the type the standard declares. No wallet, no signature, no fee. A revert comes back as the reason it gives, printed where the value would have been, because a revert is an answer.
  • Write - everything else. The server encodes the calldata and stops there. The browser hands those bytes to the reader's own wallet (EIP-1193, so any injected wallet), the wallet asks its owner, and the wallet sends it. Nothing here signs, and the server's own node connection is never in that path.

Two constraints are load-bearing:

  • The read endpoint is not an RPC passthrough. Only view/pure entries of the signature table can be named, so the callable surface is a fixed list of published getters rather than whatever a caller writes in the body.
  • No Write button exists until the wallet is connected and on this chain. The same calldata sent on another network reaches a different contract, or nothing at all.

Selectors with no published signature are listed but not callable - without an ABI there is no way to know what arguments they take.


Address page with the flow ledger Blocks list in the light theme
Mobile navigation drawer

Requirements

  • Node.js >= 24 - the server runs TypeScript natively, with no build step
  • An EVM JSON-RPC endpoint

Run it

npm install
cp server/.env.example server/.env    # then set RPC_URL and CHAIN_ID
npm run dev

Open http://localhost:3001. The API runs on :3000, with /api proxied to it.

The indexer starts with the server, catches up from START_BLOCK to the head, then follows new blocks every POLL_MS. The first sync of a long chain takes a while; the UI works while it runs and fills in as blocks land.

Against NuraChain

RPC_URL=https://rpc.nurachain.net
CHAIN_ID=1020
CHAIN_NAME=Nura Chain
CURRENCY_SYMBOL=NURA
CHAIN_SITE_URL=https://nurachain.net
START_BLOCK=0

Against a long-lived public chain

Indexing a mainnet from genesis is not practical on one machine. Start near the head:

RPC_URL=https://your-node
CHAIN_ID=1
START_BLOCK=21000000     # a recent height, NOT 0
POLL_MS=6000             # keep it under the chain's block time
BATCH_SIZE=25            # raise for a fast node, lower for a rate-limited one

Only blocks from START_BLOCK up are searchable. That is the trade for a first sync measured in minutes rather than weeks.


Configuration

Every key the server reads is documented in server/.env.example. Keep the two files in step: a key added there belongs in .env too.

Key Default What it does
PORT 3000 Server port
NODE_ENV development production serves the built client
CLIENT_DIR ../application/dist Built client, served from the same origin
SSR_ENTRY ../application/dist-server/entry.server.js SSR bundle
RPC_URL http://127.0.0.1:8545 The chain to index
CHAIN_ID 31337 Chain id, shown in the UI
CHAIN_NAME Local EVM Shown in the header and footer
CURRENCY_SYMBOL ETH Suffixes every amount
CURRENCY_DECIMALS 18 Native token decimals
CHAIN_SITE_URL (unset) The chain's website, linked from its name in the footer
EXPLORER_URL (unset) This explorer's public URL, given to wallets as the block explorer
START_BLOCK 0 Height to index from
POLL_MS 2000 How often to check for a new head
BATCH_SIZE 25 Blocks per catch-up batch
DB_PATH .data/index.db The SQLite index

Production

npm run build
NODE_ENV=production npm start

One process serves the API and the built client on one origin, so there is no CORS to configure. Put a reverse proxy in front for TLS.

In a container - build from the repo ROOT, where the workspace lockfile and .dockerignore live:

docker build -f server/Dockerfile -t nura-explorer .
docker run -p 3000:3000 --env-file server/.env nura-explorer

/api/healthz answers orchestrator probes.

As a systemd service

On a bare host, from a fresh clone - the unit runs the server directly from source, with the client built ahead of it:

npm ci
cp server/.env.example server/.env    # then set RPC_URL and CHAIN_ID
npm run build
sudo npm run service:install
sudo npm run service:start

sudo npm run service:deploy rebuilds and restarts after a git pull.

What to know before running it for real:

  • The index is a file. Back up DB_PATH, or accept a replay on loss. Deleting it is safe.
  • Reorgs are handled. On a parent-hash mismatch the indexer walks back and rolls the orphaned blocks out, rather than serving transactions that were un-mined.
  • eth_getBlockReceipts is probed once and falls back to per-transaction receipts on nodes that lack it - slower, still correct.
  • Rate limiting is on. A burst of requests answers 429.

Scripts

Command What it does
npm run dev Server and client together
npm run build Client bundle, SSR bundle, prerender
npm start Run the built app (set NODE_ENV=production)
npm run check Typecheck and lint every workspace
npm test Every suite in every workspace
npm run test:coverage Server suite with a coverage report
npm run test:shuffle Every suite in random order - the isolation gate
npm run service:deploy Rebuild and restart (root)
npm run service:install Write and load the systemd unit (root)
npm run service:uninstall Stop, disable and remove the unit (root)
npm run service:start / :stop / :restart Control the service (root)
npm run service:status What systemd thinks of it

Tests

Everything runs offline. No suite binds a port, reaches a network or touches a file the repository does not own: the index is sqlite :memory: per test, the node is a stubbed gateway or a stubbed fetch answering JSON-RPC, and the browser half runs under happy-dom. That is what makes the same command give the same answer on a laptop, on a fork's pull request, and at three in the morning.

Command Scope
npm test Everything, both workspaces
npm run test:coverage Server suite plus a coverage report in server/coverage
npm run test:shuffle Everything, in random order
npm test --workspace server Server only
npm test --workspace application Browser half only

Narrower slices, from within a workspace:

Command Scope
npm run test:unit -w server ABI coercion, bytecode analysis, the cache, configuration
npm run test:integration -w server The sqlite index, the sync loop, the RPC client
npm run test:api -w server Both HTTP surfaces - the typed API and the Etherscan shim
npm run test:security -w server Only the security suites
npm run test:property -w server Only the property and fuzz suites
npm run test:components -w application Component rendering and interaction
npm run test:stores -w application The browser stores, wallet included
npm run test:watch -w server Re-run on change

test:shuffle is a gate, not a curiosity. Both halves lean on module-level singletons - the stores in the browser, the signature table on the server - so a test that only passes in declaration order is one that will fail for somebody else, on an unrelated pull request, with no way to reproduce it. CI runs the shuffled pass on every push for that reason.

Where the suites live:

server/tests/
    support/fixtures.ts     a chain, an index and an app, built the same way by every spec
    values.spec.ts          ABI coercion and encoding - unit, boundary, property, fuzz
    contract.spec.ts        bytecode analysis - unit and fuzz over arbitrary bytes
    store.spec.ts           the sqlite index: constraints, transactions, ordering, concurrency
    indexer.spec.ts         sync state transitions, reorgs, partial failure
    client.spec.ts          the JSON-RPC client, against a stubbed node
    cache.spec.ts           the read-through cache in front of the node
    http.spec.ts            the typed API: status codes, validation, security
    etherscan.spec.ts       the wallet-facing compatibility surface
    config.spec.ts          environment and configuration
    app.spec.ts             the original end-to-end suite over the index

application/tests/
    format.spec.ts          amount arithmetic - the highest-risk code in the explorer
    locale.spec.ts          the ten dictionaries and everything that reads them
    components.spec.ts      components mounted in a DOM, driven by real events
    stores.spec.ts          scroll lock, theme, toasts
    wallet.spec.ts          the EIP-1193 wallet store, against a fake provider

Structure

application/                    the UI
  src/components/ui/            button, badge, card, input, pagination, skeleton, toast, tooltip
  src/components/chain/         hash links, cadence strip, flow ledger
  src/pages/                    home, blocks, block, txs, tx, address
server/                         the API and the indexer
  src/app.ts                    every route, schema and handler, declared once
  src/chain/client.ts           the JSON-RPC gateway
  src/chain/indexer.ts          catch-up, follow, reorg rollback, transfer decoding
  src/chain/store.ts            the SQLite index
  src/chain/contract.ts         bytecode -> selectors, event topics, compiler metadata
  src/chain/signatures.ts       the selector -> signature table that gives them names back
  src/chain/values.ts           typed text <-> abi encoding, for the arguments of a call
  src/inspect.ts                one contract: describing it, reading it, encoding a call to it

The API is declared once in server/src/app.ts, and the browser gets a typed client from that same declaration - client.blocks.one(...) is checked against the handler's own schema.


Contributing

Issues and pull requests are welcome. For anything larger than a fix, open an issue first so the approach can be agreed before you spend the time.

Before opening a pull request, all three gates must pass:

npm run check
npm test
npm run test:shuffle

House style is enforced by the linter and visible in any neighbouring file: Allman braces, one import per module, and comments that state a constraint the code cannot show rather than narrating what changed.

  • Adding a page: one row in application/src/routes.ts plus its *.page.azeroth component.
  • Adding a chain field: it starts in server/src/schemas.ts. The browser's client is inferred from the server's declaration, so the wire shape is decided in exactly one place.
  • Anything touching amounts belongs in application/src/lib/format.ts and needs a test. A uint256 does not survive a double, and an explorer that misreports a balance has failed at its only job.

Security

Do not open a public issue for a security bug. Report it privately so a fix can ship before the details are public.

Two things worth knowing before you deploy this:

  • The index is a cache, never a source of truth. Every figure the UI shows can be re-derived from the chain by deleting DB_PATH and replaying. Nothing irreplaceable lives in it.
  • RPC_URL may carry a provider key. It is read server-side and never reaches the browser - the client only ever talks to this server's own API. Keep it that way when adding endpoints.

License

MIT

About

An open block explorer. Every block, transaction and transfer is indexed locally, so you can follow value across the chain.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages