Using WebSockets with Etherlink Nodes
This is a joint post with Nomadic Labs.
With version 0.14, the Etherlink EVM node supports requests over both REST HTTP
and WebSockets. WebSocket is a communication protocol that provides
bidirectional, real-time, and persistent communication between a client and
server over a single, long-lived TCP connection. The advantage of WebSocket over
REST HTTP is to provide faster and more efficient data exchange without the
overhead of repeatedly opening and closing connections with the server and
performing TLS handshakes. Another advantage of WebSocket for Etherlink is the
newly added support for the eth_subscribe
method. The server can now send
notifications for new blocks (newHeads
), transactions in the pool (pendingTransactions
) and transaction logs (logs
)
directly to clients, in real time without the need for continuous polling. This
opens up use cases for applications that want to do blockchain analytics and
monitoring with real-time updates from the network.
You can use any websocket framework to connect to Etherlink nodes. This post includes examples for the Web3js and ws NPM modules and the wscat and websocat command-line tools. The rest of this blog post will explore how to use WebSockets in the Etherlink network.
EVM Node Setup
ℹ️ Please refer to the official Etherlink documentation for instructions on how to setup and run an Etherlink EVM node.
To activate the support for WebSockets in your EVM node, an experimental feature
flag must be set. Add the field enable_websocket
to the experimenteal_features
section of the configuration file (assuming location $EVM_NODE_DATA_DIR/config.json
), or add this section as such:
"experimental_features": {
"enable_websocket": true
}
Once this change is made, restart your EVM observer node. If you do not have a
running EVM observer node, you can quickly get started thanks to these new options
of the run
command (the example below is for the Etherlink testnet on Tezos
ghostnet):
octez-evm-node run observer \
--data-dir "$EVM_NODE_DATA_DIR" \
--dont-track-rollup-node \
--network testnet \
--init-from-snapshot
Connecting to a WebSocket
Programmatically
Assuming your EVM node is accessible locally on 127.0.0.1:8545
, you can create
a WebSocket connection with the following (in Javascript, either in the browser
or with node.js):
const WebSocket = require('ws'); // only for node.js
const ws = new WebSocket('ws://127.0.0.1:8545/ws');
// Wait for connection to be established
await new Promise(resolve => {
if (ws.readyState !== ws.OPEN) {
ws.on("open", resolve);
} else {
resolve();
}
})
ws.onmessage = (msg) => {
const data = JSON.parse(msg.data);
// Process websocket messages from server, here we just print them in
// the console
console.log('Received data on websocket:', data)
}
Using CLI tools
One can also use command line tools to connect to WebSockets. Popular options include wscat and websocat.
These tools are primarily used to test WebSocket connections. We use them here as WebSocket clients, openning a connection to the server (the EVM node), sending messages over the WebSocket, and displaying messages received from the server in response. Simply put, they allow interactive communication with a WebSocket server.
Connecting with wscat
wscat -c ws://127.0.0.1:8545/ws
Connected (press CTRL+C to quit)
>
Once you see this prompt you can interact with the server by sending JSON requests (one per line).
The following JSON-RPC call allows to query the latest block for instance.
Connected (press CTRL+C to quit)
> {"jsonrpc": "2.0","method": "eth_getBlockByNumber","params":["latest", false],"id": 1}
< ... // Response from EVM node will be shown here
Connecting with websocat
websocat ws://127.0.0.1:8545/ws -E
There is no prompt for websocat
but it works the same as wscat
.
Sending JSON-RPC Requests on the WebSocket
All Etherlink JSON-RPCs that can be made on the EVM node with REST HTTP can also be made on the WebSocket. For instance, we can issue the following to retrieve the latest block of the chain.
ws.send(JSON.stringify({
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params:["latest", false],
id: 1
}));
When receiving this message, the server will respond on the websocket and we
will see the response in the console (thanks to our ws.onmessage
above).
Received data on websocket: {
jsonrpc: '2.0',
result: {
number: '0x100d0ba',
hash: '0x63d7190ea6de62568d40f0b48b02172130e803136375bdda5f2fa702e323b7f5',
parentHash: '0x3baff862b1d24c4775b93e201adac92e817b14ae3561742740026b7344b63272',
nonce: '0x0000000000000000',
sha3Uncles: '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347',
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
transactionsRoot: '0xe1d6a2219eb75e6f879f6773e939e00df004643d16fbae004d6123f2feb1a09f',
stateRoot: '0x35421a162a343279b8e2e6a2b7d0073484182aed4c4dc02f7130fa20914d77a3',
receiptsRoot: '0x5202257adc3e3cb0a95c2bc3c2217eaed7397f54ff4a021cc23797d341410fd0',
miner: '0xcf02b9ca488f8f6f4e28e37aa1bdd16b3f1b2ad8',
difficulty: '0x0',
totalDifficulty: '0x0',
extraData: '0x',
size: '0x279',
gasLimit: '0x4000000000000',
gasUsed: '0x1589f2',
timestamp: '0x67856fca',
transactions: [
'0x4076ab3c3e2345107082ff13023bead045bcef966c89f8fed1b9f939ac438e81'
],
uncles: [],
baseFeePerGas: '0x3b9aca00',
prevRandao: '0x0000000000000000000000000000000000000000000000000000000000000000'
},
id: 1
}
Subscribing to Events with eth_subscribe
The method eth_susbcribe
is only available on WebSocket connections and allows
clients to subscribe to real-time updates for specific blockchain events, such
as new blocks, pending transactions, or contract event logs.
The response to an eth_subscribe
message consists of two parts:
- Subscription Acknowledgment: Upon successfully creating a subscription, the server responds with a unique subscription ID, confirming the subscription.
- Event Notifications: As relevant events occur (e.g., a new block or log event), the server sends push notifications containing the subscription ID and event data.
The subscription ID can be used with the method eth_unsubscribe
to stop
receiving updates for this particular event.
New Heads
With eth_subscribe
it is possible to receive notifications about new heads of
the Etherlink chain in real-time.
With Messages on the WebSocket
Subscribing to new heads events can be done by sending the following message:
ws.send(JSON.stringify({
jsonrpc: "2.0",
method: "eth_subscribe",
params:["newHeads"],
id: 2
}));
The first message that is sent by the server on the WebSocket contains the subscription ID which we can later use to unsubscribe and to identify messages when several subscriptions are active:
Received data on websocket: { jsonrpc: '2.0', result: '0xc46a38daf682aba1b16da00078d80885', id: 2 }
Then the EVM node will send notifications on the websocket with the blocks contents:
Received data on websocket: {
jsonrpc: '2.0',
method: 'eth_subscription',
params: {
result: {
number: '0x1010e2d',
hash: '0x89a9e075976cae83f069e79d2d49e49db7b804dd5d2854a1cc55531527c0e297',
parentHash: '0x52c05c75291beeeedf5f92647e1b06011949a988cf424b4379e7ac328ea72bce',
nonce: '0x0000000000000000',
sha3Uncles: '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347',
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
transactionsRoot: '0x2d0871bce6286632fbba8ed76e770a9187e2a8451cfd916780e1bb38aa712678',
stateRoot: '0xa6f0be4207991f8e76818b4d391da821d9e15296cf6cedcbb3a272aadec756be',
receiptsRoot: '0x2d5a6b0dffd96fdae789537754039a801b7f62d85b655fa10ed479e47e134ea1',
miner: '0xcf02b9ca488f8f6f4e28e37aa1bdd16b3f1b2ad8',
difficulty: '0x0',
totalDifficulty: '0x0',
extraData: '0x',
size: '0x258',
gasLimit: '0x4000000000000',
gasUsed: '0x0',
timestamp: '0x67868f27',
transactions: [],
uncles: [],
baseFeePerGas: '0x3b9aca00',
prevRandao: '0x0000000000000000000000000000000000000000000000000000000000000000'
},
subscription: '0xc46a38daf682aba1b16da00078d80885'
}
}
Received data on websocket: {
jsonrpc: '2.0',
method: 'eth_subscription',
params: {
result: {
number: '0x1010e2e',
hash: '0x12376ec73ef995f0c5b26b06257ff3c14fa2489f72d80ccfffe5cb377142c417',
parentHash: '0x89a9e075976cae83f069e79d2d49e49db7b804dd5d2854a1cc55531527c0e297',
nonce: '0x0000000000000000',
sha3Uncles: '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347',
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
transactionsRoot: '0x776ced21d739017fa60a4835a66bfce7857db7d7dd663f7394618d7104f1ec2b',
stateRoot: '0x8d329a161f0c5da6d7326257ddad11af5811607ba622d462ce3f21385bebe116',
receiptsRoot: '0x40d25aabdc333e976801012685dd05debaf1cfdf584307075e0514cec2da9370',
miner: '0xcf02b9ca488f8f6f4e28e37aa1bdd16b3f1b2ad8',
difficulty: '0x0',
totalDifficulty: '0x0',
extraData: '0x',
size: '0x279',
gasLimit: '0x4000000000000',
gasUsed: '0x2af0c1',
timestamp: '0x67868f2c',
transactions: [Array],
uncles: [],
baseFeePerGas: '0x3b9aca00',
prevRandao: '0x0000000000000000000000000000000000000000000000000000000000000000'
},
subscription: '0xc46a38daf682aba1b16da00078d80885'
}
}
When one wishes to stop receiving updates on new heads, it is sufficient to call
the eth_unsubscribe
method with the subscription ID:
ws.send(JSON.stringify({
jsonrpc: "2.0",
method: "eth_unsubscribe",
params:["0xc46a38daf682aba1b16da00078d80885"],
id: 3
}));
Received data on websocket: { jsonrpc: '2.0', result: true, id: 3 }
With web3.js
This example uses the web3.js JavaScript library to subscribe to new head block events and display the number of each block in the console.
⚠️ Warning: Notice that we use
WebsocketProvider
otherwiseeth.subscribe
will not be available.
const { Web3 } = require('web3');
const web3 = new Web3(new Web3.providers.WebsocketProvider('ws://127.0.0.1:8545/ws'));
// Subscribe to new block headers
const newBlocksSubscription = await web3.eth.subscribe('newBlockHeaders');
newBlocksSubscription.on('error', error => {
console.log('Error when subscribing to New block header:', error);
});
newBlocksSubscription.on('data', blockhead => {
// Just log the block number
console.log(`New block: ${blockhead.number}`);
});
Block number will be displayed in the console as they are produced:
New block: 16846276
New block: 16846277
New block: 16846278
New block: 16846279
New block: 16846280
New block: 16846281
New block: 16846282
New block: 16846283
To unsubscribe from updates, use the unsubscribe()
method, as
in this example:
newBlocksSubscription.unsubscribe().then(() => {
console.log('Unsubscribed from new block headers.');
});
Then the program disconnects from the websocket and stops writing block numbers to the console.
New Pending Transactions
Using any of these software packages, you can subscribe to multiple events on a single WebSocket connection. For example, this code uses web3.js
to subscribe to both new blocks and new pending transactions:
const { Web3 } = require('web3');
const web3 = new Web3(new Web3.providers.WebsocketProvider('ws://127.0.0.1:8545/ws'));
// Subscribe to new block headers
const newBlocksSubscription = await web3.eth.subscribe('newBlockHeaders');
newBlocksSubscription.on('data', blockhead => {
// Just log the block number
console.log(`New block: ${blockhead.number}`);
});
// Subscribe to pending transactions
const txsSubscription = await web3.eth.subscribe('pendingTransactions');
txsSubscription.on('data', tx => {
// Log full transaction object
console.log('New pending transaction:', tx);
});
When we're done we can unsubscribe from both:
await newBlocksSubscription.unsubscribe();
await txsSubscription.unsubscribe();
Contract Logs
Another useful kind of events for which we can receive notifications are logs
(emitted by a contract). We can provide the address of the smart contract and
the topics we are interested in.
The following snippet simply dumps all logs emitted by smart contracts in Etherlink on the console.
const { Web3 } = require('web3');
const web3 = new Web3(new Web3.providers.WebsocketProvider('ws://127.0.0.1:8545/ws'));
const logsSubscription = await web3.eth.subscribe('logs', {});
logsSubscription.on('data', console.log);
And we'll be notified of things like
{
address: '0xb1ea698633d57705e93b0e40c1077d46cd6a51d8',
topics: [
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
'0x000000000000000000000000f60e84b902e097c1e722e13fed0f14e52f5c5496',
'0x0000000000000000000000002fe633088dbf960821f4a25923a60497dbd06af5'
],
data: '0x0000000000000000000000000000000000000000000000000214e8348c4f0000',
blockNumber: '0x1011582',
transactionHash: '0xf8ae811940ed83a0dc99404beff8f8c1c7572c68d6dfda34f569785ed21544c6',
transactionIndex: '0x0',
blockHash: '0x09c6d3cfb7514d4a43a4011f00ba1a32212b37597f5bd8ed929fcf05682e5844',
logIndex: '0x0',
removed: false
}
{
address: '0x1a71f491fb0ef77f13f8f6d2a927dd4c969ece4f',
topics: [
'0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
'0x000000000000000000000000f60e84b902e097c1e722e13fed0f14e52f5c5496',
'0x0000000000000000000000002fe633088dbf960821f4a25923a60497dbd06af5'
],
data: '0x000000000000000000000000000000000000000000000002b5e3af16b1880000',
blockNumber: '0x1011583',
transactionHash: '0xc7c16dd2c54091653adae7c8ddbe77e49570b8875d9be6b94eeb6fc2689388c4',
transactionIndex: '0x0',
blockHash: '0xbbe906ef295dd5b456d28b0307602dbc909623632ac0fff20cf6ab1d79be27ea',
logIndex: '0x0',
removed: false
}
...
Notifying ERC20 Transfers
This final section presents a small useful application of subscriptions.
For instance we may want to be notified of ERC20 transfers on the Etherlink
Testnet. The following snippet does this by subscribing to logs
events for a
particular topic.
const { Web3 } = require('web3');
const web3 = new Web3(new
Web3.providers.WebsocketProvider('ws://127.0.0.1:8545/ws'));
const abi = [
{
constant: true,
inputs: [],
name: "symbol",
outputs: [{name: "", type: "string"}],
payable: false,
stateMutability: "view",
type: "function"
},
{
constant: true,
inputs: [],
name: "decimals",
outputs: [{name: "", type: "uint8"}],
payable: false,
stateMutability: "view",
type: "function"
},
];
const options = {
topics: [web3.utils.keccak256Wrapper('Transfer(address,address,uint256)')]
};
async function showTransfer(event) {
if (event.topics.length == 3) {
let transaction = web3.eth.abi.decodeLog(
[
{ type: "address", name: "from", indexed: true },
{ type: "address", name: "to", indexed: true },
{ type: "uint256", name: "value", indexed: false }
],
event.data,
event.topics
);
const contract = new web3.eth.Contract(abi, event.address);
const decimals = await contract.methods.decimals().call();
const symbol = await contract.methods.symbol().call();
const amount = Number(transaction.value) / Number(10n ** decimals);
console.log(`${amount} ${symbol}: ${transaction.from} --> ${transaction.to}`);
}
};
const erc20Transfers = await web3.eth.subscribe('logs', options);
erc20Transfers.on('error', (err) => { throw err });
erc20Transfers.on('data', showTransfer);
In the code above, we filter logs on their first topic, which we want to be Transfer
calls (the entrypoint signature needs to be hashed).
Then we call the function showTransfer
on each such new log event. We first decode the Transfer
call to get meaningful values for the parameters, then we fetch the decimals and token symbol from the ERC20 contract to display correct values and units.
You will see in the console all ERC20 transfers on Etherlink displayed as such:
0.15 WXTZ: 0xf60e84b902e097c1e722e13FED0f14E52f5c5496 --> 0x2Fe633088Dbf960821f4A25923A60497dBd06aF5
50 eUSD: 0xf60e84b902e097c1e722e13FED0f14E52f5c5496 --> 0x2Fe633088Dbf960821f4A25923A60497dBd06aF5
0.01 BTC: 0xf60e84b902e097c1e722e13FED0f14E52f5c5496 --> 0x2Fe633088Dbf960821f4A25923A60497dBd06aF5
0.1 WETH: 0xf60e84b902e097c1e722e13FED0f14E52f5c5496 --> 0x2Fe633088Dbf960821f4A25923A60497dBd06aF5
Conclusion
These are just a few examples of how you can use WebSockets to efficiently monitor the activity on Etherlink and respond immediately when certain events happen. This new support will make using existing Ethereum tooling even more straightforward. You can use this code as the basis for an indexer or to respond in real time when users make transactions.
Contact us for more information about using WebSockets with Etherlink.