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

@rockerone/ion-drive

v2.0.0

Published

A powerful, type-safe TypeScript client for streaming real-time blockchain data from XPRNetwork using State History Plugin

Readme

Ion Drive

Isomorphic XPRNetwork Block Stream Client

A powerful, type-safe TypeScript client for streaming real-time blockchain data from XPRNetwork (and other EOSIO chains) using the State History Plugin. Works in both Node.js and browsers with zero configuration.

Features

  • Isomorphic — runs in Node.js and browsers out of the box
  • Real-time block streaming from EOSIO State History Plugin (SHiP)
  • ABI-powered decoding — binary data automatically converted to readable JSON
  • Microservice architecture — composable processing pipeline with .pipe()
  • Fork detection — automatic chain reorganization detection with depth tracking
  • LIB tracking — Last Irreversible Block info available in every context
  • Transaction tracing — full transaction ID (tx_id) exposure for action traces
  • Advanced filtering — contract, table, and action whitelisting with wildcard support
  • Pluggable logging — bring your own logger or use the built-in ConsoleLogger
  • Full TypeScript support — complete type safety with proper EOSIO types

Installation

npm install @rockerone/ion-drive
# or
bun add @rockerone/ion-drive

For Node.js < 21 (no native WebSocket), also install ws:

npm install ws

Quick Start

Node.js

import {BlockStreamClient} from "@rockerone/ion-drive";

const client = new BlockStreamClient({
  socketAddress: "ws://your-node:8080",
  rpcAddress: "https://your-node",
  contracts: {
    "eosio.token": {
      tables: ["accounts", "stat"],
      actions: ["transfer", "issue"],
    },
  },
  enableDebug: true,
  logLevel: "info",
});

client.pipe(({$block, $lib, $tx, $action, $delta, $table, $logger}) => {
  if ($action) {
    $logger.info("Action received", {
      contract: $action.account,
      action: $action.name,
      tx_id: $tx?.tx_id,
      block: $block.block_number,
      lib: $lib?.block_num,
    });
  }

  return {$block, $lib, $tx, $action, $delta, $table, $logger};
});

client.start();

Browser (React)

import {BlockStreamClient} from "@rockerone/ion-drive";
import type {MicroService} from "@rockerone/ion-drive";

const client = new BlockStreamClient({
  socketAddress: "ws://your-node:8080",
  rpcAddress: "https://your-node",
  contracts: {
    "eosio.token": {
      actions: ["transfer"],
      tables: ["accounts"],
    },
  },
});

// Uses the browser's native WebSocket and fetch — no polyfills needed
const myService: MicroService = (ctx) => {
  if (ctx.$action?.name === "transfer") {
    console.log("Transfer detected!", ctx.$action.data);
  }
  return ctx;
};

client.pipe(myService).start();

Configuration

Options

| Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | socketAddress | string | Yes | — | WebSocket endpoint for State History Plugin | | rpcAddress | string | Yes | — | HTTP endpoint for chain RPC API | | contracts | Record<string, ContractConfig> | No | {} | Contract filtering configuration | | enableDebug | boolean | No | false | Enable console logging output | | logLevel | LogLevel | No | "error" | Log level: error, warn, info, micro, socket, debug | | logger | Logger | No | ConsoleLogger | Custom logger instance (e.g. winston, pino) | | webSocketConstructor | WebSocketConstructor | No | globalThis.WebSocket | Custom WebSocket constructor | | fetchFunction | typeof fetch | No | globalThis.fetch | Custom fetch function |

Contract Filtering

const client = new BlockStreamClient({
  socketAddress: "ws://your-node:8080",
  rpcAddress: "https://your-node",
  contracts: {
    // Specific tables and actions
    eosio: {
      tables: ["voters", "producers"],
      actions: ["voteproducer", "regproducer"],
    },

    // All tables, specific actions
    "eosio.token": {
      tables: ["*"], // Wildcard = all tables
      actions: ["transfer", "issue"],
    },

    // All tables, all actions
    mycontract: {
      tables: ["*"],
      // No actions = all actions
    },
  },
});

Legacy Format (Deprecated)

// Still supported but use `contracts` instead
const client = new BlockStreamClient({
  socketAddress: "ws://your-node:8080",
  rpcAddress: "https://your-node",
  tables: {
    "eosio.token": ["accounts"],
  },
});

Microservice Architecture

Chain multiple processing functions together using .pipe(). Each microservice receives a MicroServiceContext and must return one:

// Logger microservice
const logger: MicroService = ({$block, $lib, $tx, $action, $delta, $table, $logger}) => {
  if ($action) {
    $logger.info("Action", {
      contract: $action.account,
      action: $action.name,
      tx_id: $tx?.tx_id,
    });
  }
  return {$block, $lib, $tx, $action, $delta, $table, $logger};
};

// Transfer filter with finality check
const transferFilter: MicroService = ({$block, $lib, $tx, $action, ...rest}) => {
  if ($action?.name === "transfer" && $lib) {
    const isFinalized = $block.block_number <= $lib.block_num;
    console.log(`Transfer ${isFinalized ? "finalized" : "pending"}`, {
      from: $action.data.from,
      to: $action.data.to,
      amount: $action.data.quantity,
      tx_id: $tx?.tx_id,
    });
  }
  return {$block, $lib, $tx, $action, ...rest};
};

// Fork handler
const forkHandler: MicroService = ({$fork, $logger, ...rest}) => {
  if ($fork) {
    $logger.warn("Chain reorganization!", {
      fork_block: $fork.fork_block_num,
      depth: $fork.depth,
    });
  }
  return {$fork, $logger, ...rest};
};

// Chain them together
client.pipe(logger).pipe(transferFilter).pipe(forkHandler).start();

MicroService Context

Every microservice receives a MicroServiceContext:

interface MicroServiceContext {
  $block: BlockData;        // Block info (always present)
  $lib?: LibInfo;           // Last Irreversible Block
  $tx?: TransactionData;    // Transaction data with tx_id
  $fork?: ForkData;         // Fork event (chain reorganization)
  $delta?: TableDelta;      // Table delta
  $action?: ActionData;     // Action trace
  $table?: string;          // Table name shortcut
  $logger: Logger;          // Logger instance
}

BlockData

{
  block_number: number;
  block_id: string;
  timestamp: string;
  filtering: {
    contracts: string[];
    tables: Record<string, string[]>;
    enabled: boolean;
  };
}

LibInfo

{
  block_num: number;    // LIB block number
  block_id: string;     // LIB block hash
}

TransactionData

{
  tx_id: string;            // Transaction hash
  status: number;
  cpu_usage_us: number;
  net_usage_words: number;
}

ForkData

{
  fork_block_num: number;   // Block where fork occurred
  old_block_id: string;     // Previous block ID
  new_block_id: string;     // New block ID
  depth: number;            // Blocks rolled back
}

ActionData

{
  account: string;          // Contract account
  name: string;             // Action name
  data: {
    decoded: any;           // ABI-decoded action data
    hex: string;            // Raw hex data
    abi_decoded: boolean;   // Whether decoding succeeded
  };
  authorization: Array<{
    actor: string;
    permission: string;
  }>;
  filtered: boolean;
}

TableDelta

{
  type: string;
  contract: string;
  table: string;
  data: any;                // ABI-decoded table row
  processed: boolean;
  filtered: boolean;
}

Fork Detection & Finality

Ion Drive automatically tracks block history and detects chain reorganizations (forks).

Detecting Forks

client.pipe(({$fork, $logger, ...rest}) => {
  if ($fork) {
    $logger.warn("Fork detected!", {
      fork_block: $fork.fork_block_num,
      depth: $fork.depth,
    });
    // Rollback any data from blocks >= fork_block_num
  }
  return {$fork, $logger, ...rest};
});

Checking Finality

// In a microservice — compare block number against LIB
client.pipe(({$block, $lib, $tx, $action, ...rest}) => {
  if ($action && $lib) {
    const isFinalized = $block.block_number <= $lib.block_num;
    if (isFinalized) {
      // Safe to consider permanent
    }
  }
  return {$block, $lib, $tx, $action, ...rest};
});

// Or use the client helper method
const isFinalized = client.isIrreversible(blockNum);

// Get current LIB info
const lib = client.getLib();

Transaction Pool Pattern

Track transactions until they become irreversible:

const txPool = new Map<string, {block_number: number; status: string}>();

client.pipe(({$block, $lib, $tx, $logger, ...rest}) => {
  if ($tx) {
    txPool.set($tx.tx_id, {
      block_number: $block.block_number,
      status: "pending",
    });
  }

  if ($lib) {
    txPool.forEach((value, key) => {
      if (value.block_number <= $lib.block_num && value.status === "pending") {
        txPool.set(key, {...value, status: "irreversible"});
        $logger.info(`Transaction ${key} is now irreversible`);
      }
    });
  }

  return {$block, $lib, $tx, $logger, ...rest};
});

Logging

Built-in Logger

Ion Drive ships with ConsoleLogger, a lightweight logger that works in both Node.js and browsers:

import {ConsoleLogger} from "@rockerone/ion-drive";
// or
import {ConsoleLogger} from "@rockerone/ion-drive/logger";

Log Levels

| Level | Priority | Description | |-------|----------|-------------| | error | 0 | Critical errors only | | warn | 1 | Warnings and above | | info | 2 | General information | | micro | 3 | Microservice debugging | | socket | 4 | WebSocket/protocol debugging | | debug | 5 | Full debug output |

Custom Logger

Implement the Logger interface to use any logging library:

import type {Logger} from "@rockerone/ion-drive";

const myLogger: Logger = {
  error: (msg, meta) => winston.error(msg, meta),
  warn: (msg, meta) => winston.warn(msg, meta),
  info: (msg, meta) => winston.info(msg, meta),
  debug: (msg, meta) => winston.debug(msg, meta),
  socket: (msg, meta) => winston.debug(`[socket] ${msg}`, meta),
  micro: (msg, meta) => winston.debug(`[micro] ${msg}`, meta),
};

const client = new BlockStreamClient({
  socketAddress: "ws://your-node:8080",
  rpcAddress: "https://your-node",
  logger: myLogger,
});

Isomorphic Support

Ion Drive works in both Node.js and browsers without any configuration:

| Environment | WebSocket | fetch | |-------------|-----------|-------| | Browser | Native WebSocket | Native fetch | | Node.js 21+ | Native WebSocket | Native fetch | | Node.js 18-20 | ws package (peer dep) | Native fetch |

You can also inject custom implementations:

import WebSocket from "ws";

const client = new BlockStreamClient({
  socketAddress: "ws://your-node:8080",
  rpcAddress: "https://your-node",
  webSocketConstructor: WebSocket,
  fetchFunction: customFetch,
});

Examples

React Transfer Example (examples/react-transfer)

A React app that demonstrates IonDrive in the browser:

  1. Connect a wallet using @rockerone/xprnkit
  2. Send a transfer on XPR Network
  3. IonDrive detects the transfer on-chain in real-time
  4. Live head block and LIB tracking
  5. Real-time irreversibility status until the transfer is finalized
cd examples/react-transfer
npm install
npm run dev

Key files:

  • src/useTransferListener.ts — React hook wrapping BlockStreamClient as a "once" listener with live block/LIB tracking
  • src/App.tsx — UI with wallet connection, transfer button, and real-time listener status

Network Endpoints

  • XPR Network Mainnet: ws://api.rockerone.io:8080 (SHiP) / https://api.rockerone.io (RPC)

Requirements

  • Browser: any modern browser (Chrome, Firefox, Safari, Edge)
  • Node.js: 18+ (21+ for native WebSocket, or install ws)
  • TypeScript: 5+
  • State History Plugin enabled on the target blockchain node

Development

# Install dependencies
bun install

# Build
bun run build

# Watch mode
bun run dev

License

MIT License - see LICENSE file for details.

Links