diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e9268e5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules/ +db/ +tmp/ +.git/ +.gitignore +settings.json +*.log +explorerdb_dump.7z +docker-compose.yml +.env +.env.* +!.env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b383df0 --- /dev/null +++ b/.env.example @@ -0,0 +1,30 @@ +# ─── Coin ──────────────────────────────────────────────────────────────────── +COIN_NAME=BitcoinPurple +COIN_SYMBOL=BTCP +# POW, POS, or Hybrid +COIN_DIFFICULTY=POW + +# ─── MongoDB (internal to this stack) ──────────────────────────────────────── +MONGO_HOST=mongodb +MONGO_PORT=27017 +MONGO_USER=eiquidus +MONGO_PASSWORD=change-me +MONGO_DATABASE=explorerdb + +# ─── Wallet RPC ─────────────────────────────────────────────────────────────── +# bitcoinpurpled container is reachable on the shared 'purple' Docker network +WALLET_RPC_HOST=bitcoinpurpled +WALLET_RPC_PORT=13495 +# Copy rpcuser / rpcpassword values from ~/.bitcoinpurple/bitcoinpurple.conf +WALLET_RPC_USER= +WALLET_RPC_PASS= + +# ─── Explorer web ──────────────────────────────────────────────────────────── +# Host port the explorer is published on +EXPLORER_PORT=3001 +# Bootswatch theme: Cerulean, Cosmo, Cyborg, Darkly, Flatly, Slate, Solar, ... +EXPLORER_THEME=Darkly + +# ─── Sync intervals (seconds) ──────────────────────────────────────────────── +SYNC_BLOCKS_INTERVAL=120 +SYNC_PEERS_INTERVAL=300 diff --git a/.gitignore b/.gitignore index 3d45c7c..4455b0a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ logs pids *.pid *.seed +db/ # Directory for instrumented libs generated by jscoverage/JSCover lib-cov @@ -34,4 +35,4 @@ settings.json .DS_Store -package-lock.json \ No newline at end of file +package-lock.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b571a5d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,97 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**eIquidus** — a Node.js/Express blockchain block explorer for altcoins using the Bitcoin RPC protocol. It connects to a coin wallet via JSON-RPC, syncs blocks/transactions into MongoDB, and serves a web UI. + +- Node.js ≥ 20.9.0, MongoDB ≥ 7.0.2 +- Express v5, Pug templates, Mongoose ODM, Jasmine tests +- Configuration lives entirely in `settings.json` (created from `settings.json.template`) + +## Common Commands + +```bash +# Run tests +npm test # jasmine test/*Spec.js + +# Start the explorer +npm start # cluster mode (1 worker per CPU) +npm run start-instance # single instance (dev/testing) +npm run start-pm2 # production via PM2 + +# Blockchain sync (run as separate processes, not part of the web server) +npm run sync-blocks # sync new blocks from wallet +npm run sync-markets # fetch exchange market data +npm run sync-peers # discover network peers +npm run sync-masternodes # update masternode list +npm run check-blocks # verify block integrity + +# Database maintenance +npm run reindex # full reindex from wallet RPC +npm run create-backup # backup MongoDB +npm run restore-backup # restore MongoDB +npm run delete-database # wipe all explorer data + +# CSS compilation (SASS → CSS in public/css/) +node scripts/compile_css.js +``` + +## Architecture + +### Request Flow + +``` +HTTP request + → app.js (middleware: CORS, rate limiting, auth, TLS redirect) + → routes/index.js (all web & API routes) + → lib/database.js (Mongoose queries against models/) + → lib/explorer.js (data transformation utilities) + → views/*.pug (rendered response) +``` + +For live wallet data, routes call `lib/nodeapi.js` which wraps `lib/jsonrpc.js` for RPC calls. + +### Sync Flow + +``` +npm run sync-blocks + → scripts/sync.js (orchestrator, uses worker threads) + → lib/block_sync.js (parallel block fetching & processing) + → lib/database.js (bulk writes to MongoDB) +``` + +Block sync is multi-threaded (configurable parallelism). It has elastic stack-size handling for overflow errors. **The web server and sync scripts are separate processes** — sync is triggered via cron, not by the web server. + +### Key Files + +| File | Purpose | +|---|---| +| `app.js` | Express setup: plugin loader, CORS, ACL builder, TLS redirect | +| `lib/settings.js` | Parses and validates `settings.json`; all config access goes through here | +| `lib/database.js` | All MongoDB queries; wraps Mongoose models | +| `lib/explorer.js` | Pure utility functions for formatting block/tx/address data | +| `lib/block_sync.js` | Multi-threaded block synchronization engine | +| `lib/nodeapi.js` | Wallet RPC wrapper | +| `routes/index.js` | All Express routes (web pages + REST API) | +| `scripts/sync.js` | Entry point for `npm run sync-*` commands | +| `models/` | 12 Mongoose schemas: `tx`, `address`, `addresstx`, `stats`, `richlist`, `masternode`, `peers`, `markets`, `orphans`, `heavy`, `networkhistory`, `claimaddress` | +| `lib/markets/` | Per-exchange integrations (altmarkets, dextrade, dexomy, freiexchange, nonkyc, poloniex, xeggex, yobit) | + +### Configuration + +`settings.json` (gitignored, based on `settings.json.template`) controls everything: DB credentials, wallet RPC, port, TLS, themes, enabled pages, market exchanges, API access. Read through `lib/settings.js` to understand available options and their defaults. + +### Plugin System + +Early-stage plugin system. Plugins are loaded in `app.js` from the `plugins/` directory. Currently a placeholder only. + +### Testing + +Tests are in `test/` using Jasmine: +- `explorerSpec.js` — tests for `lib/explorer.js` utilities +- `marketSpec.js` — tests for market helper functions +- `testData.js` — shared fixtures + +There is no linting configured. No TypeScript. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8c398d4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM node:20-alpine + +# gettext provides envsubst for settings.json template rendering +RUN apk add --no-cache gettext bash + +WORKDIR /app + +COPY package*.json ./ +RUN npm install --omit=dev + +COPY . . + +# Compile SCSS → CSS (prestart.js also connects to MongoDB which isn't available at build time) +RUN node ./scripts/compile_css.js + +RUN chmod +x /app/docker/entrypoint.sh && mkdir -p /app/tmp + +EXPOSE 3001 + +ENTRYPOINT ["/app/docker/entrypoint.sh"] +CMD ["web"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..14bcef3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,102 @@ +networks: + purple: + external: true # shared with purple-stack (bitcoinpurpled, electrumx) + explorer-internal: # isolates MongoDB from the outside world + name: purple-explorer-internal + +services: + mongodb: + image: mongo:7 + container_name: purple-explorer-mongodb + restart: unless-stopped + networks: + - explorer-internal + volumes: + # Keep MongoDB data in this repository's ./db directory. + # Do not switch this back to a named Docker volume or the explorer will + # stop using the already-synced local database. + - type: bind + source: ./db + target: /data/db + bind: + create_host_path: true + - ./docker/mongo-init.sh:/docker-entrypoint-initdb.d/init.sh:ro + environment: + # Root admin (used only by the init script to bootstrap the app user) + MONGO_INITDB_ROOT_USERNAME: mongoadmin + MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD} + # Passed into mongo-init.sh to create the explorer app user + MONGO_APP_USER: ${MONGO_USER} + MONGO_APP_PASS: ${MONGO_PASSWORD} + MONGO_APP_DB: ${MONGO_DATABASE:-explorerdb} + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + + explorer: + build: . + image: purple-explorer:local + container_name: purple-explorer + restart: unless-stopped + depends_on: + - mongodb + networks: + - purple # reach bitcoinpurpled:13495 for RPC + - explorer-internal + ports: + - "0.0.0.0:${EXPLORER_PORT:-3001}:3001" + env_file: .env + command: web + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + + sync-blocks: + image: purple-explorer:local + container_name: purple-explorer-sync-blocks + restart: unless-stopped + depends_on: + - mongodb + - explorer # ensures the image is built before this runs + networks: + - purple # reach bitcoinpurpled RPC + - explorer-internal + env_file: .env + command: sync-blocks + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + + # sync-markets: disabled until exchanges are configured in docker/settings.json.tmpl + # To enable: uncomment, configure markets_page.exchanges in the template, then redeploy. + # sync-markets: + # image: purple-explorer:local + # container_name: purple-explorer-sync-markets + # restart: unless-stopped + # depends_on: [mongodb] + # networks: [explorer-internal] + # env_file: .env + # command: sync-markets + + sync-peers: + image: purple-explorer:local + container_name: purple-explorer-sync-peers + restart: unless-stopped + depends_on: + - mongodb + networks: + - purple # reach bitcoinpurpled for peer info + - explorer-internal + env_file: .env + command: sync-peers + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..66c2276 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,58 @@ +#!/bin/sh +set -e + +# Generate settings.json from template using environment variables +envsubst < /app/docker/settings.json.tmpl > /app/settings.json + +MODE="${1:-web}" + +case "$MODE" in + web) + # Initialize DB collections/stats document before starting workers + # (equivalent to what prestart.js does before launching pm2/forever) + node -e " + const db = require('./lib/database'); + db.connect(null, function() { + db.initialize_data_startup(function() { + process.exit(0); + }); + }); + " + exec node --stack-size=10000 ./bin/cluster + ;; + + sync-blocks) + echo "[sync-blocks] Starting block sync loop (interval: ${SYNC_BLOCKS_INTERVAL:-120}s)" + while true; do + node --stack-size=10000 ./scripts/sync.js index update || true + sleep "${SYNC_BLOCKS_INTERVAL:-120}" + done + ;; + + sync-markets) + echo "[sync-markets] Starting market sync (runs in internal loop)" + exec node --stack-size=10000 ./scripts/sync.js market + ;; + + sync-peers) + echo "[sync-peers] Starting peer sync loop (interval: ${SYNC_PEERS_INTERVAL:-300}s)" + while true; do + node --stack-size=10000 ./scripts/sync.js peers || true + sleep "${SYNC_PEERS_INTERVAL:-300}" + done + ;; + + reindex) + echo "[reindex] Running full reindex..." + exec node --stack-size=10000 ./scripts/sync.js index reindex + ;; + + check-blocks) + echo "[check-blocks] Checking for missing transactions..." + exec node --stack-size=10000 ./scripts/sync.js index check + ;; + + *) + exec "$@" + ;; +esac diff --git a/docker/mongo-init.sh b/docker/mongo-init.sh new file mode 100644 index 0000000..f2bad3e --- /dev/null +++ b/docker/mongo-init.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# Creates the explorer app user in explorerdb. +# Runs once on first container start (only when the data volume is empty). +mongosh -u "$MONGO_INITDB_ROOT_USERNAME" -p "$MONGO_INITDB_ROOT_PASSWORD" --authenticationDatabase admin <