Skip to content

nagor2/blockpulse

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

2 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

⚑ Blockpulse

Node.js Redis Ethereum Docker License

Ethereum contract watcher with Redis cache invalidation.

Blockpulse listens to new blocks, detects transactions to your contracts, and instantly deletes the affected Redis keys β€” so your next request always hits the RPC with fresh data. Cache TTL can be infinite; invalidation is event-driven, not time-based.


πŸ’‘ The Key Insight

Most backends cache contract reads with a 60-second TTL: stale for 60 seconds by design.

Blockpulse flips the model:

Without Blockpulse                  With Blockpulse
──────────────────────              ────────────────────────────
cache(balance, TTL=60s)             cache(balance, TTL=∞)
β†’ stale for up to 60s               β†’ stale only between tx and next block
β†’ RPC call every 60s regardless     β†’ RPC call only when state actually changed
Scenario 60s TTL Blockpulse (TTL=0)
No activity, 1h 60 RPC calls 0 RPC calls
1 tx per minute 60 RPC calls 1 RPC call
10 tx per second 60 RPC calls 10 RPC calls

πŸ—οΈ Architecture

  Ethereum Node
  (WebSocket / HTTP)
        β”‚
        β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     new block / tx
  β”‚  Blockpulse β”‚ ──────────────────────────► Redis DEL
  β”‚   watcher   β”‚                             (configured key patterns)
  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ Health API  β”‚  :3002
  β”‚  REST + WS  β”‚  /health Β· /api/call Β· /api/events Β· /api/batch
  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
         β”‚
         β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚    Redis     β”‚  TTL=0, invalidated on tx
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

✨ Features

  • πŸ”΄ Event-driven invalidation β€” Redis keys deleted on every matching transaction; no fixed TTL needed
  • πŸ“œ Event & transaction indexing β€” stores decoded logs and tx history per contract in Redis
  • 🌐 REST API β€” /api/call, /api/batch, /api/events, /api/transactions, /api/eth/getBalance
  • πŸ“‘ Historical sync β€” backfills past events via Etherscan-compatible API on startup
  • πŸ”— Cache dependencies β€” invalidate contract B's keys whenever contract A changes
  • πŸ” Event dependencies β€” re-index contract B's events when contract A triggers them (e.g. DEX swaps β†’ token Transfer)
  • πŸ” fullScan mode β€” scan every block for token transfers regardless of to address (DEX-transferred tokens)
  • 🎯 Pool ID filtering β€” index only specific Uniswap V4 pool IDs
  • ⛓️ Multi-chain β€” Ethereum, Polygon, Arbitrum, Base, or any EVM chain via CHAIN_ID
  • πŸ’“ WebSocket health broadcast β€” real-time status stream on port HEALTH_BROADCAST_PORT+1

πŸš€ Quick Start

Docker Compose

cp .env.example .env
cp config/config.example.js config/config.js
# edit .env and config/config.js

docker compose up
# health: http://localhost:3002/health

Native

npm install
cp .env.example .env
cp config/config.example.js config/config.js
# edit .env and config/config.js

npm start

βš™οΈ Configuration

Environment variables (.env)

Variable Required Default Description
REDIS_URL βœ… β€” Redis connection URL
RPC_WS_URL βœ… β€” WebSocket RPC endpoint
RPC_HTTP_URL βœ… β€” HTTP RPC endpoint
CHAIN_ID β€” 1 Chain ID (1=mainnet, 137=polygon, 42161=arbitrum, 8453=base…)
ETHERSCAN_API_KEY β€” β€” For historical sync
ETHERSCAN_API_URL β€” etherscan.io Etherscan-compatible API URL
START_BLOCK β€” 0 Starting block for historical sync
HEALTH_BROADCAST_PORT β€” 3002 HTTP API port (WS is port+1)
LOG_LEVEL β€” info debug / info / warn / error
USE_WS β€” β€” Set to 1 to force WebSocket mode (default: HTTP polling)

Contract config (config/config.js)

module.exports = {
  contracts: [
    {
      address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
      name: "usdc",
      abi: require("./abis/usdc.json"),       // optional: enables /api/call + event decoding
      events: ["Transfer", "Approval"],        // optional: events to index
      fullScan: false,                         // true = scan every block (for DEX-transferred tokens)
      cacheKeys: [                             // Redis patterns to delete on any tx to this contract
        "myapp:balance:*",
        "myapp:token:usdc:*"
      ]
    }
  ],

  // Invalidate contract B's keys whenever contract A changes
  cacheDependencies: {
    "myPool": ["usdc"]
  },

  // Re-index contract B's events whenever contract A changes
  // (e.g. DEX swap triggers token Transfer on a different contract)
  eventDependencies: {
    "myPool": ["usdc"]
  },

  settings: {
    blockConfirmations: 1,     // wait N blocks before processing
    cacheTTL: 0                // 0 = no TTL, pure event-driven invalidation
  }
};

🌐 REST API

All endpoints are on http://localhost:${HEALTH_BROADCAST_PORT} (default 3002).

Health & status

Method Path Description
GET /health Liveness β€” always 200; includes Redis stats
GET /ready Readiness β€” 503 if Redis not connected
GET /api/contracts List all watched contracts

Contract calls (cached)

Method Path Description
GET /api/call/:contract/:method?args=[...] Call a contract method, result cached in Redis
GET /api/call/:contract/:method?noCache=1 Bypass cache (time-dependent methods)
POST /api/batch Multiple calls in one request
GET /api/eth/getBalance?address=0x... ETH balance, cached

Indexed data

Method Path Description
GET /api/events/:address?event=Transfer&page=1&limit=25 Decoded event logs
GET /api/transactions/:address?page=1&limit=25 Transaction history

Cache management

Method Path Description
POST /api/renewCache/:name Invalidate + re-index a contract
POST /api/invalidateCache/:name Invalidate only (no re-index)
POST /api/resync Full re-index from START_BLOCK

Batch call example

curl -X POST http://localhost:3002/api/batch \
  -H "Content-Type: application/json" \
  -d '[
    {"contract": "usdc", "method": "totalSupply"},
    {"contract": "usdc", "method": "balanceOf", "args": ["0x..."]},
    {"contract": "usdc", "method": "decimals", "noCache": false}
  ]'

πŸ’“ WebSocket Health Stream

Connect to ws://localhost:3003 (port+1) to receive real-time health broadcasts:

const ws = new WebSocket('ws://localhost:3003');
ws.onmessage = (e) => console.log(JSON.parse(e.data));
// { status, blockNumber, watchedContracts, uptime, redis, ... }

🐳 Docker

FROM node:20-alpine
# ...
HEALTHCHECK CMD node -e "require('http').get('http://localhost:${HEALTH_BROADCAST_PORT:-3002}/health', ...)"

The container exposes both the HTTP API port and the WebSocket port (port+1). Mount your config/config.js as a volume or bake it into a custom image.


πŸ“„ License

MIT β€” free for commercial use. See LICENSE.

About

Ethereum contract watcher with Redis cache invalidation

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors