The blocksWSWebSocket Channel sends a real-time notification every time a new block is
added to the chain.
Compared to polling the /chain/heightGET endpoint, WebSockets push updates as they happen without the overhead of
repeated API calls.
This tutorial shows how to subscribe to the channel and display each update as it arrives.
importasyncioimportjsonimportosimportrandomimportuuidimportstomperfromwebsocketsimportconnectNODE_URL=os.getenv('NODE_URL','http://libertalia.nemtest.net:7890')WS_URL=NODE_URL.replace(':7890',':7778')print(f'Using node {NODE_URL}')# SockJS has no Python client library.# These helpers wrap the raw WebSocket transport.defsockjs_url(endpoint_url):# SockJS raw WebSocket transport adds a random server and session idserver=random.randint(100,999)session=uuid.uuid4().hexws_base=endpoint_url.replace('http','ws',1)returnf'{ws_base}/{server}/{session}/websocket'asyncdefsend_frame(websocket,frame):# SockJS wraps each client payload as a JSON array of frame stringsawaitwebsocket.send(json.dumps([frame]))asyncdefstomp_connect(websocket):awaitwebsocket.recv()# consume the SockJS open frameawaitsend_frame(websocket,stomper.connect('','',WS_URL,heartbeats=(0,0)))asyncdefstomp_subscribe(websocket,destination,sub_id):awaitsend_frame(websocket,stomper.subscribe(destination,sub_id))asyncdefstomp_unsubscribe(websocket,sub_id):awaitsend_frame(websocket,stomper.unsubscribe(sub_id))asyncdefstomp_disconnect(websocket):awaitsend_frame(websocket,stomper.disconnect())defstomp_messages(raw_frame):# Yield the JSON body of each STOMP MESSAGE in a SockJS data frameif'a'!=raw_frame[0]:# skip 'o' open, 'h' heartbeat, 'c' closereturnforpayloadinjson.loads(raw_frame[1:]):frame=stomper.unpack_frame(payload)if'MESSAGE'==frame['cmd']:yieldjson.loads(frame['body'])asyncdefmain():# Open connectionasyncwithconnect(sockjs_url(f'{WS_URL}/w/messages'))aswebsocket:awaitstomp_connect(websocket)print(f'Connected to {WS_URL}')# Subscribe to the new block channeldestination='/blocks'awaitstomp_subscribe(websocket,destination,'id-0')print(f'Subscribed to {destination} channel')# Read and format each new blocktry:asyncforraw_frameinwebsocket:forblockinstomp_messages(raw_frame):print(f'New block: height={block["height"]:,}'f' harvester={block["signer"][:16].upper()}...')# Unsubscribe on exitfinally:awaitstomp_unsubscribe(websocket,'id-0')awaitstomp_disconnect(websocket)print('Unsubscribed and disconnected')try:asyncio.run(main())exceptKeyboardInterrupt:passexceptExceptionaserror:print(error)
import{Client}from'@stomp/stompjs';importSockJSfrom'sockjs-client';constNODE_URL=process.env.NODE_URL||'http://libertalia.nemtest.net:7890';constWS_URL=NODE_URL.replace(':7890',':7778');console.log(`Using node ${NODE_URL}`);// Open connectionconstclient=newClient({webSocketFactory:()=>newSockJS(`${WS_URL}/w/messages`)});awaitnewPromise(resolve=>{client.onConnect=resolve;client.activate();});console.log(`Connected to ${WS_URL}`);// Read and format each new blockfunctionformatBlock(message){constblock=JSON.parse(message.body);console.log(`New block: height=${block.height.toLocaleString()}`+` harvester=${block.signer.substring(0,16).toUpperCase()}...`);}// Subscribe to the new block channelconstdestination='/blocks';constsubscription=client.subscribe(destination,formatBlock,{id:'id-0'});console.log(`Subscribed to ${destination} channel`);// Unsubscribe on exitprocess.on('SIGINT',()=>{subscription.unsubscribe();client.deactivate();console.log('Unsubscribed and disconnected');process.exit(0);});
//JAVA 21+//DEPS com.fasterxml.jackson.core:jackson-databind:2.17.1//DEPS org.glassfish.tyrus.bundles:tyrus-standalone-client:2.2.0importjava.io.IOException;importjava.net.URI;importjava.util.UUID;importjava.util.concurrent.CompletableFuture;importjava.util.concurrent.ThreadLocalRandom;importcom.fasterxml.jackson.databind.JsonNode;importcom.fasterxml.jackson.databind.ObjectMapper;importcom.fasterxml.jackson.databind.node.ArrayNode;importjakarta.websocket.ClientEndpoint;importjakarta.websocket.ContainerProvider;importjakarta.websocket.OnMessage;importjakarta.websocket.RemoteEndpoint;importjakarta.websocket.Session;importjakarta.websocket.WebSocketContainer;@ClientEndpointpublicfinalclassListenNewBlocks{privatestaticfinalObjectMapperJSON_MAPPER=newObjectMapper();privatefinalCompletableFuture<Void>openFuture=newCompletableFuture<>();privatefinalStringnodeUrl=System.getenv().getOrDefault("NODE_URL","http://libertalia.nemtest.net:7890");privatefinalStringwsUrl=nodeUrl.replace(":7890",":7778");publicstaticvoidmain(finalString[]args){try{newListenNewBlocks().run();}catch(finalExceptionex){System.out.println(null==ex.getMessage()?ex.toString():ex.getMessage());}}privatevoidrun()throwsException{System.out.printf("Using node %s%n",nodeUrl);// Open connectionfinalWebSocketContainercontainer=ContainerProvider.getWebSocketContainer();finalSessionsession=container.connectToServer(this,URI.create(sockJsUrl(wsUrl+"/w/messages")));finalRemoteEndpoint.Basicremote=session.getBasicRemote();openFuture.join();sendFrame(remote,"CONNECT\naccept-version:1.1,1.0\n"+"heart-beat:0,0\nhost:"+wsUrl+"\n\n\0");System.out.printf("Connected to %s%n",wsUrl);// Subscribe to the new block channelfinalStringdestination="/blocks";sendFrame(remote,"SUBSCRIBE\nid:id-0\ndestination:"+destination+"\nack:auto\n\n\0");System.out.printf("Subscribed to %s channel%n",destination);// Unsubscribe on exitRuntime.getRuntime().addShutdownHook(newThread(()->{try{sendFrame(remote,"UNSUBSCRIBE\nid:id-0\n\n\0");sendFrame(remote,"DISCONNECT\n\n\0");session.close();System.out.println("Unsubscribed and disconnected");}catch(finalIOExceptionex){thrownewIllegalStateException(ex);}}));newCompletableFuture<Void>().join();}// Read and format each new block@OnMessagepublicvoidonMessage(finalStringrawFrame)throwsIOException{if("o".equals(rawFrame)){openFuture.complete(null);return;}if(!rawFrame.startsWith("a"))return;for(finalJsonNodepayload:JSON_MAPPER.readTree(rawFrame.substring(1))){finalStringframe=payload.asText();if(!frame.startsWith("MESSAGE\n"))continue;finalintbodyStart=frame.indexOf("\n\n")+2;finalStringbody=frame.substring(bodyStart).replace("\0","");finalJsonNodeblock=JSON_MAPPER.readTree(body);System.out.printf("New block: height=%,d harvester=%s...%n",block.get("height").asLong(),block.get("signer").asText().substring(0,16).toUpperCase());}}privatestaticStringsockJsUrl(finalStringendpoint){finalintserver=ThreadLocalRandom.current().nextInt(100,1000);returnendpoint.replaceFirst("http","ws")+"/"+server+"/"+UUID.randomUUID().toString().replace("-","")+"/websocket";}privatestaticvoidsendFrame(finalRemoteEndpoint.Basicremote,finalStringframe)throwsIOException{finalArrayNodepayload=JSON_MAPPER.createArrayNode();payload.add(frame);remote.sendText(payload.toString());}}
The snippet uses the NODE_URL environment variable to set the NEM node.
If no value is provided, a default one is used.
WS_URL defines the WebSocket endpoint for the same node.
It is derived from NODE_URL by replacing port 7890, the default HTTP API port, with 7778, the default NIS
WebSocket port.
The program runs until interrupted with Ctrl+C, which triggers the unsubscribe step before closing the connection.
Python SockJS helpers
There is no SockJS client library for Python, so a few small helper methods are defined at the top of the file
for convenience.
// Open connectionconstclient=newClient({webSocketFactory:()=>newSockJS(`${WS_URL}/w/messages`)});awaitnewPromise(resolve=>{client.onConnect=resolve;client.activate();});console.log(`Connected to ${WS_URL}`);
// Open connectionfinalWebSocketContainercontainer=ContainerProvider.getWebSocketContainer();finalSessionsession=container.connectToServer(this,URI.create(sockJsUrl(wsUrl+"/w/messages")));finalRemoteEndpoint.Basicremote=session.getBasicRemote();openFuture.join();sendFrame(remote,"CONNECT\naccept-version:1.1,1.0\n"+"heart-beat:0,0\nhost:"+wsUrl+"\n\n\0");System.out.printf("Connected to %s%n",wsUrl);
The first step is to open a connection to the node's /w/messages endpoint and start a STOMP session
over it.
# Subscribe to the new block channeldestination='/blocks'awaitstomp_subscribe(websocket,destination,'id-0')print(f'Subscribed to {destination} channel')
// Subscribe to the new block channelconstdestination='/blocks';constsubscription=client.subscribe(destination,formatBlock,{id:'id-0'});console.log(`Subscribed to ${destination} channel`);
// Subscribe to the new block channelfinalStringdestination="/blocks";sendFrame(remote,"SUBSCRIBE\nid:id-0\ndestination:"+destination+"\nack:auto\n\n\0");System.out.printf("Subscribed to %s channel%n",destination);
The code subscribes to the blocksWS channel.
The node then notifies subscribers every time a new block is added to the chain (approximately every minute).
Notifications are not always evenly spaced.
When the node adds several blocks at once, for example while catching up with its peers, the channel delivers them in
a quick burst, still one notification per block.
The subscription is given an id (id-0) which is used to unsubscribe on exit.
Each incoming message is then passed to the formatting logic below.
# Read and format each new blocktry:asyncforraw_frameinwebsocket:forblockinstomp_messages(raw_frame):print(f'New block: height={block["height"]:,}'f' harvester={block["signer"][:16].upper()}...')
// Read and format each new blockfunctionformatBlock(message){constblock=JSON.parse(message.body);console.log(`New block: height=${block.height.toLocaleString()}`+` harvester=${block.signer.substring(0,16).toUpperCase()}...`);}
// Read and format each new block@OnMessagepublicvoidonMessage(finalStringrawFrame)throwsIOException{if("o".equals(rawFrame)){openFuture.complete(null);return;}if(!rawFrame.startsWith("a"))return;for(finalJsonNodepayload:JSON_MAPPER.readTree(rawFrame.substring(1))){finalStringframe=payload.asText();if(!frame.startsWith("MESSAGE\n"))continue;finalintbodyStart=frame.indexOf("\n\n")+2;finalStringbody=frame.substring(bodyStart).replace("\0","");finalJsonNodeblock=JSON_MAPPER.readTree(body);System.out.printf("New block: height=%,d harvester=%s...%n",block.get("height").asLong(),block.get("signer").asText().substring(0,16).toUpperCase());}}
The body of each incoming message is the new block, following the Block
schema.
For each message, the snippet prints two of its fields:
height: The height of the new block.
signer: The public key of the harvester account that produced the block.
A NEM block does not include its own hash in this payload, so this tutorial identifies each block by its height
and also prints the harvester's signer.
New blocks are not yet final
New blocks are already part of the chain but not yet irreversible.
Until enough subsequent blocks surpass the rewrite limit, rollbacks are still possible.
After a rollback, the channel can even report a block with a lower height than one already received, because the
incoming blocks replace blocks that were reported earlier.
// Unsubscribe on exitprocess.on('SIGINT',()=>{subscription.unsubscribe();client.deactivate();console.log('Unsubscribed and disconnected');process.exit(0);});
// Unsubscribe on exitRuntime.getRuntime().addShutdownHook(newThread(()->{try{sendFrame(remote,"UNSUBSCRIBE\nid:id-0\n\n\0");sendFrame(remote,"DISCONNECT\n\n\0");session.close();System.out.println("Unsubscribed and disconnected");}catch(finalIOExceptionex){thrownewIllegalStateException(ex);}}));
When the program is interrupted (Ctrl+C), the code unsubscribes from the channel and ends the STOMP session before
closing the connection.
This ensures a clean disconnection from the node.
Using node http://libertalia.nemtest.net:7890
Connected to http://libertalia.nemtest.net:7778
Subscribed to /blocks channel
New block: height=682,352 harvester=95BA0E864CD5EDB3...
New block: height=682,353 harvester=95BA0E864CD5EDB3...
New block: height=682,354 harvester=7C814B3B44B1A98B...
New block: height=682,355 harvester=95BA0E864CD5EDB3...
New block: height=682,356 harvester=95BA0E864CD5EDB3...
Unsubscribed and disconnected
The output shows:
Connection (line 2): The STOMP session is established over the node's WebSocket endpoint at port 7778.
Subscription (line 3): The /blocks channel is subscribed.
New blocks (lines 4-8): New block notifications arrive approximately every minute.
Unsubscribe (line 9): On Ctrl+C, the code unsubscribes and disconnects.