npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xdc.org/interaction-detector

v1.0.7

Published

Standalone TypeScript library for detecting all on-chain contract interactions — events, direct calls, internal calls, and transaction tracing — on XDC and EVM-compatible chains.

Readme

XDC Interaction Detector

Standalone TypeScript library for detecting all on-chain contract interactions on XDC and EVM-compatible chains.

Combines: events + direct calls + internal calls + transaction tracing into a single unified detection engine.

Framework-agnostic. Zero runtime dependencies on any specific backend. Just detection — you decide what to do with the results.

Table of Contents


Features

  • Real-time event monitoring — WebSocket push + HTTP polling with automatic deduplication
  • Historical block scanning — Query any block range with auto-chunked fetching
  • Transaction tracing — Full call trees, state diffs, and balance changes via debug_traceTransaction
  • Explorer API integration — Direct & internal transaction collection via XDCScan / Etherscan-compatible APIs
  • XDC-first, EVM-compatible — Handles XDC's non-standard ABI encoding, xdc address prefix, and 100-block range limit
  • Pluggable checkpoints — Memory, file, or custom backends for restart persistence
  • Zero framework dependency — Works in any Node.js environment: Express, Fastify, serverless functions, CLI scripts, background workers

Installation

npm install @xdc.org/interaction-detector

Dependencies: ethers v6, ws — all installed automatically. HTTP calls use Node.js native fetch (requires Node ≥ 18).


Quick Start

import { ContractWatcher } from '@xdc.org/interaction-detector';

const watcher = new ContractWatcher({
  // RPC endpoints
  rpcUrl: 'https://rpc.xdc.network',
  wsUrl: 'wss://rpc.xdc.network/ws', // optional — enables real-time push
  chainId: 50, // XDC Mainnet

  // Contracts to monitor
  contracts: [
    {
      address: '0x0000000000000000000000000000000000000088',
      abi: ['event Vote(address indexed _voter, address indexed _candidate, uint256 _cap)'],
      name: 'XDCValidator',
    },
  ],

  // Explorer API — enables direct & internal call detection
  explorer: {
    apiUrl: 'https://api.etherscan.io/v2/api',
    apiKey: 'YOUR_ETHERSCAN_API_KEY', // optional — higher rate limits
    chainId: 50, // XDC Mainnet
    rateLimitPerSec: 5,
  },

  // Checkpoint — survive restarts
  checkpoint: { backend: 'file', path: './checkpoints' },
});

// Decoded contract events (Transfer, Swap, Vote, etc.)
watcher.on('event', event => {
  console.log(`${event.name} from ${event.contractName} at block ${event.blockNumber}`);
  console.log('  Args:', event.args);
});

// ALL interactions (events + direct + internal + delegate + static calls)
watcher.on('interaction', interaction => {
  console.log(`[${interaction.type}] tx ${interaction.txHash} (source: ${interaction.source})`);
});

await watcher.start();

Core Classes

1. ContractWatcher — Real-Time Monitoring

Watches one or more contract addresses for all interactions in real-time using three complementary detection paths:

| Path | Method | What it catches | Latency | | ------------ | --------------------------- | ----------------------------------- | ------------- | | WebSocket | eth_subscribe("logs") | Contract events | ~2 seconds | | HTTP Polling | eth_getLogs | Contract events (reliable fallback) | 15–30 seconds | | Explorer API | txlist + txlistinternal | Direct calls + internal calls | 30–60 seconds |

Creating a watcher:

import { ContractWatcher } from '@xdc.org/interaction-detector';

const watcher = new ContractWatcher({
  // ─── Required ───────────────────────────────────────────
  rpcUrl: 'https://rpc.xdc.network',
  contracts: [
    {
      address: '0xContractAddress',       // 0x or xdc prefix both work
      abi: [...],                          // optional — enables event decoding
      name: 'MyDeFiPool',                 // optional — human label
    },
    // Watch multiple contracts simultaneously
    { address: '0xAnotherContract', name: 'Governance' },
  ],

  // ─── Optional: WebSocket (real-time push) ───────────────
  wsUrl: 'wss://rpc.xdc.network/ws',
  chainId: 50,                            // default: 50 (XDC Mainnet)

  // ─── Optional: Explorer API (direct + internal calls) ───
  explorer: {
    apiUrl: 'https://api.etherscan.io/v2/api', // Etherscan v2 supports XDC
    apiKey: 'YOUR_ETHERSCAN_API_KEY',      // optional — get higher rate limits
    chainId: 50,                           // XDC Mainnet (required for Etherscan v2)
    rateLimitPerSec: 5,                    // default: 5 req/s
    pollIntervalMs: 60_000,               // how often to check explorer (default: 60s)
  },

  // ─── Optional: Polling tuning ───────────────────────────
  polling: {
    intervalMs: 15_000,                    // default: 30s
    maxBlockRange: 100,                    // XDC limit — don't change unless using another chain
    concurrency: 3,                        // parallel chunk fetches
  },

  // ─── Optional: WebSocket tuning ─────────────────────────
  ws: {
    enabled: true,                         // default: true
    reconnectDelayBaseMs: 5_000,
    reconnectDelayMaxMs: 30_000,
    maxReconnectAttempts: 20,
    heartbeatIntervalMs: 60_000,
    heartbeatTimeoutMs: 10_000,
  },

  // ─── Optional: Checkpoint persistence ───────────────────
  checkpoint: {
    backend: 'file',                       // 'memory' | 'file' | 'custom'
    path: './checkpoints',
  },

  // ─── Optional: Fallback RPCs ────────────────────────────
  fallbackRpcUrls: ['https://rpc1.xinfin.network'],

  // ─── Optional: Log level ────────────────────────────────
  logLevel: 'info',                        // 'debug' | 'info' | 'warn' | 'error' | 'silent'
});

Listening for events:

// ── Decoded contract events ──────────────────────────────────
watcher.on('event', event => {
  console.log(event.contract); // '0xcontractaddress'
  console.log(event.contractName); // 'MyDeFiPool'
  console.log(event.name); // 'Swap'
  console.log(event.args); // { sender: '0x...', amount0In: 1000n, ... }
  console.log(event.blockNumber); // 75123456
  console.log(event.txHash); // '0xabc...'
  console.log(event.logIndex); // 3
  console.log(event.timestamp); // 1711792800 (Unix seconds)
  console.log(event.signature); // 'Swap(address,uint256,uint256,address)'
  console.log(event.raw); // { topics: [...], data: '0x...' }
});

// ── ALL interactions (unified type) ──────────────────────────
watcher.on('interaction', interaction => {
  console.log(interaction.type); // 'event' | 'direct_call' | 'internal_call' | 'delegate_call' | 'static_call'
  console.log(interaction.source); // 'rpc_logs' | 'ws_logs' | 'explorer_txlist' | 'explorer_internal'
  console.log(interaction.txHash);
  console.log(interaction.from); // sender (when available)
  console.log(interaction.to); // contract address
  console.log(interaction.methodId); // '0xa9059cbb' (for direct/internal calls)
  console.log(interaction.methodName); // 'transfer(address,uint256)' (if ABI found)
  console.log(interaction.value); // native value in wei
  console.log(interaction.isError); // true if call reverted
});

// ── Raw logs (for contracts without ABI) ─────────────────────
watcher.on('log', log => {
  console.log(log.topics[0]); // event signature hash
  console.log(log.data); // raw encoded data
});

// ── Lifecycle events ─────────────────────────────────────────
watcher.on('connected', info => {
  console.log(`Connected via ${info.type}: ${info.url}`);
});

watcher.on('disconnected', info => {
  console.log(`Disconnected: ${info.reason}`);
});

watcher.on('checkpoint', blockNumber => {
  console.log(`Checkpoint saved at block ${blockNumber}`);
});

watcher.on('error', err => {
  console.error(`Error: ${err.message}`);
});

Starting and stopping:

// Start monitoring
await watcher.start();

// ... later, graceful shutdown
await watcher.stop();

Deduplication: Events detected by both WebSocket and polling are automatically deduplicated using txHash + logIndex composite keys. This prevents double-counting when both detection paths catch the same event.


2. BlockScanner — Historical Queries

Scans a block range for all events and interactions involving a contract. Automatically handles XDC's 100-block eth_getLogs limit with chunked parallel fetching.

Scanning for events:

import { BlockScanner } from '@xdc.org/interaction-detector';

const scanner = new BlockScanner({
  rpcUrl: 'https://rpc.xdc.network',
  maxBlockRange: 100, // default: 100 (XDC limit)
  concurrency: 3, // parallel chunk fetches
  logLevel: 'info',

  // Optional: explorer for direct + internal tx enrichment
  explorer: {
    apiUrl: 'https://api.etherscan.io/v2/api',
    apiKey: 'YOUR_ETHERSCAN_API_KEY',
    chainId: 50,
  },
});

// ── Scan for decoded events ──────────────────────────────────
const events = await scanner.getEvents({
  address: '0x0000000000000000000000000000000000000088',
  abi: [
    'event Vote(address indexed _voter, address indexed _candidate, uint256 _cap)',
    'event Unvote(address indexed _voter, address indexed _candidate, uint256 _cap)',
  ],
  fromBlock: 75_000_000,
  toBlock: 75_100_000,

  // Optional: filter by specific event name
  eventFilter: 'Vote',

  // Optional: progress callback for large scans
  onProgress: (processed, total) => {
    console.log(`${Math.round((processed / total) * 100)}% complete`);
  },
});

// events is DecodedEvent[]
for (const event of events) {
  console.log(`[block ${event.blockNumber}] ${event.name}`, event.args);
}

Scanning for all interactions (events + explorer txlist + txlistinternal):

const interactions = await scanner.getInteractions({
  address: '0x0000000000000000000000000000000000000088',
  abi: [...],
  fromBlock: 75_000_000,
  toBlock: 75_100_000,
});

// interactions is ContractInteraction[]
for (const i of interactions) {
  console.log(`[${i.type}] block ${i.blockNumber} tx ${i.txHash}`);
  if (i.type === 'event') console.log('  Event:', i.event?.name, i.event?.args);
  if (i.type === 'direct_call') console.log('  Method:', i.methodName);
  if (i.type === 'internal_call') console.log('  From:', i.from);
}

3. TransactionTracer — Deep Transaction Analysis

Traces a single transaction to extract the full execution story: call tree, state diffs, balance changes, and all events.

Note: Requires an RPC endpoint with the debug namespace enabled. For historical transactions, an archive node is required. For recent/current blocks, a regular full node works.

Full trace:

import { TransactionTracer } from '@xdc.org/interaction-detector';

const tracer = new TransactionTracer({
  rpcUrl: 'https://archive-rpc.xdc.network', // must support debug_traceTransaction
  timeoutMs: 120_000, // tracing can be slow on complex txs
  logLevel: 'info',
});

// Register ABIs for method name decoding in call trees
tracer.registerABI(
  '0xContractAddress',
  ['function swap(uint256,uint256,address,bytes)', 'function transfer(address,uint256)'],
  'MyDEX',
);

const result = await tracer.trace('0xTransactionHash');

Using the trace result:

// ── Call Tree ────────────────────────────────────────────────
// Nested structure showing every CALL, STATICCALL, DELEGATECALL
console.log(result.callTree.type); // 'CALL'
console.log(result.callTree.from); // '0xSender'
console.log(result.callTree.to); // '0xContract'
console.log(result.callTree.method); // 'swap(uint256,uint256,address,bytes)' (if ABI registered)
console.log(result.callTree.value); // '0x0' (native value)
console.log(result.callTree.calls); // CallTreeNode[] — nested sub-calls
// Example nested call:
//   CALL 0xRouter → swap(...)
//     CALL 0xPool → mint(...)
//       STATICCALL 0xOracle → getPrice()
//       CALL 0xTokenA → transfer(...)

// ── State Diffs ──────────────────────────────────────────────
// Storage slot changes (before/after for each modified slot)
for (const diff of result.stateDiffs) {
  console.log(`${diff.contract} slot ${diff.slot}`);
  console.log(`  before: ${diff.before}`);
  console.log(`  after:  ${diff.after}`);
}

// ── Balance Changes ──────────────────────────────────────────
// Native token balance changes per address
for (const change of result.balanceChanges) {
  console.log(`${change.address}: ${change.delta} wei (${change.token})`);
}

// ── Events ───────────────────────────────────────────────────
// All events emitted during execution (decoded if ABI registered)
for (const event of result.events) {
  console.log(`${event.name} from ${event.contract}`, event.args);
}

// ── Metadata ─────────────────────────────────────────────────
console.log(result.gasUsed); // 234567
console.log(result.involvedContracts); // ['0x...', '0x...', '0x...']
console.log(result.blockNumber); // 75123456

Partial traces (lighter weight):

// Just the call tree (no state diffs)
const callTree = await tracer.traceCallTree('0xTxHash');

// Just the state diffs + balance changes (no call tree)
const { stateDiffs, balanceChanges } = await tracer.traceStateDiffs('0xTxHash');

Call tree utilities:

import { flattenCallTree, findCallsTo, extractInvolvedContracts } from '@xdc.org/interaction-detector';

// Flatten the nested tree into a linear array
const allCalls = flattenCallTree(result.callTree);
console.log(`Total calls in execution: ${allCalls.length}`);

// Find all calls targeting a specific contract
const tokenCalls = findCallsTo(result.callTree, '0xTokenAddress');

// Get all unique addresses involved
const addresses = extractInvolvedContracts(result.callTree);

LogPoller — Standalone Log Fetching

The LogPoller is used internally by ContractWatcher and BlockScanner, but is also exported for standalone use when you need direct control over eth_getLogs fetching with automatic chunking and concurrency.

import { LogPoller, RpcClient } from '@xdc.org/interaction-detector';
import type { FetchLogsResult } from '@xdc.org/interaction-detector';

const rpc = new RpcClient('https://rpc.xdc.network');
const poller = new LogPoller(
  rpc,
  ['0x0000000000000000000000000000000000000088'], // addresses to monitor
  { maxBlockRange: 100, concurrency: 3 }, // optional PollingConfig
  'info', // optional log level
);

// ── Basic usage — returns RawLog[] ──────────────────────────
const logs = await poller.fetchLogs(75_000_000, 75_001_000);
console.log(`Fetched ${logs.length} logs`);

// ── With failure tracking — returns FetchLogsResult ─────────
// Tracks which block ranges failed, so you can retry or alert
const result: FetchLogsResult = await poller.fetchLogs(75_000_000, 75_001_000, { trackFailures: true });

console.log(`Fetched ${result.logs.length} logs`);
if (result.failedRanges.length > 0) {
  console.warn('Some ranges failed:');
  for (const range of result.failedRanges) {
    console.warn(`  blocks ${range.from}–${range.to}: ${range.error}`);
  }
}

FetchLogsResult type:

interface FetchLogsResult {
  /** Successfully fetched logs */
  logs: RawLog[];
  /** Block ranges that failed to fetch (data may be missing for these ranges) */
  failedRanges: Array<{ from: number; to: number; error: string }>;
}

Note: The default fetchLogs(from, to) call (without { trackFailures: true }) returns RawLog[] directly for backward compatibility. Failed chunks return empty arrays silently — use the trackFailures option when you need visibility into partial failures.


Explorer API Client

Standalone Etherscan-compatible API client. Works with XDCScan, Etherscan v2, BSCScan, PolygonScan, and any Etherscan-compatible explorer.

import { ExplorerClient } from '@xdc.org/interaction-detector';

const explorer = new ExplorerClient({
  apiUrl: 'https://api.etherscan.io/v2/api', // Etherscan v2 — supports XDC + 80 chains
  // apiUrl: 'https://xdc.blocksscan.io/api', // XDCScan (alternative)
  // apiUrl: 'https://api.bscscan.com/api',   // BSCScan
  apiKey: 'YOUR_ETHERSCAN_API_KEY', // optional — higher rate limits
  chainId: 50, // XDC Mainnet (required for Etherscan v2)
  rateLimitPerSec: 5, // built-in token-bucket rate limiter
});

Available methods:

// ── Transaction lists ────────────────────────────────────────
// Get external transactions to/from a contract
const txs = await explorer.getTransactions('0xAddress', {
  startBlock: 75_000_000,
  endBlock: 75_100_000,
  sort: 'asc', // or 'desc'
});

// Get internal transactions (CALL, DELEGATECALL from other contracts)
const internalTxs = await explorer.getInternalTransactions('0xAddress', {
  startBlock: 75_000_000,
  endBlock: 75_100_000,
});

// Get internal transactions within a specific tx
const txInternals = await explorer.getInternalTransactionsByTx('0xTxHash');

// ── Events ───────────────────────────────────────────────────
// Get event logs with optional topic filter
const logs = await explorer.getLogs('0xAddress', {
  fromBlock: 75_000_000,
  toBlock: 75_100_000,
  topic0: '0xddf252ad...', // optional — filter by event signature
});

// ── Contract ABI ─────────────────────────────────────────────
// Auto-fetch verified contract ABI
const abi = await explorer.getContractABI('0xAddress');
if (abi) {
  console.log(`Fetched ABI with ${abi.length} entries`);
}

// ── Token transfers ──────────────────────────────────────────
const transfers = await explorer.getTokenTransfers('0xAddress', {
  startBlock: 75_000_000,
  endBlock: 75_100_000,
});

// ── Balance ──────────────────────────────────────────────────
const balance = await explorer.getBalance('0xAddress');

// ── Cleanup ──────────────────────────────────────────────────
explorer.destroy(); // Cleans up rate limiter timers

Explorer compatibility:

| Explorer | Base URL | Chain | Free Rate Limit | | ------------------- | --------------------------------- | -------------------------------- | --------------- | | Etherscan v2 ⭐ | https://api.etherscan.io/v2/api | XDC (50) + 80 chains via chainId | 5 req/s | | XDCScan | https://xdc.blocksscan.io/api | XDC Mainnet (50) | 5 req/s | | BSCScan | https://api.bscscan.com/api | BSC (56) | 5 req/s | | PolygonScan | https://api.polygonscan.com/api | Polygon (137) | 5 req/s | | Custom | Any Etherscan-compatible URL | Any EVM chain | Configurable |


Event Decoder & ABI Registry

The decoder handles both standard Solidity ABI encoding and XDC's non-standard encoding (where all params are packed into the data field).

import { AbiRegistry, EventDecoder } from '@xdc.org/interaction-detector';

// ── Register ABIs ────────────────────────────────────────────
const registry = new AbiRegistry();

// Register manually
registry.register(
  '0xContractAddress',
  [
    'event Transfer(address indexed from, address indexed to, uint256 value)',
    'event Approval(address indexed owner, address indexed spender, uint256 value)',
  ],
  'MyToken',
);

// Register from a JSON ABI array
registry.register('0xAnotherContract', require('./MyContract.json').abi, 'MyContract');

// Auto-fetch from block explorer (verified contracts only, requires API key)
const explorer = new ExplorerClient({
  apiUrl: 'https://api.etherscan.io/v2/api',
  apiKey: 'YOUR_ETHERSCAN_API_KEY',
  chainId: 50,
});
const success = await registry.registerFromExplorer('0xVerifiedContract', explorer, 'VerifiedToken');
console.log(success ? 'ABI fetched!' : 'Contract not verified');

// ── Query the registry ───────────────────────────────────────
registry.has('0xContractAddress'); // true
registry.getName('0xContractAddress'); // 'MyToken'
registry.getAddresses(); // ['0x...', '0x...']
registry.getEventName('0xddf252ad...'); // 'Transfer' (from any registered contract)

// ── Decode raw logs ──────────────────────────────────────────
const decoder = new EventDecoder(registry);

const decoded = decoder.decode(rawLog);
if (decoded) {
  console.log(decoded.name); // 'Transfer'
  console.log(decoded.args.from); // '0x...'
  console.log(decoded.args.to); // '0x...'
  console.log(decoded.args.value); // 1000000000000000000n
}

Checkpoint Persistence

Checkpoints let the watcher resume from where it left off after a restart.

| Backend | Storage | Survives Restart? | Best For | | -------- | -------------------------------- | ----------------- | ------------------------------ | | memory | JavaScript Map in RAM | ❌ No | Development, testing | | file | checkpoints.json on disk | ✅ Yes | Simple production deployments | | custom | Your own (Redis, SQL, InfluxDB…) | ✅ Yes | Distributed / production-grade |

Built-in Backends

import { MemoryCheckpoint, FileCheckpoint, createCheckpointBackend } from '@xdc.org/interaction-detector';

// ── Memory (development / testing) ───────────────────────────
// Stores in RAM only — lost when process stops
const memCp = new MemoryCheckpoint();
await memCp.save('watcher-key', 75_000_000);
await memCp.load('watcher-key'); // 75000000

// ── File (simple production) ─────────────────────────────────
// Auto-creates directory and file on first save
const fileCp = new FileCheckpoint('./checkpoints');
await fileCp.save('watcher-key', 75_000_000);
// Persists to ./checkpoints/checkpoints.json (relative to process.cwd())
// Survives process restarts

// ── Factory function ─────────────────────────────────────────
const cp = createCheckpointBackend({ backend: 'file', path: './data' });
const cp2 = createCheckpointBackend({ backend: 'memory' });

Custom Backends

Implement the CheckpointBackend interface — just two methods:

interface CheckpointBackend {
  save(key: string, blockNumber: number): Promise<void>;
  load(key: string): Promise<number | null>;
}

Redis:

checkpoint: {
  backend: 'custom',
  custom: {
    async save(key: string, blockNumber: number) {
      await redis.set(`checkpoint:${key}`, blockNumber);
    },
    async load(key: string) {
      const val = await redis.get(`checkpoint:${key}`);
      return val ? parseInt(val) : null;
    },
  },
},

PostgreSQL / MySQL:

checkpoint: {
  backend: 'custom',
  custom: {
    async save(key: string, blockNumber: number) {
      await pool.query(
        `INSERT INTO checkpoints (key, block_number) VALUES ($1, $2)
         ON CONFLICT (key) DO UPDATE SET block_number = $2`,
        [key, blockNumber],
      );
    },
    async load(key: string) {
      const result = await pool.query(
        'SELECT block_number FROM checkpoints WHERE key = $1',
        [key],
      );
      return result.rows[0]?.block_number ?? null;
    },
  },
},

InfluxDB:

checkpoint: {
  backend: 'custom',
  custom: {
    async save(key: string, blockNumber: number) {
      await influx.writePoints([{
        measurement: 'checkpoints',
        tags: { key },
        fields: { block_number: blockNumber },
      }]);
    },
    async load(key: string) {
      const result = await influx.query(
        `SELECT LAST(block_number) FROM checkpoints WHERE key = '${key}'`,
      );
      return result[0]?.block_number ?? null;
    },
  },
},

Recommendation: Use Redis or SQL for checkpoint storage. InfluxDB is better suited for storing the detected events/interactions as time-series data rather than simple key-value checkpoint state.


Utility Functions

import {
  // Address utilities
  normalizeAddress, // '0xABC...' or 'xdcABC...' → '0xabc...'
  toXdcAddress, // '0xabc...' → 'xdcabc...'
  toEthAddress, // 'xdcabc...' → '0xabc...'
  isAddress, // validate hex address (20 bytes)
  addressEqual, // compare addresses (case-insensitive, prefix-aware)

  // Formatting (precision-safe for large BigInt values)
  formatXDC, // bigint wei → '1.23M XDC' / '456.78K XDC'
  formatWei, // bigint wei → '1.23M' (generic, configurable decimals)
  parseHexOrDecimal, // '0x100' → 256, '256' → 256
  toHex, // 256 → '0x100'
  shortAddress, // '0x1234...abcd'
  sleep, // await sleep(1000)

  // Logger
  Logger, // new Logger('MyModule', 'info')
} from '@xdc.org/interaction-detector';

Address normalization behavior:

normalizeAddress('0xAbCdEf...'); // → '0xabcdef...' (lowercased)
normalizeAddress('xdcAbCdEf...'); // → '0xabcdef...' (xdc → 0x)
normalizeAddress(''); // → '0x0000000000000000000000000000000000000000' (zero address)

Note: normalizeAddress returns the zero address for empty or falsy inputs, consistent with EVM conventions. This prevents empty strings from propagating as ghost entries in maps and sets.

Formatting precision: Both formatXDC and formatWei use bigint arithmetic internally, so large amounts (beyond JavaScript's Number.MAX_SAFE_INTEGER) are displayed correctly without loss of precision.


Configuration Reference

InteractionDetectorConfig (ContractWatcher)

| Option | Type | Default | Description | | ----------------- | ------------------ | ---------- | -------------------------------------------- | | rpcUrl | string | required | Primary HTTP RPC URL | | wsUrl | string | — | WebSocket RPC URL (enables real-time events) | | contracts | ContractConfig[] | required | Contracts to monitor | | explorer | ExplorerConfig | — | Block explorer API settings | | polling | PollingConfig | — | HTTP polling tuning | | ws | WsConfig | — | WebSocket tuning | | checkpoint | CheckpointConfig | memory | Checkpoint persistence | | chainId | number | 50 | Chain ID | | fallbackRpcUrls | string[] | [] | Fallback RPC endpoints | | logLevel | LogLevel | 'info' | Log verbosity |

ContractConfig

| Option | Type | Default | Description | | --------- | -------- | ---------- | ----------------------------------------------------- | | address | string | required | Contract address (0x or xdc prefix) | | abi | any[] | — | ABI for event decoding. Human-readable or JSON format | | name | string | — | Human-readable label (shown in decoded events) |

ExplorerConfig

| Option | Type | Default | Description | | ----------------- | -------- | ---------- | --------------------------------------- | | apiUrl | string | required | Explorer API base URL | | apiKey | string | — | API key for higher rate limits | | chainId | number | — | Chain ID (required for Etherscan v2) | | rateLimitPerSec | number | 5 | Max requests per second | | pollIntervalMs | number | 60000 | How often to poll txlist/txlistinternal |

PollingConfig

| Option | Type | Default | Description | | --------------- | -------- | ------- | ---------------------------------------- | | intervalMs | number | 30000 | How often to poll eth_getLogs | | maxBlockRange | number | 100 | Max blocks per request (XDC limit = 100) | | concurrency | number | 3 | Parallel chunk fetches |

WsConfig

| Option | Type | Default | Description | | ---------------------- | --------- | ------- | --------------------------------------------- | | enabled | boolean | true | Enable WebSocket subscription | | reconnectDelayBaseMs | number | 5000 | Base reconnect delay | | reconnectDelayMaxMs | number | 30000 | Max reconnect delay (exponential backoff cap) | | maxReconnectAttempts | number | 20 | Max attempts before 5-min cooldown | | heartbeatIntervalMs | number | 60000 | Heartbeat ping interval | | heartbeatTimeoutMs | number | 10000 | Heartbeat response timeout |

CheckpointConfig

| Option | Type | Default | Description | | --------- | ------------------- | ----------------- | -------------------------------------------- | | backend | string | 'memory' | 'memory', 'file', or 'custom' | | path | string | './checkpoints' | Directory for 'file' backend | | custom | CheckpointBackend | — | Custom implementation for 'custom' backend |


XDC-Specific Notes

  • Non-standard ABI encoding: Some XDC system contracts (including XDCValidator at 0x...0088) encode all event parameters in the data field with only topic[0] (the event signature). Standard decoders like ethers.js fail on these. The library includes an automatic fallback decoder that handles this transparently.

  • Block range limit: XDC RPC limits eth_getLogs to 100 blocks per request. The LogPoller and BlockScanner automatically chunk requests. Don't set maxBlockRange above 100 for XDC.

  • Address prefix: XDC uses the xdc prefix instead of 0x. All library functions accept both formats. Internally, everything normalizes to lowercase 0x. Empty/falsy inputs normalize to the zero address (0x000...000).

  • Tracing: debug_traceTransaction requires an archive node for historical transactions. For monitoring current blocks in real-time, a standard full node works fine.

  • Atomic checkpoints: The FileCheckpoint backend uses atomic write-then-rename to prevent data corruption on process crash. Checkpoint data is always consistent.


Architecture

┌──────────────────────────────────────────────────────────┐
│            @xdc.org/interaction-detector                 │
│                                                          │
│  ┌───────────────────────────────────────────────────┐   │
│  │  ContractWatcher         (real-time monitoring)   │   │
│  │    ├── WsManager         (WebSocket subscription) │   │
│  │    ├── LogPoller*        (eth_getLogs fallback)   │   │
│  │    ├── ExplorerClient*   (txlist + txlistinternal)│   │
│  │    ├── EventDecoder      (ABI + XDC fallback)     │   │
│  │    └── Checkpoint        (pluggable persistence)  │   │
│  └───────────────────────────────────────────────────┘   │
│                                                          │
│  ┌────────────────────────────────────────────────────┐  │
│  │  BlockScanner            (historical queries)      │  │
│  │    ├── LogPoller*        (chunked eth_getLogs)     │  │
│  │    └── ExplorerClient*   (API adapter)             │  │
│  └────────────────────────────────────────────────────┘  │
│                                                          │
│  ┌────────────────────────────────────────────────────┐  │
│  │  TransactionTracer       (debug_traceTransaction)  │  │
│  │    ├── CallTreeParser    (callTracer output)       │  │
│  │    └── StateDiffParser   (prestateTracer output)   │  │
│  └────────────────────────────────────────────────────┘  │
│                                                          │
│  ┌────────────────────┐  ┌───────────────────────────┐   │
│  │  RPC Client        │  │  Utilities                │   │
│  │  (retry, timeout,  │  │  (address normalize,      │   │
│  │   fallback, WS)    │  │   format, logger, cache)  │   │
│  └────────────────────┘  └───────────────────────────┘   │
│                                                          │
│  * = also exported as standalone classes                 │
└──────────────────────────────────────────────────────────┘

Detection pipeline:

Events (eth_subscribe + eth_getLogs)
    ↓ collect tx hashes
XDCScan (txlist + txlistinternal)
    ↓ merge + deduplicate
ContractInteraction records
    ↓ optionally
debug_traceTransaction (per tx hash)
    ↓ parse call tree + state diffs
Full execution story

Examples

See the examples/ directory for runnable scripts:

| Example | What it demonstrates | | ------------------------------ | ------------------------------------------ | | basic-watcher.ts | Real-time event monitoring with checkpoint | | historical-scan.ts | Scanning a block range for events | | trace-transaction.ts | Deep-diving a single transaction | | full-interaction-detector.ts | All 3 methods combined |

Run any example with:

npm run build
npx tsx examples/basic-watcher.ts
npx tsx examples/trace-transaction.ts 0xYourTxHash

Development

# Install dependencies
npm install

# Compile TypeScript
npm run build

# Run unit tests (114 tests across 11 test files)
npm test

# Watch mode (auto-recompile on save)
npm run dev

License

MIT