This guide covers listening for blockchain events, streaming data, and building notification systems on XPR Network.
XPR Network provides several ways to get real-time updates:
| Method | Use Case | Latency |
|---|---|---|
| Hyperion Streaming | Action/delta streams | ~1-2 seconds |
| Polling | Simple integrations | 1-30 seconds |
| State History Plugin | Full node operators | Real-time |
Hyperion provides WebSocket-based streaming for real-time action and delta notifications.
wss://proton.eosusa.io/stream
Listen for specific contract actions in real-time:
import WebSocket from 'ws';
interface StreamMessage {
type: 'action' | 'delta';
content: any;
}
class HyperionStream {
private ws: WebSocket | null = null;
private endpoint = 'wss://proton.eosusa.io/stream';
connect(): Promise<void> {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.endpoint);
this.ws.on('open', () => {
console.log('Connected to Hyperion stream');
resolve();
});
this.ws.on('error', (error) => {
console.error('Stream error:', error);
reject(error);
});
this.ws.on('close', () => {
console.log('Stream disconnected');
this.reconnect();
});
});
}
// Subscribe to contract actions
subscribeActions(contract: string, action: string, callback: (data: any) => void): void {
if (!this.ws) throw new Error('Not connected');
// Send subscription request
this.ws.send(JSON.stringify({
type: 'action_stream',
req_id: `${contract}:${action}`,
data: {
contract,
action,
start_from: 'head' // Start from current block
}
}));
// Handle messages
this.ws.on('message', (data: string) => {
const message: StreamMessage = JSON.parse(data);
if (message.type === 'action') {
callback(message.content);
}
});
}
// Subscribe to table deltas
subscribeDeltas(contract: string, table: string, callback: (data: any) => void): void {
if (!this.ws) throw new Error('Not connected');
this.ws.send(JSON.stringify({
type: 'delta_stream',
req_id: `${contract}:${table}`,
data: {
code: contract,
table,
start_from: 'head'
}
}));
this.ws.on('message', (data: string) => {
const message: StreamMessage = JSON.parse(data);
if (message.type === 'delta') {
callback(message.content);
}
});
}
private reconnect(): void {
setTimeout(() => {
console.log('Reconnecting...');
this.connect();
}, 5000);
}
disconnect(): void {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
}
// Usage
const stream = new HyperionStream();
await stream.connect();
// Listen for all transfers
stream.subscribeActions('eosio.token', 'transfer', (action) => {
console.log('Transfer:', action.data);
// { from: 'alice', to: 'bob', quantity: '100.0000 XPR', memo: 'Payment' }
});
// Listen for NFT mints
stream.subscribeActions('atomicassets', 'mintasset', (action) => {
console.log('NFT minted:', action.data);
});Monitor table changes in real-time:
// Watch for user profile changes
stream.subscribeDeltas('eosio.proton', 'usersinfo', (delta) => {
if (delta.present) {
console.log('Profile updated:', delta.data);
} else {
console.log('Profile removed:', delta.primary_key);
}
});
// Watch for oracle price updates
stream.subscribeDeltas('oracles', 'data', (delta) => {
console.log('Price updated:', delta.data);
});For simpler integrations, poll the Hyperion API:
class ActionPoller {
private lastTimestamp: string = '';
private pollInterval: number;
private running: boolean = false;
constructor(
private account: string,
private filter: string | undefined,
intervalMs: number = 5000
) {
this.pollInterval = intervalMs;
this.lastTimestamp = new Date().toISOString();
}
start(callback: (actions: any[]) => void): void {
this.running = true;
this.poll(callback);
}
stop(): void {
this.running = false;
}
private async poll(callback: (actions: any[]) => void): Promise<void> {
while (this.running) {
try {
const url = new URL('https://proton.eosusa.io/v2/history/get_actions');
url.searchParams.set('account', this.account);
url.searchParams.set('after', this.lastTimestamp);
url.searchParams.set('sort', 'asc');
url.searchParams.set('limit', '100');
if (this.filter) {
url.searchParams.set('filter', this.filter);
}
const response = await fetch(url);
const data = await response.json();
if (data.actions && data.actions.length > 0) {
callback(data.actions);
// Update timestamp to latest action
const lastAction = data.actions[data.actions.length - 1];
this.lastTimestamp = lastAction['@timestamp'];
}
} catch (error) {
console.error('Polling error:', error);
}
await this.sleep(this.pollInterval);
}
}
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Usage: Poll for incoming transfers
const poller = new ActionPoller('myaccount', 'eosio.token:transfer', 3000);
poller.start((actions) => {
for (const action of actions) {
if (action.act.data.to === 'myaccount') {
console.log('Received payment:', action.act.data);
// Handle incoming payment
}
}
});class TablePoller<T> {
private lastData: Map<string, T> = new Map();
constructor(
private contract: string,
private table: string,
private scope: string = contract,
private pollInterval: number = 5000
) {}
async start(
onAdd: (row: T) => void,
onUpdate: (oldRow: T, newRow: T) => void,
onRemove: (row: T) => void,
getKey: (row: T) => string
): Promise<void> {
while (true) {
try {
const { rows } = await rpc.get_table_rows({
code: this.contract,
scope: this.scope,
table: this.table,
limit: 1000
});
const currentKeys = new Set<string>();
for (const row of rows) {
const key = getKey(row);
currentKeys.add(key);
const existing = this.lastData.get(key);
if (!existing) {
onAdd(row);
} else if (JSON.stringify(existing) !== JSON.stringify(row)) {
onUpdate(existing, row);
}
this.lastData.set(key, row);
}
// Check for removed rows
for (const [key, row] of this.lastData) {
if (!currentKeys.has(key)) {
onRemove(row);
this.lastData.delete(key);
}
}
} catch (error) {
console.error('Table poll error:', error);
}
await new Promise(r => setTimeout(r, this.pollInterval));
}
}
}
// Usage: Watch challenges table
const challengePoller = new TablePoller('pricebattle', 'challenges');
challengePoller.start(
(challenge) => console.log('New challenge:', challenge),
(old, updated) => console.log('Challenge updated:', old.id, '->', updated),
(challenge) => console.log('Challenge removed:', challenge.id),
(row) => String(row.id)
);Build a notification service that watches the chain and notifies users:
import WebSocket from 'ws';
import { Server } from 'socket.io';
interface Subscription {
userId: string;
account: string;
filters: string[];
}
class NotificationService {
private hyperion: HyperionStream;
private io: Server;
private subscriptions: Map<string, Subscription[]> = new Map();
constructor(ioServer: Server) {
this.io = ioServer;
this.hyperion = new HyperionStream();
}
async start(): Promise<void> {
await this.hyperion.connect();
// Subscribe to common actions
this.watchTransfers();
this.watchNFTs();
}
private watchTransfers(): void {
this.hyperion.subscribeActions('eosio.token', 'transfer', (action) => {
const { from, to, quantity, memo } = action.data;
// Notify recipient
this.notifyAccount(to, {
type: 'transfer_received',
from,
quantity,
memo,
txId: action.trx_id
});
// Notify sender
this.notifyAccount(from, {
type: 'transfer_sent',
to,
quantity,
memo,
txId: action.trx_id
});
});
}
private watchNFTs(): void {
this.hyperion.subscribeActions('atomicassets', 'transfer', (action) => {
const { from, to, asset_ids } = action.data;
this.notifyAccount(to, {
type: 'nft_received',
from,
assetIds: asset_ids,
txId: action.trx_id
});
});
}
private notifyAccount(account: string, notification: any): void {
// Emit to all connected clients watching this account
this.io.to(`account:${account}`).emit('notification', notification);
// Could also store in database, send push notification, etc.
}
// Called when user connects
subscribeUser(socketId: string, account: string): void {
this.io.sockets.sockets.get(socketId)?.join(`account:${account}`);
}
}
// Express + Socket.io setup
import express from 'express';
import { createServer } from 'http';
import { Server } from 'socket.io';
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, { cors: { origin: '*' } });
const notificationService = new NotificationService(io);
io.on('connection', (socket) => {
console.log('Client connected:', socket.id);
socket.on('subscribe', (account: string) => {
notificationService.subscribeUser(socket.id, account);
console.log(`${socket.id} subscribed to ${account}`);
});
});
notificationService.start();
httpServer.listen(3001);import { io, Socket } from 'socket.io-client';
class NotificationClient {
private socket: Socket;
private handlers: Map<string, ((data: any) => void)[]> = new Map();
constructor(serverUrl: string) {
this.socket = io(serverUrl);
this.socket.on('notification', (notification) => {
const handlers = this.handlers.get(notification.type) || [];
handlers.forEach(handler => handler(notification));
// Also call 'all' handlers
const allHandlers = this.handlers.get('all') || [];
allHandlers.forEach(handler => handler(notification));
});
}
subscribe(account: string): void {
this.socket.emit('subscribe', account);
}
on(type: string, handler: (data: any) => void): void {
const handlers = this.handlers.get(type) || [];
handlers.push(handler);
this.handlers.set(type, handlers);
}
}
// React hook
import { useEffect, useState } from 'react';
export function useNotifications(account: string) {
const [notifications, setNotifications] = useState<any[]>([]);
const [client] = useState(() => new NotificationClient('http://localhost:3001'));
useEffect(() => {
if (!account) return;
client.subscribe(account);
client.on('all', (notification) => {
setNotifications(prev => [notification, ...prev].slice(0, 50));
});
}, [account]);
return notifications;
}
// Usage in component
function NotificationBell({ account }) {
const notifications = useNotifications(account);
return (
<div>
{notifications.map((n, i) => (
<div key={i}>
{n.type === 'transfer_received' && (
<span>Received {n.quantity} from {n.from}</span>
)}
{n.type === 'nft_received' && (
<span>Received NFT from {n.from}</span>
)}
</div>
))}
</div>
);
}Wait for transaction confirmation:
interface TxResult {
transaction_id: string;
processed: {
block_num: number;
block_time: string;
};
}
async function waitForConfirmation(
txId: string,
confirmations: number = 1,
timeoutMs: number = 30000
): Promise<boolean> {
const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
try {
const response = await fetch(
`https://proton.eosusa.io/v2/history/get_transaction?id=${txId}`
);
const data = await response.json();
if (data.trx_id) {
// Transaction found
const info = await rpc.get_info();
const txBlockNum = data.actions[0]?.block_num;
const currentBlock = info.head_block_num;
const confirmedBlocks = currentBlock - txBlockNum;
if (confirmedBlocks >= confirmations) {
return true;
}
}
} catch (error) {
// Transaction not yet indexed, continue waiting
}
await new Promise(r => setTimeout(r, 1000));
}
return false;
}
// Usage
const result = await session.transact({ actions }, { broadcast: true });
const confirmed = await waitForConfirmation(result.transaction_id, 3);
if (confirmed) {
console.log('Transaction confirmed!');
} else {
console.log('Confirmation timeout');
}Sync blockchain data to a database for fast queries:
import { Pool } from 'pg';
class BlockchainSync {
private pool: Pool;
private stream: HyperionStream;
constructor(dbConfig: any) {
this.pool = new Pool(dbConfig);
this.stream = new HyperionStream();
}
async start(): Promise<void> {
await this.stream.connect();
// Sync transfers
this.stream.subscribeActions('eosio.token', 'transfer', async (action) => {
await this.pool.query(
`INSERT INTO transfers (tx_id, from_account, to_account, quantity, memo, timestamp)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (tx_id) DO NOTHING`,
[
action.trx_id,
action.data.from,
action.data.to,
action.data.quantity,
action.data.memo,
action['@timestamp']
]
);
});
// Sync challenges
this.stream.subscribeDeltas('pricebattle', 'challenges', async (delta) => {
if (delta.present) {
await this.pool.query(
`INSERT INTO challenges (id, creator, opponent, amount, status, created_at)
VALUES ($1, $2, $3, $4, $5, NOW())
ON CONFLICT (id) DO UPDATE SET
opponent = $2, status = $4`,
[delta.data.id, delta.data.creator, delta.data.opponent, delta.data.amount, delta.data.status]
);
} else {
await this.pool.query('DELETE FROM challenges WHERE id = $1', [delta.primary_key]);
}
});
}
}Send webhooks when events occur:
import axios from 'axios';
interface WebhookConfig {
url: string;
account: string;
events: string[];
secret: string;
}
class WebhookDispatcher {
private webhooks: WebhookConfig[] = [];
register(config: WebhookConfig): void {
this.webhooks.push(config);
}
async dispatch(event: string, account: string, payload: any): Promise<void> {
const matching = this.webhooks.filter(
w => w.account === account && w.events.includes(event)
);
for (const webhook of matching) {
try {
const signature = this.sign(payload, webhook.secret);
await axios.post(webhook.url, {
event,
payload,
timestamp: Date.now()
}, {
headers: {
'X-Webhook-Signature': signature,
'Content-Type': 'application/json'
}
});
} catch (error) {
console.error(`Webhook failed: ${webhook.url}`, error);
}
}
}
private sign(payload: any, secret: string): string {
const crypto = require('crypto');
return crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
}
}
// Usage
const dispatcher = new WebhookDispatcher();
dispatcher.register({
url: 'https://myapp.com/webhooks/payments',
account: 'merchant',
events: ['transfer_received'],
secret: 'my-webhook-secret'
});
// In your stream handler:
stream.subscribeActions('eosio.token', 'transfer', (action) => {
dispatcher.dispatch('transfer_received', action.data.to, action.data);
});// Action subscription
{
type: 'action_stream',
data: { contract: 'eosio.token', action: 'transfer' }
}
// Delta subscription
{
type: 'delta_stream',
data: { code: 'pricebattle', table: 'challenges' }
}| Filter | Description |
|---|---|
eosio.token:transfer |
All token transfers |
atomicassets:* |
All NFT actions |
pricebattle:* |
All PriceBattle actions |
eosio:newaccount |
New account creation |
| Use Case | Recommended Interval |
|---|---|
| Real-time UI | 1-3 seconds |
| Background sync | 5-10 seconds |
| Analytics | 30-60 seconds |
- Use streaming over polling when possible
- Filter server-side - don't fetch everything and filter client-side
- Implement reconnection logic for WebSocket connections
- Use database indexes for synced blockchain data
- Batch notifications to avoid overwhelming users
TypeScript library for real-time XPR Network streaming via State History Plugin.
Repository: https://github.com/SuperstrongBE/block-stream
Features:
- WebSocket streaming with automatic ABI decoding
- Microservice architecture with chainable processors
- Contract whitelisting and granular filtering
- Winston logging with configurable levels
Installation:
npm install @aspect/block-stream
# or
bun install @aspect/block-streamExample:
import { BlockStreamClient } from '@aspect/block-stream';
const client = new BlockStreamClient({
socketAddress: 'ws://proton-ship.eosusa.io:8080',
contracts: {
'eosio.token': {
tables: ['accounts'],
actions: ['transfer']
},
'pricebattle': {
tables: ['*'], // All tables
actions: ['*'] // All actions
}
}
});
client
.pipe(async (ctx) => {
if (ctx.$action) {
console.log('Action:', ctx.$action.name, ctx.$action.data);
}
if (ctx.$delta) {
console.log('Delta:', ctx.$table, ctx.$delta);
}
})
.start();Best for: Production indexers, analytics pipelines, real-time dashboards requiring direct State History access.