@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
Maintainers
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-driveFor Node.js < 21 (no native WebSocket), also install ws:
npm install wsQuick 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:
- Connect a wallet using
@rockerone/xprnkit - Send a transfer on XPR Network
- IonDrive detects the transfer on-chain in real-time
- Live head block and LIB tracking
- Real-time irreversibility status until the transfer is finalized
cd examples/react-transfer
npm install
npm run devKey files:
- src/useTransferListener.ts — React hook wrapping
BlockStreamClientas 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 devLicense
MIT License - see LICENSE file for details.
