Skip to main content

WebSocket Subscriptions

Real-time blockchain updates via WebSocket for reactive applications.

Enabling WebSocket

Via CLI

npx cardano-devkit start --websocket

Via Code

const devnet = createDevnetManager({
enableWebSocket: true
});

Connecting

const ws = new WebSocket('ws://localhost:10080/ws');

ws.onopen = () => {
console.log('Connected to devnet');
};

ws.onclose = () => {
console.log('Disconnected from devnet');
};

ws.onerror = (error) => {
console.error('WebSocket error:', error);
};

Subscription Types

New Blocks

ws.onopen = () => {
ws.send(JSON.stringify({
type: 'subscribe',
channel: 'blocks'
}));
};

ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'block') {
console.log('New block:', {
height: data.height,
slot: data.slot,
hash: data.hash,
txCount: data.txCount
});
}
};

Address Activity

ws.send(JSON.stringify({
type: 'subscribe',
channel: 'address',
address: 'addr_test1qz...'
}));

ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'address_tx') {
console.log('Address activity:', {
txHash: data.txHash,
direction: data.direction, // 'incoming' | 'outgoing'
amount: data.amount
});
}
};

Transaction Confirmations

ws.send(JSON.stringify({
type: 'subscribe',
channel: 'tx',
txHash: 'abc123...'
}));

ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'tx_confirmed') {
console.log('Transaction confirmed:', {
txHash: data.txHash,
block: data.blockHeight,
confirmations: data.confirmations
});
}
};

Message Types

Incoming Messages

TypeDescriptionFields
blockNew block producedheight, slot, hash, txCount
address_txActivity on watched addresstxHash, direction, amount
tx_confirmedWatched tx confirmedtxHash, blockHeight, confirmations
rollbackChain rollback occurredfromSlot, toSlot, depth
errorSubscription errormessage, code

Outgoing Messages

TypeDescriptionFields
subscribeSubscribe to channelchannel, optional: address, txHash
unsubscribeUnsubscribe from channelchannel, optional: address, txHash
pingKeep-alive ping-

React Integration

useWebSocket Hook

import { useState, useEffect, useCallback } from 'react';

function useDevnetWebSocket() {
const [blocks, setBlocks] = useState<Block[]>([]);
const [connected, setConnected] = useState(false);
const [ws, setWs] = useState<WebSocket | null>(null);

useEffect(() => {
const socket = new WebSocket('ws://localhost:10080/ws');

socket.onopen = () => {
setConnected(true);
socket.send(JSON.stringify({
type: 'subscribe',
channel: 'blocks'
}));
};

socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'block') {
setBlocks(prev => [...prev.slice(-99), data]);
}
};

socket.onclose = () => setConnected(false);

setWs(socket);

return () => socket.close();
}, []);

const watchAddress = useCallback((address: string) => {
ws?.send(JSON.stringify({
type: 'subscribe',
channel: 'address',
address
}));
}, [ws]);

return { blocks, connected, watchAddress };
}

BlockMonitor Component

function BlockMonitor() {
const { blocks, connected } = useDevnetWebSocket();

return (
<div>
<h2>
Block Monitor
<span className={connected ? 'connected' : 'disconnected'}>
{connected ? '🟢' : '🔴'}
</span>
</h2>
<ul>
{blocks.slice(-10).reverse().map(block => (
<li key={block.hash}>
Block #{block.height} - {block.txCount} txs
</li>
))}
</ul>
</div>
);
}

Complete Example

class DevnetSubscriber {
private ws: WebSocket | null = null;
private handlers: Map<string, Function[]> = new Map();

connect(url = 'ws://localhost:10080/ws'): Promise<void> {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(url);

this.ws.onopen = () => resolve();
this.ws.onerror = reject;

this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
const handlers = this.handlers.get(data.type) || [];
handlers.forEach(h => h(data));
};
});
}

subscribe(channel: string, options?: { address?: string; txHash?: string }) {
this.ws?.send(JSON.stringify({
type: 'subscribe',
channel,
...options
}));
}

on(type: string, handler: Function) {
const handlers = this.handlers.get(type) || [];
handlers.push(handler);
this.handlers.set(type, handlers);
}

close() {
this.ws?.close();
}
}

// Usage
const subscriber = new DevnetSubscriber();
await subscriber.connect();

subscriber.on('block', (block) => {
console.log(`New block: ${block.height}`);
});

subscriber.subscribe('blocks');

Error Handling

ws.onmessage = (event) => {
const data = JSON.parse(event.data);

if (data.type === 'error') {
switch (data.code) {
case 'INVALID_ADDRESS':
console.error('Invalid address format');
break;
case 'SUBSCRIPTION_LIMIT':
console.error('Too many subscriptions');
break;
case 'RATE_LIMITED':
console.error('Rate limited, slow down');
break;
default:
console.error('Unknown error:', data.message);
}
}
};

Reconnection Strategy

function createReconnectingWebSocket(url: string) {
let ws: WebSocket | null = null;
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
const reconnectDelay = 1000;

function connect() {
ws = new WebSocket(url);

ws.onopen = () => {
reconnectAttempts = 0;
console.log('Connected');
// Resubscribe to channels
};

ws.onclose = () => {
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
console.log(`Reconnecting (${reconnectAttempts})...`);
setTimeout(connect, reconnectDelay * reconnectAttempts);
} else {
console.error('Max reconnection attempts reached');
}
};
}

connect();
return { getSocket: () => ws };
}

Next Steps