Skip to content

About

Official FusionLayer Block Explorer.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

Repository files navigation

EVM Block Explorer

A self-hosted block explorer for Proof-of-Work EVM chains — Etherscan/Blockscout-style UI, a full block indexer, PoW reward and fee-burn accounting, token tracking, a Solidity contract verifier, live gas tracker, and both a native REST API and an Etherscan-compatible API.

Everything is driven by one config file: chain name, currency, chain ID, RPC endpoints, reward schedule, and branding.


What it does

Indexing

  • Blocks, transactions, receipts, logs, uncles
  • Internal transactions via debug_traceBlockByNumber (callTracer) or trace_block, auto-detected
  • Reorg detection and rollback (configurable depth)
  • ERC-20 / ERC-721 / ERC-1155 transfer extraction, token metadata, holder balances
  • Native balance refresh for touched accounts
  • Daily statistics rollups that back every chart

PoW accounting

  • Block rewards read from the chain itself (trace_block reward traces or coinbase balance deltas), with a configurable schedule as fallback
  • Uncle rewards (uncleNumber + 8 − blockNumber) × base / 8 and inclusion bonus base / 32
  • EIP-1559 base-fee burn tracked per transaction and per block (disable for pre-1559 chains)
  • Optional treasury/dev-fund split off the block subsidy

Explorer UI

  • Dashboard with live blocks/transactions (SSE) and summary graphs
  • Blocks, uncles, transactions, internal transactions, token transfers
  • Transaction detail showing which contract function was called, with decoded arguments — from the verified ABI when available, otherwise from a signature database
  • Decoded event logs, address pages with tabs, token pages with holders and NFT inventory
  • Top accounts (rich list), top miners with hashrate distribution
  • Charts & statistics page, live gas tracker
  • Contract source viewer, Read Contract, Write Contract (via your browser wallet)
  • Dark and light themes

Contract verification

  • Single flattened file, multiple files, or Solidity standard-JSON input
  • Compiler binaries downloaded and cached on demand (checksum-verified)
  • Full vs partial match, with metadata, immutables and library placeholders masked
  • Constructor arguments recovered from the deployment transaction
  • Proxy detection (EIP-1167 minimal proxies, EIP-1967 slots)
  • Etherscan-compatible verifysourcecode so hardhat verify and forge verify-contract work

Requirements

  • Ubuntu 22.04 or 24.04 (the installer targets Debian/Ubuntu; the app itself is portable)
  • Node.js 20+
  • PostgreSQL 14+
  • An EVM node with JSON-RPC enabled

For internal transactions, expose a tracing API on the node:

geth --http --http.api eth,net,web3,debug --gcmode archive

Erigon/OpenEthereum-family nodes are detected through trace_block instead. Without either, the explorer runs fine — the Internal Transactions tabs are simply empty and a warning is logged.


Install on Ubuntu

git clone <your-repo> evm-explorer && cd evm-explorer
sudo bash deploy/install.sh \
  --rpc http://127.0.0.1:8545 \
  --chain-id 5070 \
  --chain-name "FusionLayer" \
  --short-name FXL \
  --currency-name FusionLayer \
  --symbol FXL \
  --domain explorer.example.com \
  --ssl admin@example.com

The installer sets up Node 20, PostgreSQL, nginx, a service user, a database with a generated password, builds both apps, installs two systemd units, and configures nginx to serve the explorer on both the bare server IP and your domain.

When it finishes you get:

Access URL
Direct (no nginx) http://<server-ip>:4000
Via nginx, bare IP http://<server-ip>/
Via nginx, domain https://explorer.example.com/

Useful flags: --port, --start-block, --db-name/--db-user/--db-pass, --install-dir, --skip-nginx, --force-config. Run bash deploy/install.sh --help for the full list.

Docker alternative

cp config/chain.config.example.json config/chain.config.json   # edit it
docker compose -f deploy/docker-compose.yml up -d --build

A node on the Docker host is reachable from the containers at http://host.docker.internal:8545.


Manual setup (development)

# database
sudo -u postgres createuser explorer --pwprompt
sudo -u postgres createdb -O explorer explorer

# config
cp config/chain.config.example.json config/chain.config.json
cp .env.example .env            # optional overrides

# backend
cd server && npm install && npm run build
node dist/tools/migrate.js      # apply schema
node dist/indexer/main.js &     # indexer
node dist/api/main.js &         # API + UI

# frontend (dev server with hot reload, proxies /api to :4000)
cd ../web && npm install && npm run dev

For production the API serves web/dist itself, so a single port covers UI and API.


Configuration

config/chain.config.json — every field is documented below. Any value can be overridden by an environment variable (see .env.example), which takes precedence.

chain

"chain": {
  "name": "FusionLayer",
  "shortName": "FXL",
  "chainId": 5070,
  "networkId": 5070,
  "consensus": "pow",
  "currency": { "name": "FusionLayer", "symbol": "FXL", "decimals": 18 },
  "logoUrl": "",
  "description": "Proof-of-Work EVM chain"
}

The indexer warns at startup if chainId disagrees with the node's eth_chainId.

rpc

"rpc": {
  "urls": ["http://127.0.0.1:8545", "http://backup:8545"],
  "batchSize": 40,
  "timeoutMs": 30000,
  "maxRetries": 3,
  "maxConcurrency": 4
}

Multiple URLs are used as failover — a transport error rotates to the next endpoint.

rewards — the PoW money supply

Block rewards are measured from the chain, not assumed from a formula. The explorer never has to be told what a block paid: it asks the node, so forks, treasury splits and any consensus quirk are picked up automatically.

"rewards": {
  "source": "trace",
  "fallback": "schedule",
  "schedule": {
    "mode": "decay",
    "decay": { "initialWei": "9000000000000000000", "intervalBlocks": 500000, "reductionPercent": 5 },
    "constantWei": "9000000000000000000",
    "eras": []
  },
  "uncles": { "enabled": true, "maxDepth": 6, "inclusionRewardDivisor": 32, "formula": "ethereum" },
  "burn": { "enabled": true },
  "treasury": []
}

How rewards are obtained

source How it works Requires
trace (default) Reads the type: "reward" entries from trace_block — the engine's own record of what it credited, per block and per uncle a client with the trace_ API (Erigon, Nethermind, OpenEthereum-family)
balanceDelta Derives issuance from the coinbase balance change across the block, subtracting the fees, transfers and gas the indexer already knows about an archive node (historical eth_getBalance)
schedule Computes the reward from the formula below nothing

At startup the indexer probes the node and logs which source it settled on:

[rewards] block rewards will be read from trace_block reward traces (probed block 1)

With source: "trace" the indexer also prefers trace_block for internal transactions, since one call then returns both — no extra RPC round trip per block.

fallback decides what happens for a block the primary source cannot report (schedule, or none to record zero). Every block stores which method produced its number, and the UI labels it: traced / balance delta / estimated — so an estimated figure is never passed off as fact.

The schedule (fallback only)

Consulted only when the chain cannot be measured, so it should mirror consensus as closely as you know it.

schedule.mode Behaviour
constant Every block pays constantWei
eras Reward changes at the listed fork blocks (Ethereum-style 5 → 3 → 2)
decay initialWei reduced by reductionPercent every intervalBlocks

FusionLayer ships as decay: 9 FXL, −5% every 500,000 blocks, i.e. 9 → 8.55 → 8.1225 → … Fractional percentages are allowed.

Era boundaries are inclusive of the interval block: block 500,000 still pays 9 FXL and the first reduction lands on 500,001 (the ECIP-1017 convention). This was measured on chain, not assumed — eth_getBalance deltas give exactly 9.000000 FXL at block 500,000 and 8.550000 at 500,001.

Note the schedule is an approximation of consensus by construction — prefer a traceable or archive node so it is never used.

  • uncles.formula: "none" disables uncle rewards entirely. Use it for chains that record uncles but do not pay for them — FusionLayer is one of these. Measured on chain: the includer receives a flat subsidy regardless of uncle count, and a non-includer uncle miner receives 0 FXL (verified at blocks 495,003 and 495,008). With none, the explorer neither credits uncle miners nor carves an inclusion bonus out of the measured subsidy.
  • burn.enabled: false for chains that never activated EIP-1559 — all fees then go to the miner.
  • treasury diverts a percentage of the block subsidy when using schedule. Under trace, treasury payouts are detected automatically from reward traces addressed to non-coinbase accounts and shown as their own line, so this list can stay empty.

Treasury / dev-fund grants

treasury describes protocol payments to a fixed address:

"treasury": [
  { "label": "Team", "address": "0x560ea0…", "mode": "fixed",
    "amountWei": "2000000000000000000", "percent": 0, "fromBlock": 1, "toBlock": 5000000 }
]
mode Meaning
fixed amountWei per block, paid on top of the miner subsidy (does not reduce it)
percent a share carved out of the miner subsidy

toBlock: 0 means forever. Like block rewards these are measured when the node allows it — the grant address's balance delta is taken with its own transaction activity removed — and only fall back to the numbers above when historical state is unavailable.

FusionLayer pays the team 2 FXL per block for the first 5,000,000 blocks. Measured on chain: the grant is flat and does not decay with the miner reward (at block 510,192 the miner received 8.55 FXL and the team 2.0 FXL). Total issuance is therefore 11 FXL/block in era 0, not 9.

Supply accounting

The explorer separates created coin from recycled coin:

  • totalIssued — subsidy + uncle payouts + treasury grants. Transaction fees are excluded, since they move existing coin rather than create it.
  • totalBurnt — EIP-1559 base fee destroyed.
  • circulatingSupply — chain.genesisSupplyWei + totalIssued − totalBurnt. Set genesisSupplyWei if your chain has a premine; FusionLayer has none, so it is "0".

These cover the indexed range only. If you start indexing at a non-zero startBlock, they describe that window rather than the whole chain.

Changing the reward config after indexing does not retroactively fix stored rewards. Re-index from the affected height:

sudo systemctl stop explorer-indexer
sudo -u explorer node /opt/evm-explorer/server/dist/tools/maintenance.js reindex-from <N>
sudo systemctl start explorer-indexer

indexer

"indexer": {
  "startBlock": 0,
  "confirmations": 0,
  "batchBlocks": 20,
  "pollIntervalMs": 2000,
  "reorgDepth": 64,
  "receipts": { "method": "auto" },
  "traces": { "enabled": true, "method": "auto" },
  "tokens": { "enabled": true, "trackBalances": true, "metadataConcurrency": 4 },
  "balances": { "enabled": true, "batchSize": 200, "intervalMs": 5000 },
  "stats": { "intervalMs": 60000 }
}
  • receipts.method — auto probes eth_getBlockReceipts and falls back to batched eth_getTransactionReceipt. Force with block or individual.
  • traces.method — auto, callTracer, traceBlock, or off.
  • confirmations — keep the indexer N blocks behind the head on a chain with frequent reorgs.

server

"server": {
  "host": "0.0.0.0",
  "port": 4000,
  "publicUrl": "https://explorer.example.com",
  "corsOrigins": ["*"],
  "trustProxy": true,
  "rateLimit": { "enabled": true, "max": 300, "windowMs": 60000 },
  "serveWeb": true
}

serveWeb: false turns the process into a pure API (useful if a CDN serves web/dist).

verification, features, web, price

"verification": { "enabled": true, "solcDir": "./data/solc", "allowDownload": true, "compileTimeoutMs": 120000 },
"features": { "internalTransactions": true, "tokens": true, "verification": true, "charts": true,
              "etherscanApi": true, "topAccounts": true, "gasTracker": true },
"web": { "title": "FXL Explorer", "tagline": "", "themeColor": "#f2a900",
         "links": { "website": "", "github": "", "twitter": "", "discord": "" } },
"price": { "enabled": false, "source": "coingecko", "coingeckoId": "", "refreshMs": 300000 }

Turning a features flag off hides the nav entry and returns 404 from the matching endpoints. For an air-gapped host set verification.allowDownload: false and pre-populate data/solc/ with the compiler binaries and list.json.


Operating it

systemctl status explorer-api explorer-indexer
journalctl -u explorer-indexer -f
systemctl restart explorer-api explorer-indexer     # after editing the config

curl localhost:4000/api/v1/health                   # db + rpc + indexer lag

After rebuilding the web UI, restart the API. The static file handler resolves web/dist when the API boots, so a freshly built bundle gets a new hashed filename that the running process will not serve — the request falls through to the SPA handler and returns index.html with a text/html content type. The browser then refuses it ("Expected a JavaScript-or-Wasm module script") and the page renders blank:

cd /opt/evm-explorer/web && npm run build && systemctl restart explorer-api

/api/v1/health returns 503 when the database or RPC is unreachable — point your uptime monitor at it.

Maintenance CLI — run from /opt/evm-explorer/server as the explorer user:

node dist/tools/maintenance.js status              # chain head, indexed head, lag, counters
node dist/tools/maintenance.js rebuild-stats       # recompute every day in stats_daily
node dist/tools/maintenance.js recount             # recompute the counters table
node dist/tools/maintenance.js reindex-from 500000 # drop data from a height and rewind
node dist/tools/maintenance.js refresh-balances    # flag every address for a balance refresh
node dist/tools/maintenance.js refresh-token 0x…   # refetch one token's metadata
node dist/tools/maintenance.js reset-tokens        # refetch metadata for every token

Re-index from scratch

sudo systemctl stop explorer-indexer explorer-api
sudo -u postgres psql -c "DROP DATABASE explorer;"
sudo -u postgres createdb -O explorer explorer
sudo systemctl start explorer-api explorer-indexer

Backup

sudo -u postgres pg_dump -Fc explorer > explorer-$(date +%F).dump

Sync speed. Indexing is bounded by RPC round-trips. Raise indexer.batchBlocks and rpc.batchSize if the node keeps up; lower them if the node starts timing out. Tracing is by far the most expensive step — set traces.enabled: false for a fast initial sync and turn it back on later (already-indexed blocks will not gain traces retroactively without a re-index).

labels — address display names

Addresses can be shown as names instead of hashes. Two sources feed one registry:

"labels": {
  "file": "./address-names.json",
  "fusionx": {
    "enabled": true,
    "address": "0x13e4b3fE79388C6eF206481655c8557320811104",
    "profileBaseUrl": "https://fusionx.social",
    "refreshMs": 300000
  }
}

Manual entries live in config/address-names.json, resolved next to the chain config. Only name is required:

{
  "0x560ea05cc475396cb3d957d55b5da3e54d819a1e": {
    "name": "FusionLayer Team",
    "type": "team",
    "description": "Protocol treasury.",
    "link": "https://fusionlayer.org/",
    "links": { "GitHub": "https://github.com/0xFusionLayer" },
    "verified": true
  }
}

type renders as a badge (free text — exchange, bridge, team, contract, burn…). Keys are case-insensitive and $-prefixed keys are treated as comments.

Applying an edit. The file is read once at API startup and cached, and only the API reads it — the indexer is not involved, so nothing needs re-indexing:

sudo nano /opt/evm-explorer/config/address-names.json
sudo systemctl restart explorer-api

Browsers pick the change up within 5 minutes, or immediately on reload. Confirm with curl -s localhost:4000/api/v1/labels | jq '.count'.

The file is never overwritten by an upgrade — it is excluded from the installer's rsync and only seeded from address-names.example.json when absent, so hand-curated names survive install.sh even with --force-config. Edit the live file on the server; the example is only a template.

A malformed entry is skipped with a warning rather than taking the API down — check journalctl -u explorer-api -n 30 after a restart if a name does not show up.

FusionX usernames are mirrored from the social contract. The indexer enumerates the registry via getTotalUsers / getUserAddressById / getUserBasic / getUserProfile and stores it in fusionx_users. A full pass runs every refreshMs, which also catches username and profile edits that an append-only event scan would miss. Addresses then display as @username with a link to ${profileBaseUrl}/${username}. Set enabled: false on a chain without FusionX.

Manual entries win over on-chain usernames, so you can override a self-assigned name.

  • Names appear anywhere an address is rendered — block miners, transaction from/to, account lists — but links always use the address, never the name.
  • Both are searchable: arrow, @arrow and FusionLayer Team all resolve to their address, with exact username matches ranked first.
  • GET /api/v1/labels returns the whole set (small enough to fetch once and cache client-side); GET /api/v1/labels/:address returns one.

Upgrading

Upgrading is designed to be safe: nothing is destructive, and indexed data survives.

cd /opt/evm-explorer
systemctl stop explorer-indexer explorer-api

# drop in the new source, keeping config/ and data/
unzip -o ~/explorer.zip

cd server && npm install && npm run build
cd ../web && npm install && npm run build
cd ../server && npm run migrate

systemctl start explorer-api explorer-indexer
journalctl -u explorer-indexer -f

What protects you:

  • Migrations are additive and idempotent. Every statement uses IF NOT EXISTS / ADD COLUMN … DEFAULT, applied files are recorded in schema_migrations, and an advisory lock stops the API and indexer racing to migrate at the same moment. Re-running npm run migrate on an up-to-date database is a no-op. New columns land on existing rows with a default rather than rewriting them.
  • Config gains new keys automatically. The file is merged over built-in defaults, so a key added in a later version starts with a sensible value and your file keeps working untouched.
  • Renamed or removed keys stop the boot. Unknown keys would otherwise be ignored while the renamed ones silently fell back to defaults — a stale rewards.mode config would have started cleanly and paid the default 2 coin subsidy instead of FusionLayer's 9. Startup now refuses and prints the old → new mapping.
  • The installer will not clobber your config. deploy/install.sh keeps an existing config/chain.config.json unless you pass --force-config.

When a re-index is required

Indexed rows are a snapshot of how the code interpreted each block, so changing that interpretation does not rewrite history by itself.

Change Re-index?
API, web UI, styling, charts No — restart only
New API field computed from stored columns No
Reward logic, treasury grants, burn settings Yes, from the affected height
Token detection, tx method decoding, trace settings Yes, for the affected range
Adding a column that is only populated going forward No

Re-index a range without discarding the rest of the chain. Stop the indexer first — this deletes indexed data from that height and rewinds the cursor, and the indexer refills it on next start:

systemctl stop explorer-indexer
cd /opt/evm-explorer/server
npm run maintenance -- reindex-from 495000     # drop from this height, rewind the cursor
systemctl start explorer-indexer               # refills as it catches back up

Deleting a range decrements the running totals (total_issued, total_burnt, block and transaction counts) as part of the same transaction, so re-indexing does not double-count supply. If the counters ever look wrong anyway, rebuild them from the underlying tables:

npm run maintenance -- recount          # counters table, recomputed from scratch
npm run maintenance -- rebuild-stats    # daily chart aggregates
npm run maintenance -- status           # chain/indexer health and current counters

Version pinning

package-lock.json is committed for both server/ and web/, so npm install reproduces the same dependency tree on the VPS. Node 20+ is the only host requirement.


API

Native REST API under /api/v1 and an Etherscan-compatible surface at /api. Full endpoint listing is at /api-docs in the UI.

curl localhost:4000/api/v1/stats
curl localhost:4000/api/v1/blocks?limit=5
curl localhost:4000/api/v1/transactions/0x…
curl "localhost:4000/api?module=account&action=balance&address=0x…"

Live updates come from /api/v1/stream (Server-Sent Events, blocks and transactions events).

Verifying contracts from tooling

No API key is needed — any value is accepted.

// hardhat.config.ts
etherscan: {
  apiKey: { fxl: "any" },
  customChains: [{
    network: "fxl",
    chainId: 5070,
    urls: { apiURL: "https://explorer.example.com/api", browserURL: "https://explorer.example.com" }
  }]
}
forge verify-contract <ADDRESS> <CONTRACT> \
  --verifier etherscan \
  --verifier-url https://explorer.example.com/api \
  --etherscan-api-key any

Layout

config/         chain.config.json — the single source of truth
db/migrations/  SQL schema, applied automatically at startup
server/         TypeScript backend
  src/config.ts     config loader with env overrides
  src/rpc.ts        batching JSON-RPC client with failover
  src/indexer/      block ingestion, rewards, traces, tokens, balances, stats
  src/api/          Fastify server, routes, gas oracle, SSE stream
  src/verifier/     solc manager, bytecode matching, verification
  src/tools/        migrate.ts (schema) and maintenance.ts (ops CLI)
web/            React + Vite single-page UI
deploy/         install.sh, nginx templates, systemd units, Docker

The API and the indexer are separate processes sharing one database, so the indexer can be restarted or paused without taking the site down.


Notes and limits

  • Vyper verification is not implemented — Solidity only.
  • Compiler binaries are the official linux-amd64 builds. On another platform, point verification.solcBaseUrl/solcListUrl at the matching directory on binaries.soliditylang.org.
  • total_addresses, total_contracts and total_tokens counters are recomputed by the stats job (default every 60s), so they trail the live tables slightly.
  • Token holder counts are refreshed periodically rather than on every transfer; balances themselves are updated per block.
  • The rich list ranks accounts the indexer has seen. Accounts funded in the genesis block only appear once they are touched by a transaction, unless you index from block 0.

About

Official FusionLayer Block Explorer.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages