oracle-listener
v0.2.1
Published
Config-driven EVM oracle event listener library
Readme
oracle-listener
A lightweight, config-driven library for listening to EVM smart contract events from a Node.js server.
You provide the contract address, ABI, event name, and a handler function. The library handles all the plumbing: block polling, catch-up on restart, deduplication, and reorg protection.
Install
npm install oracle-listenerQuick start
import { OracleListener } from 'oracle-listener';
const listener = new OracleListener({
rpcUrl: 'http://localhost:8545',
contractAddress: '0xYourContractAddress',
abi: YourContractABI,
eventName: 'YourEventName',
onEvent: async (event) => {
console.log(event.args);
// your logic here
},
});
await listener.start();Persist state across restarts
By default the library uses an in-memory store, which means it starts from the latest block on every restart. Use FileStore to resume from where it left off:
import { OracleListener, FileStore } from 'oracle-listener';
const listener = new OracleListener({
rpcUrl: 'http://localhost:8545',
contractAddress: '0xYourContractAddress',
abi: YourContractABI,
eventName: 'YourEventName',
store: new FileStore('./last-block.json'),
onEvent: async (event) => {
// any events missed while your server was down are replayed here first
await handleEvent(event.args);
},
});
await listener.start();Full config reference
const listener = new OracleListener({
// required
rpcUrl: 'http://localhost:8545', // RPC endpoint (HTTP or WS)
contractAddress: '0xABC...', // contract to watch
abi: [...], // full contract ABI
eventName: 'OracleRequested', // one event, or array for multiple
onEvent: async (event) => { ... }, // called for every new event
// optional
pollInterval: 5000, // how often to poll, in ms (default: 5000)
confirmationBlocks: 2, // blocks to wait before processing (default: 2)
startBlock: 1000000n, // start block if no saved state (default: 'latest')
batchSize: 1000, // max blocks to fetch per poll (default: 1000)
store: new FileStore(...), // where to save progress (default: InMemoryStore)
onError: (err) => { ... }, // called on RPC errors (default: console.error)
// reconnect (all optional)
reconnect: true, // auto-reconnect on RPC failure (default: true)
reconnectInitialDelay: 1000, // first retry delay in ms (default: 1000)
reconnectMaxDelay: 60000, // max retry delay in ms (default: 60000)
reconnectMaxAttempts: 0, // max attempts, 0 = unlimited (default: 0)
onReconnect: () => { ... }, // called when RPC connection is restored
});The onEvent callback
The event object is a viem Log with decoded args:
onEvent: async (event) => {
event.args // decoded event arguments (cast to your expected shape)
event.blockNumber // bigint
event.transactionHash
event.logIndex
event.address // contract address
}Stores
The library uses a BlockStore to persist the last processed block. Two built-in options:
| Store | Import | Persistence | Use when |
|---|---|---|---|
| InMemoryStore | default | None — resets on restart | Dev/testing |
| FileStore | oracle-listener | JSON file on disk | Production, no external deps |
| MongoStore | oracle-listener | MongoDB collection | Production, existing Mongo setup |
MongoDB store
MongoStore is built in — pass it a MongoDB Collection from your existing connection:
import { OracleListener, MongoStore } from 'oracle-listener';
import { MongoClient } from 'mongodb';
const client = new MongoClient(process.env.MONGODB_URI ?? 'mongodb://localhost:27017');
await client.connect();
const collection = client.db('myapp').collection('oracle_state');
const listener = new OracleListener({
// ...
store: new MongoStore(collection),
// optional: custom document key (default: 'lastBlock')
// store: new MongoStore(collection, 'myListenerBlock'),
});Requires mongodb >= 4.0.0 as a peer dependency (npm install mongodb).
Bring your own store
Implement the BlockStore interface to plug in any other storage:
import type { BlockStore } from 'oracle-listener';
class RedisStore implements BlockStore {
async getLastBlock(): Promise<bigint | null> {
const val = await redis.get('lastBlock');
return val ? BigInt(val) : null;
}
async setLastBlock(block: bigint): Promise<void> {
await redis.set('lastBlock', block.toString());
}
}Listening to multiple events
Pass an array to eventName to listen for more than one event on the same contract in a single poll loop:
const listener = new OracleListener({
rpcUrl: 'http://localhost:8545',
contractAddress: '0xABC...',
abi: MyContractABI,
eventName: ['OracleRequested', 'OracleFulfilled'],
onEvent: async (event) => {
if (event.eventName === 'OracleRequested') {
// handle request
} else if (event.eventName === 'OracleFulfilled') {
// handle fulfillment
}
},
});Stop the listener
await listener.start();
// later, e.g. on SIGTERM
listener.stop();Stats
Call listener.getStats() at any time to get the current state. Plug it into your existing server's health/status route:
// Express example
app.get('/oracle/stats', (req, res) => {
const stats = listener.getStats();
res.json({
...stats,
lastProcessedBlock: stats.lastProcessedBlock?.toString(), // bigint → string
});
});Response shape:
{
"status": "running",
"lastProcessedBlock": "31242",
"failureCount": 0,
"rpcUrl": "http://localhost:8545",
"contractAddress": "0xABC...",
"eventName": "OracleRequested",
"uptime": 42
}status is one of "running", "reconnecting" (RPC is down, backoff active), or "stopped".
Handling RPC failures
By default the library reconnects automatically when the RPC goes down. On each failure it calls onError, then waits before retrying with exponential backoff:
failure 1 → retry in 1s
failure 2 → retry in 2s
failure 3 → retry in 4s
failure 4 → retry in 8s
failure 5 → retry in 16s
failure 6 → retry in 32s
failure 7+ → retry in 60s (capped at reconnectMaxDelay)When the RPC comes back, the library resumes from lastProcessedBlock — no events are missed.
const listener = new OracleListener({
// ...
onError: (err) => {
console.error('RPC error:', err.message);
},
onReconnect: () => {
console.log('RPC restored, resuming...');
},
});To disable auto-reconnect and handle retries yourself, set reconnect: false.
Requirements
- Node.js >= 18
- An EVM-compatible RPC endpoint (Ethereum, Besu, Anvil, Hardhat, etc.)
How it works
On every poll interval:
- Fetches the current block number
- Subtracts
confirmationBlocksto get a "safe" block (reorg protection) - Fetches all matching event logs from
lastProcessedBlock + 1to the safe block, in batches - For each log: checks deduplication, calls
onEvent, moves on - Saves the safe block to the store
On startup, it loads the last saved block from the store and replays any events it missed while it was down.
