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
| Type | Description | Fields |
|---|---|---|
block | New block produced | height, slot, hash, txCount |
address_tx | Activity on watched address | txHash, direction, amount |
tx_confirmed | Watched tx confirmed | txHash, blockHeight, confirmations |
rollback | Chain rollback occurred | fromSlot, toSlot, depth |
error | Subscription error | message, code |
Outgoing Messages
| Type | Description | Fields |
|---|---|---|
subscribe | Subscribe to channel | channel, optional: address, txHash |
unsubscribe | Unsubscribe from channel | channel, optional: address, txHash |
ping | Keep-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 };
}