Skip to content

XinFinOrg/XDCInteractionDetector

Repository files navigation

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,
});

// ── 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

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors