Blockchain Monitoring

Monitoring the blockchain is possibly an important part of your application on NEM. The most obvious example being monitoring activities involving your account. Two approaches are possible.

The first is polling, meaning that regularly you retrieve the last block and handle it. If your polling interval is significantly smaller than the average block generation time, you can be sure you will handle all blocks. And if your polling interval is bigger and multiple blocks have been generated between two polling, you can still request all new blocks in your handler. This approach has downsides, notably that the client is regularly sending queries, even if activity of interest to the poller occured on the blockchain. This means that the scalability of this approach is limited. But its development is very easy and might be sufficient in most cases.

The second way is subscribing to notifications from the NIS instance you connect to. This is done with websockets. This is the way it is done in the NanoWallet. This is a cleaner approach, but is a bit more complex.

Active monitoring: the polling approach

The pollign approach is very easy to explain, and it translates immediately in runnable code in any language, expecially if it provides REST and JSON libraries. Here are the steps to do in a loop:

  • retrieve the last block
    • if this is a new block, handle it and remember this was the last block we've seen
    • if this is a block we already handled, wait for x seconds.
  • repeat

The number of seconds x we wait (the polling interval) will determine if we will handle all blocks. If it is smaller than the average generation time of a block, we can be sure that all blocks will be retrieved at least once by this code. We call this rapid polling in opposition of slow polling. An interval of 15 seconds seems safe for rapid polling.

Rapid polling

It is quite straight-forward to translate that to code, even in a bash script using httpie and jq. Let's see how this can be done!

First we initialise 2 variables: the monitored address, and the last block seen by the script which is set to 0 at the start of the script:

observed_address="TA6XFSJYZYAIYP7FL7X2RL63647FRMB65YC6CO3G"
last_block_analysed=0

As we will continually monitor the blockchain, our code will be wrapped in an infinite while loop:

while true; do

done

In the loop, we retrieve the last block by sending a GET query to /chain/last-block, and save the result in variable block:

block=$(http :7890/chain/last-block)

This will retrieve the last block in a json document. Here is an example from the mainnet chain showing the format of the document retrieved.

$ http 199.217.112.135:7890/chain/last-block

GET /chain/last-block HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Host: 199.217.112.135:7890
User-Agent: HTTPie/0.9.8

HTTP/1.1 200 OK
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Origin: *
Content-Encoding: gzip
Content-Type: application/json
Server: Jetty(9.2.11.v20150529)
Transfer-Encoding: chunked
Vary: Accept-Encoding, User-Agent

{
    "height": 1106506, 
    "prevBlockHash": {
        "data": "00f421e0b9a8c449bc37f0e19c21c5b1a1c24258eed8d347b0351ff2fe20078e"
    }, 
    "signature": "96a8b050cd0ae341da7ea3aafa11a966821c2e3d0b614800a6d53fe6e5bbf5f2730c64d7f80b43898e1cb3eb0048e7a65f09bee591decfc07092d068064a7804", 
    "signer": "8ca523bd39f350799b0cc150055b713bd7c65e9bbc94b90502c973a47730a431", 
    "timeStamp": 67011602, 
    "transactions": [
        {
            "amount": 133400000000, 
            "deadline": 67033173, 
            "fee": 14000000, 
            "message": {
                "payload": "39333630383163323265303031636336", 
                "type": 1
            }, 
            "recipient": "NBZMQO7ZPBYNBDUR7F75MAKA2S3DHDCIFG775N3D", 
            "signature": "9240c0fc833d2ae351ea2ef5e5b2723ee9e7df87c05c40d754f232a7c58666b556dbfda1ab3804a5d91877e868017e3c7c9b8cd3a23f8fe045668b568c36ab01", 
            "signer": "5d348ef8fa3cacd0fdc6a1532e47d92bb01cc119045b19940fdd9daac7e36890", 
            "timeStamp": 67011573, 
            "type": 257, 
            "version": 1744830465
        }, 
        {
            "amount": 347000000, 
            "deadline": 67015172, 
            "fee": 6000000, 
            "message": {
                "payload": "4e6f64652072657761726473207061796f75743a20726f756e6420323032312d32303234", 
                "type": 1
            }, 
            "mosaics": [], 
            "recipient": "NBKQSUMBKTSFBQ3AMXAZMRNONDXQFQAABYFIMPV4", 
            "signature": "cffee2a656befe6bb30652e2d8716680931e4b892164319ad6299c0a9875088d7efd5a7f17765277948d42ae1a146c8f99903904c4563fcf16c3e40f9db4880e", 
            "signer": "d96366cdd47325e816ff86039a6477ef42772a455023ccddae4a0bd5d27b8d23", 
            "timeStamp": 67011572, 
            "type": 257, 
            "version": 1744830466
        }
    ], 
    "type": 1, 
    "version": 1744830465
}

Parsing that with jq is easy. The height of the block retrieved is available in the top level property height, extract with jq using the filter .height. As the JSON describing the last block is saved in variable $block, we can simply pipe it to jq

chain_height=$(echo "$block" | jq '.height')

The transactions are stored in the top level property named transactions as an array value, that can be empty if no transaction is included in that block. This corresponds to a jq filter '.transactions[]?, the question mark indicating that it can be empty. But we are only interested in the transactions to our address. This means that the property recipient of the transaction should hold a value equal to our $observed_address. This corresponds to a filte select(.recipient=="'$observed_address'"). Assigning the transaction select by our filter to a variable found_recipient results in this code:

found_recipient=$(echo "$block" | jq -r '.transactions[]?|select(.recipient=="'$observed_address'")')

That variable will be empty if no transaction involving the monitored address was found. We can use the bash empty string test [[ -n $variable ]] to determine if the block holds a transaction to our monitored address:

if [[ -n $found_recipient ]] ; then
  echo "Our monitored address received a transaction!"
else
  echo "Not our observed recipient"
fi

Putting all this in a script results in this.

Bash
#!/bin/bash

observed_address="TA6XFSJYZYAIYP7FL7X2RL63647FRMB65YC6CO3G"
last_block_analysed=0
while true; do
    # get last block
    block=$(http :7890/chain/last-block)
    echo "$block"
    chain_height=$(echo "$block" | jq '.height')
    if [[ $last_block_analysed -eq $chain_height ]]; then
        echo "Waiting at $chain_height"
        sleep 15
    else
        last_block_analysed=$chain_height
        found_recipient=$(echo "$block" | jq -r '.transactions[]?|select(.recipient=="'$observed_address'")')
        if [[ -n $found_recipient ]] ; then
          echo "Our monitored address received a transaction!"
        else
          echo "Not our observed recipient"
        fi
    fi
done
Ruby
require 'rest-client'
require 'json'

observed_address="TC6LCF7ZCL5WL4HTE64JPUU2UPVKNZB2LMVUNELV"
last_block_analysed=0
while true
  r=RestClient.get 'localhost:7890/chain/height'
  chain_height=JSON.parse(r.body)["height"]
  if last_block_analysed==chain_height
    puts "sleeping"
    sleep 15
  else
    r=RestClient.get('localhost:7890/chain/last-block')
    b=JSON.parse(r.body)
    last_block_analysed=b["height"]
    b["transactions"].select{|t| t["recipient"]=="observed_address"}.each do |recipient|
      puts "We received a transaction!"
    end
  end
end

Slow polling

If the polling interval is too big, the code has to be prepared to query blocks that were generated during the polling interval.

We will implement this in Ruby. Here are the steps we will execute:

  • retrieve the block chain heigh
  • while the last block analysed has a height smaller than the current blockchain heigh:
    • fetch the block following the last block analysed
    • check if the block has a transaction of interest
    • update the last block analysed to the one we just analysed
  • wait some time
  • repeat

As this requires a bit more logic than the previous example, we will structure our code a bit better. For each operation we will define a function. The first one is to retrieve the current blockchain height, which issues a get request to /chain/height.

def current_height
  res = RestClient.get('http://localhost:7890/chain/height')
  JSON.parse(res.body)["height"]
end

We will also need a function to retrieve a block at a specific height. This is done by passing a JSON payload specifying the height to a POST request:

def block_at(height)
  res = RestClient.post('http://localhost:7890/block/at/public', {'height': height}.to_json , {content_type: :json, accept: :json})
  JSON.parse(res.body)
end

This code sends a POST request to /block/at/public, with a JSON payload specifying the height of the block to return, and sets HTTP headers regarging the content type.

We will also need to check if a block as a transaction to the observed address, and this is done with this function:

def block_has_recipient(block,recipient)
    block["transactions"].collect {|tx| tx["recipient"]}.include?(recipient)
end

What this code is first collect all recipients found in transaction, and then check if this array contains our recipient of interes.

With these helper functions in place, putting the pieces together is a nearly literal translation of the steps highlighted above. We require the needed libraries, define the helper functions, initialise the variables holding the address observed, and the height of the last block analysed (in our case set to the current height), resulting in this code:

require 'rest-client'
require 'json'

def block_at(height)
  res = RestClient.post('http://localhost:7890/block/at/public', {'height': height}.to_json , {content_type: :json, accept: :json})
  JSON.parse(res.body)
end

def current_height
  res = RestClient.get('http://localhost:7890/chain/height')
  JSON.parse(res.body)["height"]
end

def block_has_recipient(block,recipient)
    block["transactions"].collect {|tx| tx["recipient"]}.include?(recipient)
end

observed_address="TDK4QKF7HBHEAFTEUROFMCAFJQGBZTSZ2ZSGZKZM"
last_block_analysed=current_height()

while true
  chain_height=current_height()
  while last_block_analysed<chain_height
    # get block at height last_block_analysed+1
    block=block_at(last_block_analysed+1)
    # analyse block
    if block_has_recipient(block,observed_address)
      puts "transaction found in block at height #{last_block_analysed+1}"
    else
      puts "no watched recipient in block at heigh #{last_block_analysed+1}"
    end
    last_block_analysed=last_block_analysed+1
  end
  sleep 300
end

Passive monitoring: the subscription approach

While in the previous sections our program was actively contacting our NIS instance to check if action should be taken because a new block was generated, we can use a passive approach in which our program subscribes to notification of specific events. This is done over a websocket connection.

The NEM Infrastructure Server listens for websocket connections on port 7778. It expects STOMP formatted messages. If your language provides libraries to communicate with STOMP over websockets, it should be easy to interoperate with NIS. Just note that Websocksets require a handshake, which is visible at the stomp client level. If you are interested in more details, a good explanation is available on the web.

We will work in javascript, for which the library stompjs covers our needs. These can be installed with

npm install -g stompjs

You might find examples using SockJS, and this guide started by using it. However, due to problems with SockJS (such as code working one day but not the day after), we switched to a solution using only stompjs.

The implementation is quite straightforward when you know how and to which messages you need to subscribe. We will work in the nodejs REPL, which you can start with node, or if you use the containers of this guide, with ./ndev repl.js.

The URL you need to connect to for websockets is http://$NIS:7778/w/messages/websocket (replace $NIS with the IP or hostname of the NIS you want to connect to). If you run in the tools container, you can use localhost.

To be notified of new blocks, you subscribe to /blocks/new. The body of the notification is simply the height of the new block.

Now that we have all information, we can take a closer look at the implementation. First we require the libraries:

var stomp=require('stompjs');

Note that if you use the repljs script with ndev repl.js, these are already available and this step is not required.

Then we open the websocket connection and create the STOMP connection over it.

var stompClient = stomp.overWS('ws://localhost:7778/w/messages/websocket');

As mentioned above, websockets require a handshake, and we need to initiate it from the STOMP client. When the connection is established, it will trigger a callback passed as second argument to the connect function:

stompClient.connect({}, callback);

The callback is a function taking as argument the frame received from the server. In this case it will be the frame confirming the connection. In the callback you can subscribe to the messages of interest. In this example we will subscribe to the new block notification, and simply log the height of the new block in the console:

var callback=function(frame){
        stompClient.subscribe('/blocks/new', function(data) {
                             var blockHeight = JSON.parse(data.body);
                             console.log(blockHeight);
                         });
}

Putting it all together, here is our code:

// require libraries if not using repl.js script
var stomp=require('stompjs');
// create a STOMP client over that websocket connection
var stompClient = stomp.overWS('ws://localhost:7778/w/messages/websocket');
// Define the callback function that we want to execute after connection.
// Here we subscribe to new block notifications
var callback=function(frame){
        stompClient.subscribe('/blocks/new', function(data) {
                             var blockHeight = JSON.parse(data.body);
                             console.log(blockHeight);
                         });
}

// Connect and subscribe 
stompClient.connect({}, callback );

This will notify your program when a new block is available, but you would still need to go and retrieve the block to check if it includes transactions involving your account. But we can do better! We can ask to be notified of new transactions involving a specific account. This is done by subscribing to the channel /w/api/account/subscribe with the payload "{'account':'$ADDRESS'}" (replace $ADDRESS by the account's address, all uppercase and without hyphen). Here is an example illustrating the format of the frame you receive:

 Frame {
  command: 'MESSAGE',
  headers: 
   { 'content-length': '541',
     'message-id': '3tbjipuy-147',
     subscription: 'sub-0',
     destination: '/transactions/TA6XFSJYZYAIYP7FL7X2RL63647FRMB65YC6CO3G' },
  body: '{"meta":{"innerHash":{},"id":0,"hash":{"data":"ccd0ab18ea047922b646b82f6171d227a348cfe35ef793930e950e5c82243cdf"},"height":942799},"transaction":{"timeStamp":67191609,"amount":1000000,"signature":"dcb6e30b2a750d5bc95b94a33b620bfc90dbd2a71a730a335315fdf55d859467bc70ae797c91fe52ce0a44fa83db84ecc090cf3b91aea4928f3113ec51b3b907","fee":1000000,"recipient":"TA6XFSJYZYAIYP7FL7X2RL63647FRMB65YC6CO3G","type":257,"deadline":67195209,"message":{},"version":-1744830463,"signer":"73211c5f54b7595ade5cd0c5583b91076e33eb99c8b601cf76043e5a176b4f57"}}\r\n',
  ack: [Function],
  nack: [Function] }

With this information, it is easy to write our program that will log to the console the amount in microXEMs of transactions involving our account:

stomp=require('stompjs');

stompClient = stomp.overWS('ws://localhost:7778/w/messages/websocket');
stompClient.debug = undefined;
stompClient.connect({}, function(frame) {
        stompClient.subscribe('/unconfirmed/TA6XFSJYZYAIYP7FL7X2RL63647FRMB65YC6CO3G', 
                              function(data) {
                                  var body = JSON.parse(data.body);
                                  console.log(body.transaction.amount); }        );
});

You might want to check if this is an incoming our outgoing transaction. But that's left as an exercice for the reader ;-)

If you want to have a look at other websocket features proposed by NIS, the best place to look is the deprecated lightwallet documentation.