XChain Platform Explorer: Architecture
Position in the Data Pipeline
flowchart TD
NODE["Coin Node (bitcoind / litecoind / dogecoind)"]
DECODER["xchain-decoder"]
DECDB[("Decoder DB (MariaDB)")]
INDEXER["xchain-indexer"]
IDXDB[("Indexer DB (MariaDB)")]
EXPLORER["xchain-explorer"]
OUT["REST API / JSON-RPC / Web UI"]
HUB["xchain-hub"]
NODE -->|"JSON-RPC polling"| DECODER
DECODER --> DECDB
DECDB -->|"SQL reads"| INDEXER
INDEXER --> IDXDB
IDXDB -->|"SQL reads (read-only)"| EXPLORER
EXPLORER --> OUT
HUB -->|"config discovery, 60s refresh"| EXPLORER
The explorer sits at the end of the data pipeline. It reads indexed state from the Indexer database (read-only access) and presents it through three interfaces: a REST API, a JSON-RPC 2.0 endpoint, and a web block explorer. It also connects to the Decoder database for raw transaction data lookups. The explorer never writes to any database.
Internal Components
flowchart TD
API["api.js<br>Express + JSON-RPC server<br>HTTP + HTTPS listeners, Helmet, CORS, rate limit"]
XCE["XChainExplorer<br>Main orchestrator class<br>URL routing, request processing,<br>response formatting, icon/relay handlers"]
DB["Database<br>~9,400 LOC<br>50+ queries<br>Parameterized"]
CONFIG["Config<br>Local + Hub<br>60s auto-sync<br>Coin configs"]
HUBCONN["Hub Connector<br>JSON-RPC client<br>ping + getAll<br>axios-based"]
UTIL["Utility<br>BigNumber<br>Timers<br>Sanitization"]
COINCONFIGS["Coin Configs<br>BTC.js<br>LTC.js<br>DOGE.js"]
API --> XCE
XCE --> DB
XCE --> CONFIG
XCE --> HUBCONN
DB --> UTIL
CONFIG --> COINCONFIGS
Source Files
| File | Class / Module | Role |
|---|---|---|
src/api.js |
None | Entry point: Express server, SSL, Helmet, CORS, rate limiting, JSON-RPC router |
src/XChainExplorer.js |
XChainExplorer |
Main orchestrator: URL routing (130+ routes), request processing, response formatting, icon/relay handlers, SPV proof endpoint dispatch |
src/db.js |
Database |
All SQL queries (~9,400 lines), connection pool management, pagination, caching |
src/config.js |
None | Configuration loading from hub or local config.json, 60-second auto-sync, coin/network discovery |
src/utility.js |
Utility |
BigNumber math, timer functions, sanitization (escapeLike, sanitizeInt), type checking |
src/XChainHubConnector.js |
XChainHubConnector |
JSON-RPC client for xchain-hub (ping, getAllConfig) |
src/XChainDecoderConnector.js |
XChainDecoderConnector |
JSON-RPC client for xchain-decoder’s health endpoint; lets /api/status expose per-coin chain-tip lag without polling decoder ports separately |
src/XChainIndexerConnector.js |
XChainIndexerConnector |
JSON-RPC client for xchain-indexer; proxies read-only feequote and feeschedule endpoints so fee logic stays single-sourced in the indexer |
src/proofServer.js |
ProofServer |
SPV light-client proof server (spec §8.1): builds Merkle balance/state proofs from the indexer’s state_tree_nodes table for client-side verification against quorum-signed checkpoint roots |
src/merkle.js |
None | Consensus-critical, DB-free Merkle primitives for the additive state commitment, per-block content root, and top-level state root; shared byte-identically with xchain-indexer and xchain-sdk |
src/checkpoint_commitment_activation.js |
None | Flag-day gate (SPV Phase 2, spec §6.1/§6.3): determines at which BTC block the signed checkpoint canonical gains state_root and block_merkle_root fields; consensus-critical, vendored across hub/indexer/explorer |
src/equivocation_header.js |
None | Consensus-critical equivocation header (`EQUIV |
src/stake_weighted_quorum.js |
None | Consensus-critical source-deduplicated stake predicate (3 x tally > 2 x total stake); the 2f+1 signer count is the separate pre-activation rule, not this used by every settlement gate and the checkpoint verifier; vendored byte-identically across all consensus-bearing services |
src/IconDownloader.js |
IconDownloader |
In-process worker that downloads, resizes, and caches token icons from the indexer’s icons table |
src/IconResolver.js |
IconResolver |
Pure icon URL resolution logic; mirrors the priority chain used in the web UI’s xchain.js so server and browser select the same source |
src/configs/BTC.js |
None | Bitcoin-specific: chain info, network addresses (burn, gas, protocol, community) |
src/configs/LTC.js |
None | Litecoin-specific configuration |
src/configs/DOGE.js |
None | Dogecoin-specific configuration |
src/config.json |
None | Local database connection configuration (fallback when hub is unavailable) |
Static Content (src/content/)
| Directory | Contents |
|---|---|
content/html/ |
40+ HTML template files for the web block explorer |
content/css/ |
Bootstrap, Highcharts, DataTables, and custom stylesheets |
content/js/ |
jQuery, Bootstrap JS, Highcharts, DataTables, QR code, custom scripts |
content/charts/ |
Chart template files (candlestick, market-depth, line) |
content/images/ |
PNG/ICO images for the web UI |
content/icons/ |
Token icon files (served via the /icon endpoint) |
content/json/ |
JSON data files |
content/fonts/ |
Web fonts |
Request Processing Pipeline
Every incoming request follows this sequence:
1. Middleware Stack
flowchart LR
REQ["Request"] --> RL["Rate Limiter"] --> HELMET["Helmet"] --> CORS["CORS"] --> ROUTER["Express Router"]
- Rate limiter: 500 requests per 60-second window per IP (configurable)
- Helmet: Sets security headers including Content Security Policy
- CORS: Validates origin against configured allowed origins
2. Route Matching
The XChainExplorer.setupUrls() method defines three categories of routes:
| Category | URL Pattern | Response Format |
|---|---|---|
| HTML | /{COIN}/tokens, /{COIN}/address/{QUERY}, etc. |
HTML template file |
| API | /{COIN}/api/{method}/{query}/{type} |
JSON object |
| Explorer | /{COIN}/explorer/{method}/{query}/{type} |
DataTables JSON |
Special routes handled separately:
/icon/*: Token icon files with fallback/relay?url=: CORS proxy for external resources
3. Request Processing (processRequest)
- Load config: Fetch current configuration (hub-sourced or local)
- Parse URL: Extract coin prefix, route type (html/api/explorer), method, query, and type from the path
- Validate coin: Check coin is in
COIN_SUPPORTED; if not, return 503. Check coin is inCOIN_AVAILABLE; if not, redirect to coin-unavailable page. A configured coin whose newest indexed block has aged pastEXPLORER_TIP_MAX_AGE_Sis treated the same way, so a frozen replica refuses the read rather than serving stale data as live;/{COIN}/api/statusis exempt, since that is the endpoint an operator reads to see the freeze - Match route: Find the matching URL pattern in the route table
- Build config object: Populate method, search query, type, pagination parameters, and offset data
- Execute query: Call the corresponding database method (e.g.,
db.getSends(config)) - Format response: Apply
getPagingDataResults()to format data for the response type
flowchart TD
A["Load config<br>(hub-sourced or local)"]
B["Parse URL<br>(coin prefix, route type, method, query, type)"]
C["Validate coin<br>(COIN_SUPPORTED / COIN_AVAILABLE)"]
D["Match route<br>(find pattern in route table)"]
E["Execute query<br>(call db.method(config))"]
F["Format response<br>(getPagingDataResults)"]
A --> B --> C --> D --> E --> F
4. Response Formatting
API responses return full JSON objects with alphabetically-sorted keys:
{
"data": [ { "tick": "TOKEN", "amount": "100", ... }, ... ],
"total": 42,
"runtime": "15ms"
}
Explorer responses return DataTables-compatible arrays for the web UI:
{
"recordsTotal": 42,
"recordsFiltered": 42,
"data": [
[1, "field1|field2|field3", 100],
...
],
"runtime": "12ms"
}
Explorer data uses pipe-delimited strings for compact transmission to the frontend DataTables library.
5. Pagination
Two pagination modes are supported:
API pagination (SQL OFFSET/LIMIT):
page: Page number (1-based)limit: Results per page (capped per method; default max 100, getBalances/getHolders max 500)sortorder:ASCorDESCstart: Row offset (alternative to page)length: Row count (alternative to limit)
Explorer pagination (cursor-based):
action: Paging direction:first,last,next,prevoffset: Current cursor position (action_index or block_index)length: Records per page
SPV Light-Client Proof Server
The ProofServer class (src/proofServer.js) serves read-only Merkle proofs for the SPV light-client protocol (Phase 3, spec §8.1). It is instantiated by XChainExplorer on startup and handles four proof endpoint families:
GET /{COIN}/api/proof/balance/:address/:tick - SMT balance inclusion / non-inclusion proof
GET /{COIN}/api/proof/action/:actionIndex - Per-block fixed-Merkle inclusion proof
GET /{COIN}/api/proof/validator-set - Stake-weight SMT proofs (BTC-only)
GET /{COIN}/api/proof/contract-state/:idx/:key - Reserved; returns 501 in state_root_version 1
GET /{COIN}/api/checkpoints/range - Forward-ordered checkpoint slice for light-client sync
All proofs are derived from the indexer DB’s state_tree_nodes and state_tree_roots tables, which are NOT replicated by xchain-sync. The proof server checks that its local tree assembles to the same root as the signed checkpoint before returning any proof; if they disagree (server bug or divergence), it returns an error rather than a proof the client cannot verify.
The cryptographic primitives used are in src/merkle.js, which is vendored byte-identically across xchain-indexer, xchain-explorer, and xchain-sdk so that a proof produced here verifies under merkle.verifyCompressedSmtProof (balance/validator) or merkle.verifyFixedMerkleProof (action) in the SDK.
See API.md for the full request/response shapes and error codes.
WebSocket Server
The explorer provides a real-time event streaming API via WebSockets. Four modules in src/ws/ handle the lifecycle:
src/ws/
├── WebSocketServer.js # Connection handling, upgrade, WELCOME, message routing
├── ChannelManager.js # Subscription tracking with filters (types, statuses, ticks, etc.)
├── ChangeDetector.js # Polls DB for new blocks/actions, emits lifecycle events
└── Broadcaster.js # Routes events to subscribed clients through filter pipeline
Data flow:
flowchart TD
CD["ChangeDetector polls<br>MAX(block_index) / MAX(action_index)"]
DIFF["diff detected"]
FETCH["fetch new rows"]
EMIT["emit block / action / lifecycle_event / entity_update"]
BC["Broadcaster evaluates per-client filters<br>(types → statuses → ticks)"]
PROJ["apply fields projection"]
SEND["send to matching clients"]
CD --> DIFF --> FETCH --> EMIT --> BC --> PROJ --> SEND
The WebSocket server attaches to the same HTTP/HTTPS servers as Express via the upgrade event. No additional port. Clients connect to /{COIN}/api/websocket.
See WEBSOCKET.md for the full API reference.
Database Layer
The Database class (src/db.js, ~9,400 lines) is the largest component. Key patterns:
Query Building
All data methods follow a consistent pattern:
- Method dispatch:
getData(config)callsgetQuery(config)which dispatches to the appropriateget*method - WHERE clause:
getQueryWhereSql(config)builds parameterized WHERE conditions based on method and search type - Offset handling:
getQueryOffsetSql(config)adds cursor-based pagination for Explorer queries - Execution: Queries execute against the connection pool and return
[data, args, count]
Caching
- Address ID cache: LRU cache for
index_addresseslookups (avoids repeated joins) - Tick ID cache: LRU cache for
index_tickerslookups - Action data cache: LRU cache for immutable action records (action data never changes once written)
Connection Management
- Connection pooling via the
mariadbnpm package - Automatic reconnection on connection loss
- Separate connections for Indexer and Decoder databases
Coin Prefix Mapping
The explorer uses coin prefixes to route requests to the correct database:
| Network | Prefix | Database |
|---|---|---|
| Bitcoin mainnet | BTC |
XChain_BTC_Mainnet_Indexer |
| Bitcoin testnet | TBTC |
XChain_BTC_Testnet_Indexer |
| Bitcoin regtest | RBTC |
XChain_BTC_Regtest_Indexer |
| Litecoin mainnet | LTC |
XChain_LTC_Mainnet_Indexer |
| Litecoin testnet | TLTC |
XChain_LTC_Testnet_Indexer |
| Litecoin regtest | RLTC |
XChain_LTC_Regtest_Indexer |
| Dogecoin mainnet | DOGE |
XChain_DOGE_Mainnet_Indexer |
| Dogecoin testnet | TDOGE |
XChain_DOGE_Testnet_Indexer |
| Dogecoin regtest | RDOGE |
XChain_DOGE_Regtest_Indexer |
A single explorer instance can serve multiple coins and networks simultaneously, routing each request to the appropriate database based on the coin prefix in the URL path.
Copyright © 2025–2026 Dankest, LLC
Based on XChain Platform by Dankest, LLC – https://dankest.llc
Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-or-later) with a commercial license available for proprietary use.
You may use, modify, and distribute this material under the terms of the License. See LICENSE and NOTICE for full terms. See the licensing overview.