Pular para o conteúdo principal

zkSync Era WebSocket Guide

WebSocket connections provide real-time, bidirectional communication with the zkSync Era blockchain, enabling your applications to receive instant updates for new blocks, transactions, and smart contract events without polling.

Why Use WebSockets?

Advantages over HTTP Polling

  • Real-time Updates: Instant notifications when events occur
  • Lower Latency: No polling delays
  • Reduced Bandwidth: Only receive data when it changes
  • Better Performance: Persistent connection reduces overhead
  • Event-Driven: React to blockchain events as they happen

Common Use Cases

  • Trading Applications: Real-time price feeds and order book updates
  • DeFi Dashboards: Live liquidity and yield changes
  • NFT Marketplaces: Instant sale and listing notifications
  • Wallet Applications: Balance updates and transaction confirmations
  • Analytics Platforms: Real-time blockchain metrics

Connection Setup

WebSocket Endpoints

wss://api.blockeden.xyz/zksync/${accessKey}
wss://ethereum-sepolia.blockeden.xyz/<your-api-key>
wss://polygon-mainnet.blockeden.xyz/<your-api-key>
wss://arbitrum-mainnet.blockeden.xyz/<your-api-key>

Basic Connection

JavaScript (Browser/Node.js)

const ws = new WebSocket('wss://api.blockeden.xyz/zksync/${accessKey}');

ws.onopen = function(event) {
console.log('WebSocket connected');
};

ws.onmessage = function(event) {
const data = JSON.parse(event.data);
console.log('Received:', data);
};

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

ws.onclose = function(event) {
console.log('WebSocket closed:', event.code, event.reason);
};

Python

import asyncio
import websockets
import json

async def connect():
uri = "wss://api.blockeden.xyz/zksync/${accessKey}"

async with websockets.connect(uri) as websocket:
print("WebSocket connected")

# Subscribe to new heads
subscribe_msg = {
"jsonrpc": "2.0",
"method": "eth_subscribe",
"params": ["newHeads"],
"id": 1
}
await websocket.send(json.dumps(subscribe_msg))

# Listen for messages
async for message in websocket:
data = json.loads(message)
print(f"Received: {data}")

# Run the connection
asyncio.run(connect())

Go

package main

import (
"encoding/json"
"fmt"
"log"
"github.com/gorilla/websocket"
)

type SubscriptionRequest struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params []string `json:"params"`
ID int `json:"id"`
}

func main() {
url := "wss://api.blockeden.xyz/zksync/${accessKey}"

conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
log.Fatal("dial:", err)
}
defer conn.Close()

// Subscribe to new heads
req := SubscriptionRequest{
JSONRPC: "2.0",
Method: "eth_subscribe",
Params: []string{"newHeads"},
ID: 1,
}

if err := conn.WriteJSON(req); err != nil {
log.Fatal("write:", err)
}

// Read messages
for {
var msg map[string]interface{}
if err := conn.ReadJSON(&msg); err != nil {
log.Fatal("read:", err)
}
fmt.Printf("Received: %+v\n", msg)
}
}

Subscription Types

1. New Block Headers (newHeads)

Subscribe to new block headers as they're mined.

const subscribeToNewHeads = () => {
const subscription = {
jsonrpc: '2.0',
method: 'eth_subscribe',
params: ['newHeads'],
id: 1
};

ws.send(JSON.stringify(subscription));
};

// Handle new block headers
ws.onmessage = function(event) {
const data = JSON.parse(event.data);

if (data.method === 'eth_subscription' && data.params.subscription) {
const blockHeader = data.params.result;
console.log('New block:', {
number: parseInt(blockHeader.number, 16),
hash: blockHeader.hash,
timestamp: parseInt(blockHeader.timestamp, 16),
gasUsed: parseInt(blockHeader.gasUsed, 16),
gasLimit: parseInt(blockHeader.gasLimit, 16)
});
}
};

2. Pending Transactions (newPendingTransactions)

Subscribe to new pending transactions in the mempool.

const subscribeToPendingTransactions = () => {
const subscription = {
jsonrpc: '2.0',
method: 'eth_subscribe',
params: ['newPendingTransactions'],
id: 2
};

ws.send(JSON.stringify(subscription));
};

// Handle pending transactions
ws.onmessage = function(event) {
const data = JSON.parse(event.data);

if (data.method === 'eth_subscription' && data.params.subscription) {
const txHash = data.params.result;
console.log('New pending transaction:', txHash);

// Optionally fetch full transaction details
fetchTransactionDetails(txHash);
}
};

3. Smart Contract Events (logs)

Subscribe to specific smart contract events using log filters.

const subscribeToContractEvents = (contractAddress, topics) => {
const subscription = {
jsonrpc: '2.0',
method: 'eth_subscribe',
params: [
'logs',
{
address: contractAddress,
topics: topics
}
],
id: 3
};

ws.send(JSON.stringify(subscription));
};

// Subscribe to ERC-20 Transfer events
const ERC20_TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
subscribeToContractEvents('0xA0b86a33E6c0e4A2a2a5FB1C6A6D6a30BF8b6B3a', [ERC20_TRANSFER_TOPIC]);

// Handle contract events
ws.onmessage = function(event) {
const data = JSON.parse(event.data);

if (data.method === 'eth_subscription' && data.params.subscription) {
const log = data.params.result;
console.log('Contract event:', {
address: log.address,
topics: log.topics,
data: log.data,
blockNumber: parseInt(log.blockNumber, 16),
transactionHash: log.transactionHash
});
}
};

4. Synchronized Blocks (syncing)

Subscribe to synchronization status updates.

const subscribeToSyncing = () => {
const subscription = {
jsonrpc: '2.0',
method: 'eth_subscribe',
params: ['syncing'],
id: 4
};

ws.send(JSON.stringify(subscription));
};

// Handle sync status
ws.onmessage = function(event) {
const data = JSON.parse(event.data);

if (data.method === 'eth_subscription' && data.params.subscription) {
const syncStatus = data.params.result;
if (syncStatus === false) {
console.log('Node is fully synchronized');
} else {
console.log('Sync status:', {
startingBlock: parseInt(syncStatus.startingBlock, 16),
currentBlock: parseInt(syncStatus.currentBlock, 16),
highestBlock: parseInt(syncStatus.highestBlock, 16)
});
}
}
};

Advanced Usage Patterns

Managing Multiple Subscriptions

class zkSync EraWebSocketManager {
constructor(endpoint) {
this.endpoint = endpoint;
this.ws = null;
this.subscriptions = new Map();
this.messageHandlers = new Map();
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
}

connect() {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.endpoint);

this.ws.onopen = () => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;
resolve();
};

this.ws.onmessage = (event) => {
this.handleMessage(JSON.parse(event.data));
};

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

this.ws.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason);
this.handleReconnection();
};
});
}

subscribe(type, params, handler) {
const id = Date.now();
const subscription = {
jsonrpc: '2.0',
method: 'eth_subscribe',
params: [type, ...(params || [])],
id: id
};

this.ws.send(JSON.stringify(subscription));
this.messageHandlers.set(id, handler);

return id;
}

unsubscribe(subscriptionId) {
const unsubscribe = {
jsonrpc: '2.0',
method: 'eth_unsubscribe',
params: [subscriptionId],
id: Date.now()
};

this.ws.send(JSON.stringify(unsubscribe));
this.subscriptions.delete(subscriptionId);
}

handleMessage(data) {
if (data.method === 'eth_subscription') {
// Handle subscription data
const subscriptionId = data.params.subscription;
const handler = this.subscriptions.get(subscriptionId);
if (handler) {
handler(data.params.result);
}
} else if (data.id && this.messageHandlers.has(data.id)) {
// Handle subscription response
const handler = this.messageHandlers.get(data.id);
this.messageHandlers.delete(data.id);

if (data.result) {
this.subscriptions.set(data.result, handler);
}
}
}

handleReconnection() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = Math.pow(2, this.reconnectAttempts) * 1000;

setTimeout(() => {
console.log(`Reconnecting... Attempt ${this.reconnectAttempts}`);
this.connect().then(() => {
// Resubscribe to all previous subscriptions
this.resubscribeAll();
});
}, delay);
}
}

resubscribeAll() {
// Implementation depends on your subscription tracking
console.log('Resubscribing to all previous subscriptions');
}
}

// Usage
const wsManager = new zkSync EraWebSocketManager('wss://api.blockeden.xyz/zksync/${accessKey}');

wsManager.connect().then(() => {
// Subscribe to new blocks
wsManager.subscribe('newHeads', null, (blockHeader) => {
console.log('New block:', blockHeader);
});

// Subscribe to USDC transfers
wsManager.subscribe('logs', [{
address: '0xA0b86a33E6c0e4A2a2a5FB1C6A6D6a30BF8b6B3a',
topics: ['0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef']
}], (log) => {
console.log('USDC transfer:', log);
});
});

Real-time Trading Dashboard

class TradingDashboard {
constructor(wsEndpoint) {
this.wsManager = new zkSync EraWebSocketManager(wsEndpoint);
this.priceFeeds = new Map();
this.orderBookUpdates = new Map();
}

async start() {
await this.wsManager.connect();

// Subscribe to DEX events
this.subscribeToUniswapV3();
this.subscribeToSushiSwap();
this.subscribeToNewBlocks();
}

subscribeToUniswapV3() {
// Uniswap V3 Swap events
const SWAP_TOPIC = '0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67';

this.wsManager.subscribe('logs', [{
address: '0x1F98431c8aD98523631AE4a59f267346ea31F984', // Uniswap V3 Factory
topics: [SWAP_TOPIC]
}], (log) => {
this.handleUniswapSwap(log);
});
}

subscribeToSushiSwap() {
// SushiSwap Swap events
const SWAP_TOPIC = '0xd78ad95fa46c994b6551d0da85fc275fe613ce37657fb8d5e3d130840159d822';

this.wsManager.subscribe('logs', [{
topics: [SWAP_TOPIC]
}], (log) => {
this.handleSushiSwap(log);
});
}

subscribeToNewBlocks() {
this.wsManager.subscribe('newHeads', null, (blockHeader) => {
this.updateBlockInfo(blockHeader);
});
}

handleUniswapSwap(log) {
// Parse Uniswap V3 swap data
const poolAddress = log.address;
const data = this.parseSwapData(log.data);

this.updatePriceFeed('uniswap', poolAddress, data);
}

handleSushiSwap(log) {
// Parse SushiSwap swap data
const poolAddress = log.address;
const data = this.parseSwapData(log.data);

this.updatePriceFeed('sushiswap', poolAddress, data);
}

updatePriceFeed(exchange, pool, swapData) {
const key = `${exchange}:${pool}`;
const currentFeed = this.priceFeeds.get(key) || { prices: [], volume: 0 };

currentFeed.prices.push({
price: swapData.price,
timestamp: Date.now(),
volume: swapData.amount
});

// Keep only last 100 prices
if (currentFeed.prices.length > 100) {
currentFeed.prices.shift();
}

currentFeed.volume += swapData.amount;
this.priceFeeds.set(key, currentFeed);

// Emit update to UI
this.emitPriceUpdate(exchange, pool, currentFeed);
}

parseSwapData(data) {
// Implementation depends on the specific DEX
// This is a simplified example
return {
price: parseFloat(data.slice(0, 66)),
amount: parseFloat(data.slice(66, 130))
};
}

emitPriceUpdate(exchange, pool, feed) {
// Send to frontend/dashboard
console.log(`Price update for ${exchange}:${pool}`, feed);
}

updateBlockInfo(blockHeader) {
const blockInfo = {
number: parseInt(blockHeader.number, 16),
timestamp: parseInt(blockHeader.timestamp, 16),
gasUsed: parseInt(blockHeader.gasUsed, 16),
gasLimit: parseInt(blockHeader.gasLimit, 16)
};

console.log('New block:', blockInfo);
}
}

// Usage
const dashboard = new TradingDashboard('wss://api.blockeden.xyz/zksync/${accessKey}');
dashboard.start();

NFT Marketplace Monitor

class NFTMarketplaceMonitor {
constructor(wsEndpoint) {
this.wsManager = new zkSync EraWebSocketManager(wsEndpoint);
this.marketplaces = {
opensea: '0x00000000006c3852cbEf3e08E8dF289169EdE581',
looksrare: '0x59728544B08AB483533076417FbBB2fD0B17CE3a',
x2y2: '0x74312363e45DCaBA76c59ec49a7Aa8A65a67EeD3'
};
}

async start() {
await this.wsManager.connect();

Object.entries(this.marketplaces).forEach(([name, address]) => {
this.subscribeToMarketplace(name, address);
});
}

subscribeToMarketplace(name, address) {
// Subscribe to all events from marketplace contract
this.wsManager.subscribe('logs', [{
address: address
}], (log) => {
this.handleMarketplaceEvent(name, log);
});
}

handleMarketplaceEvent(marketplace, log) {
const eventSignature = log.topics[0];

switch (eventSignature) {
case '0x9d9af8e38d66c62e2c12f0225249fd9d721c54b83f48d9352c97c6cacdcb6f31': // Sale
this.handleSale(marketplace, log);
break;
case '0x3cbb63f144840e5b1b0a38a7c19211d2e89de4d7c5faf8b2d3c1776c302d1d33': // Listing
this.handleListing(marketplace, log);
break;
default:
console.log(`Unknown event from ${marketplace}:`, eventSignature);
}
}

handleSale(marketplace, log) {
// Parse sale data
const saleData = this.parseSaleData(log);

console.log(`🎉 NFT Sale on ${marketplace}:`, {
collection: saleData.collection,
tokenId: saleData.tokenId,
price: saleData.price,
buyer: saleData.buyer,
seller: saleData.seller,
txHash: log.transactionHash
});

// Send notification to users
this.sendSaleNotification(marketplace, saleData);
}

handleListing(marketplace, log) {
// Parse listing data
const listingData = this.parseListingData(log);

console.log(`📝 New Listing on ${marketplace}:`, {
collection: listingData.collection,
tokenId: listingData.tokenId,
price: listingData.price,
seller: listingData.seller
});
}

parseSaleData(log) {
// Implementation depends on marketplace contract
return {
collection: log.topics[1],
tokenId: parseInt(log.topics[2], 16),
price: parseInt(log.data.slice(0, 66), 16),
buyer: `0x${log.data.slice(66, 106)}`,
seller: `0x${log.data.slice(106, 146)}`
};
}

parseListingData(log) {
// Implementation depends on marketplace contract
return {
collection: log.topics[1],
tokenId: parseInt(log.topics[2], 16),
price: parseInt(log.data.slice(0, 66), 16),
seller: `0x${log.data.slice(66, 106)}`
};
}

sendSaleNotification(marketplace, saleData) {
// Send to Discord, Telegram, email, etc.
console.log(`Sending notification for ${marketplace} sale`);
}
}

// Usage
const nftMonitor = new NFTMarketplaceMonitor('wss://api.blockeden.xyz/zksync/${accessKey}');
nftMonitor.start();

Error Handling & Reconnection

Robust Error Handling

class RobustWebSocketClient {
constructor(endpoint, options = {}) {
this.endpoint = endpoint;
this.options = {
reconnectInterval: 5000,
maxReconnectAttempts: 10,
heartbeatInterval: 30000,
...options
};

this.ws = null;
this.reconnectAttempts = 0;
this.heartbeatTimer = null;
this.subscriptions = new Map();
this.isConnected = false;
}

connect() {
return new Promise((resolve, reject) => {
try {
this.ws = new WebSocket(this.endpoint);

this.ws.onopen = () => {
console.log('WebSocket connected');
this.isConnected = true;
this.reconnectAttempts = 0;
this.startHeartbeat();
this.resubscribeAll();
resolve();
};

this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleMessage(data);
} catch (error) {
console.error('Error parsing message:', error);
}
};

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

this.ws.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason);
this.isConnected = false;
this.stopHeartbeat();

if (event.code !== 1000) { // Not a normal closure
this.handleReconnection();
}
};

} catch (error) {
reject(error);
}
});
}

startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (this.isConnected && this.ws.readyState === WebSocket.OPEN) {
this.ping();
}
}, this.options.heartbeatInterval);
}

stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}

ping() {
// Send a simple request to check connection
const ping = {
jsonrpc: '2.0',
method: 'net_version',
params: [],
id: 'ping'
};

this.send(ping);
}

send(data) {
if (this.isConnected && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
return true;
} else {
console.warn('WebSocket not connected, queueing message');
return false;
}
}

handleReconnection() {
if (this.reconnectAttempts < this.options.maxReconnectAttempts) {
this.reconnectAttempts++;
const delay = Math.min(
this.options.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1),
30000 // Max 30 seconds
);

console.log(`Reconnecting in ${delay}ms... Attempt ${this.reconnectAttempts}`);

setTimeout(() => {
this.connect().catch((error) => {
console.error('Reconnection failed:', error);
});
}, delay);
} else {
console.error('Max reconnection attempts reached');
}
}

resubscribeAll() {
// Resubscribe to all active subscriptions
this.subscriptions.forEach((handler, subscription) => {
// Implementation depends on your subscription tracking
});
}

disconnect() {
this.stopHeartbeat();
if (this.ws) {
this.ws.close(1000, 'Client disconnecting');
}
}
}

Performance Optimization

Efficient Message Processing

class OptimizedWebSocketProcessor {
constructor(endpoint) {
this.ws = new RobustWebSocketClient(endpoint);
this.messageQueue = [];
this.processing = false;
this.batchSize = 100;
this.batchTimeout = 100; // ms
}

async start() {
await this.ws.connect();
this.ws.onMessage = (data) => {
this.queueMessage(data);
};

this.startBatchProcessor();
}

queueMessage(message) {
this.messageQueue.push({
message,
timestamp: Date.now()
});

// Process immediately if queue is full
if (this.messageQueue.length >= this.batchSize) {
this.processBatch();
}
}

startBatchProcessor() {
setInterval(() => {
if (this.messageQueue.length > 0 && !this.processing) {
this.processBatch();
}
}, this.batchTimeout);
}

async processBatch() {
if (this.processing || this.messageQueue.length === 0) {
return;
}

this.processing = true;
const batch = this.messageQueue.splice(0, this.batchSize);

try {
await this.processBatchAsync(batch);
} catch (error) {
console.error('Error processing batch:', error);
} finally {
this.processing = false;
}
}

async processBatchAsync(batch) {
// Process messages in parallel where possible
const promises = batch.map(item => this.processMessage(item.message));
await Promise.allSettled(promises);
}

async processMessage(message) {
// Your message processing logic
if (message.method === 'eth_subscription') {
const subscription = message.params.subscription;
const result = message.params.result;

// Handle based on subscription type
this.handleSubscriptionMessage(subscription, result);
}
}

handleSubscriptionMessage(subscription, result) {
// Route to appropriate handler
console.log(`Processing subscription ${subscription}:`, result);
}
}

Testing WebSocket Connections

Unit Testing

// Mock WebSocket for testing
class MockWebSocket {
constructor(url) {
this.url = url;
this.readyState = WebSocket.CONNECTING;
this.messages = [];

// Simulate connection
setTimeout(() => {
this.readyState = WebSocket.OPEN;
if (this.onopen) this.onopen();
}, 100);
}

send(data) {
this.messages.push(JSON.parse(data));
}

close() {
this.readyState = WebSocket.CLOSED;
if (this.onclose) this.onclose({ code: 1000, reason: 'Normal closure' });
}

simulateMessage(data) {
if (this.onmessage) {
this.onmessage({ data: JSON.stringify(data) });
}
}
}

// Test example
describe('WebSocket Manager', () => {
let wsManager;
let mockWs;

beforeEach(() => {
global.WebSocket = MockWebSocket;
wsManager = new zkSync EraWebSocketManager('wss://test.example.com');
});

test('should connect and subscribe', async () => {
await wsManager.connect();

const handler = jest.fn();
wsManager.subscribe('newHeads', null, handler);

// Simulate subscription confirmation
mockWs.simulateMessage({
id: 1,
result: 'subscription-id-123'
});

// Simulate new block
mockWs.simulateMessage({
method: 'eth_subscription',
params: {
subscription: 'subscription-id-123',
result: { number: '0x123', hash: '0xabc...' }
}
});

expect(handler).toHaveBeenCalledWith({ number: '0x123', hash: '0xabc...' });
});
});

Best Practices

1. Connection Management

  • Always implement reconnection logic
  • Use heartbeat/ping to detect connection issues
  • Handle network interruptions gracefully
  • Limit concurrent connections

2. Resource Management

  • Unsubscribe from unused subscriptions
  • Implement proper cleanup on disconnect
  • Monitor memory usage for long-running connections
  • Use connection pooling for multiple subscriptions

3. Error Handling

  • Implement exponential backoff for reconnections
  • Log errors for debugging
  • Handle malformed messages gracefully
  • Set connection timeouts

4. Performance

  • Batch process messages when possible
  • Use appropriate buffer sizes
  • Implement message filtering client-side
  • Monitor subscription overhead

5. Security

  • Validate all incoming data
  • Use secure WebSocket connections (wss://)
  • Implement rate limiting
  • Keep API keys secure

Next Steps

  • Learn about [Web3.js Integration]./zksync-era-web3-integration.md for JavaScript development
  • Explore [Ethers.js Integration]./zksync-era-ethers-integration.md for modern web development
  • Check out smart contract interaction patterns for advanced use cases
  • Follow [JSON-RPC API Reference]./zksync-era-json-rpc-api.md for low-level operations

Resources


Cost: 300 CUs / req