How to listen to for chaincode events

Majra Čaluk
Mar 5 · 11 min read

So far we talked about:

In this article we will talk about different types of channel-based events and event listeners, when to use them and particularly we will learn how to implement the chaincode event listener. The article is simplified and detail explained version of official tutorial about this topic so we encourage you to read that too.

About channel-based events and event listeners

Generally speaking, event is an action or occurrence detected by a program. The user clicks the mouse button is example of one event. Programs/applications must have implemented some kind of the event listener to be able to listen to for events and take certain actions when they occur.

Channel-based events are specific to a single channel and they happen when blocks are added to the channel ledger. A client application may use the Fabric Node.js client to register a listener to receive blocks as they are added to the channel ledger. The Fabric Node.js client can also assist client applications by processing the incoming blocks and looking for specific transactions or chaincode events. Based on received informations, client applications can take certain actions.

In Hyperledger Fabric, there are three types of event listeners:

  • Block Listener — we use it when there is a need to monitor for new blocks being added to the ledger. The Fabric client Node.js will be notified when a new block is committed to the ledger on the peer. After that, application can take certain actions.

Implementation of the chaincode event listener

In the CryptoKajmak application we implemented the chaincode event that sends notifications to the end users every time the kajmak has been deleted from the ledger due to the expiration date. Notifications have this form:

Kajmak with ID kajmakID whose owner is owner was deleted because it has expired on date expirationDate

For example,

Kajmak with ID 4 whose owner is majra was deleted because it has expired on date 27.02.2019. 11:30 am

Notifications are written in the local file called notifikacije.txt that is located on the user’s computer in the application’s folder (crypto-kajmak folder). That notifications can be used by the client application or some other external application.

Process of the chaincode event implementation has two main steps:

  1. Call SetEvent method in the chaincode

Note: you need to be familiar with JavaScript promises.

Let’s begin with the first step.


STEP 1: Call SetEvent method in the chaincode

This step is very easy.

First, we need to find (or write, if the function does not already exists) the function in the chaincode file for which we want to emit events. In our case, that is the deleteKajmak function.

Code of the deleteKajmak function without SetEvent method in kajmak-chaincode.go file:

//deleteKajmak method definitionfunc (s *SmartContract) deleteKajmak(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {     if len(args) != 8 {
return shim.Error("Incorrect number of arguments. Expecting 8")
}
err := APIstub.DelState(args[0])
if (err != nil) {
return shim.Error(fmt.Sprintf("Failed to delete kajmak: %s", args[0]))
}
fmt.Printf("- deleteKajmak:\n%s\n", args[0])
return shim.Success(nil);
}

Next, we will call SetEvent method. That is shim.ChaincodeStubInterface API method that looks like this

SetEvent(name string, payload []byte) error

This method is usually placed below PutState, DelState or similar methods which interact with the ledger. It has two parameters. First is the name of the chaincode event. Data type is string. Second one is the payload (data) that we want to emit from within our chaincode. Data type is bytes.

Let’s create our payload data (notification) that we will emit from the chaincode (first as a string and then we will convert it into bytes):

eventPayload := "Kajmak with ID " + args[0] + " whose owner is " + args[2] + " was deleted because it has expired on date " + args[7]payloadAsBytes := []byte(eventPayload)

Now, we will call SetEvent method with proper arguments. Name of our event will be deleteEvent. Below SetEvent function, we will check if there was some error.

eventErr := APIstub.SetEvent("deleteEvent",payloadAsBytes)
if (eventErr != nil) {
return shim.Error(fmt.Sprintf("Failed to emit event"))
}

And that is all we need to add in our chaincode.

Function deleteKajmak in the kajmak-chaincode.go file now look like this:

//deleteKajmak method definitionfunc (s *SmartContract) deleteKajmak(APIstub shim.ChaincodeStubInterface, args []string) sc.Response {     if len(args) != 8 {
return shim.Error("Incorrect number of arguments. Expecting 8")
}
err := APIstub.DelState(args[0])
if (err != nil) {
return shim.Error(fmt.Sprintf("Failed to delete kajmak: %s", args[0]))
}
eventPayload := "Kajmak with ID " + args[0] + " whose owner is " + args[2] + " was deleted because it has expired on date " + args[7] payloadAsBytes := []byte(eventPayload)
eventErr := APIstub.SetEvent("deleteEvent",payloadAsBytes)
if (eventErr != nil) {
return shim.Error(fmt.Sprintf("Failed to emit event"))
}
fmt.Printf("- deleteKajmak:\n%s\n", args[0])
return shim.Success(nil);
}

STEP 2: Implement Event Listener using client Node.js on the client application side

As we already said, a client application may use the Fabric Node.js client to register an event listener. That is exactly what we will do in the CryptoKajmak application to listen to for events emitted from within the chaincode.

Fabric Node.js client is "placed" in the helper.js file and that file is the point where the client application (CryptoKajmak) and the blockchain network interact with each other. We want to listen to for events from deleteKajmak function in the chaincode so we will implement the chaincode event listener in deleteKajmak function in helper.js file.

Code that we need for listening to chaincode events is placed after we sent proposal for ledger update and before we send proposal response to the orderer in helper.js file.

We will show deleteKajmak function in helper.js file before and after implementation of the chaincode event listener and after that we will write detail explanations.

Function deleteKajmak in helper.js file before implementing the chaincode listener is listed below:

var deleteKajmak = async function(username,userOrg,kajmakData) {
var error_message = null;
try {
var array = kajmakData.split("-");
console.log(array);
var key = array[0]
var name = array[1]
var owner = array[2]
var animal = array[3]
var location = array[4]
var quantity = array[5]
var productionDate = array[6]
var expirationDate = array[7]
var client = await getClientForOrg(userOrg,username);
logger.debug('Successfully got the fabric client for the organization "%s"', userOrg);
var channel = client.getChannel('mychannel');
if(!channel) {
let message = util.format('Channel %s was not defined in the connection profile', channelName);
logger.error(message);
throw new Error(message);
}
var targets = null;
if(userOrg == "Org1") {
targets = ['peer0.org1.example.com'];
} else if(userOrg == "Org2") {
targets = ['peer0.org2.example.com'];
}
var tx_id = client.newTransactionID();
console.log("Assigning transaction_id: ", tx_id._transaction_id);
var tx_id_string = tx_id.getTransactionID();
var request = {
targets: targets,
chaincodeId: 'kajmak-app',
fcn: 'deleteKajmak',
args: [key, name, owner, animal, location, quantity, productionDate, expirationDate],
chainId: channel,
txId: tx_id
};
let results = await channel.sendTransactionProposal(request);
var proposalResponses = results[0];
var proposal = results[1];
let isProposalGood = false;
if (proposalResponses && proposalResponses[0].response && proposalResponses[0].response.status === 200) {
isProposalGood = true;
console.log('Transaction proposal was good');
} else {
console.error('Transaction proposal was bad');
}
if (isProposalGood) {
console.log(util.format('Successfully sent Proposal and received ProposalResponse: Status - %s, message - "%s"', proposalResponses[0].response.status, proposalResponses[0].response.message));
var promises = [];
var requestMain = {
txId: tx_id,
proposalResponses: proposalResponses,
proposal: proposal
};
var sendPromise = channel.sendTransaction(requestMain);
promises.push(sendPromise);
let results = await Promise.all(promises);
logger.debug(util.format('------->>> R E S P O N S E : %j', results));
let response = results.pop(); // orderer results are last in the results
if (response.status === 'SUCCESS') {
logger.info('Successfully sent transaction to the orderer.');
} else {
error_message = util.format('Failed to order the transaction. Error code: %s',response.status);
logger.debug(error_message);
}
} else {
error_message = util.format('Failed to send Proposal and receive all good ProposalResponse');
logger.debug(error_message);
}
} catch(error) {
logger.error('Failed to delete kajmak with error: %s', error.toString());
}
};

Brief explanation of the code above:

Proposal for ledger update is sent to the endorsing peers using sendTransactionProposal method from Channel class. If proposal is good, proposal response is sent to the orderer using sendTransaction method from Channel class. Orderer then orders transactions into block and broadcast block to the commiting peers who commit block to the ledger.

Function deleteKajmak in helper.js file after implementing the chaincode listener is listed below:

var deleteKajmak = async function(username,userOrg,kajmakData) {
var error_message = null;
try {
var array = kajmakData.split("-");
console.log(array);
var key = array[0]
var name = array[1]
var owner = array[2]
var animal = array[3]
var location = array[4]
var quantity = array[5]
var productionDate = array[6]
var expirationDate = array[7]
var client = await getClientForOrg(userOrg,username);
logger.debug('Successfully got the fabric client for the organization "%s"', userOrg);
var channel = client.getChannel('mychannel');
if(!channel) {
let message = util.format('Channel %s was not defined in the connection profile', channelName);
logger.error(message);
throw new Error(message);
}
var targets = null;
if(userOrg == "Org1") {
targets = ['peer0.org1.example.com'];
} else if(userOrg == "Org2") {
targets = ['peer0.org2.example.com'];
}
var tx_id = client.newTransactionID();
console.log("Assigning transaction_id: ", tx_id._transaction_id);
var tx_id_string = tx_id.getTransactionID();
var request = {
targets: targets,
chaincodeId: 'kajmak-app',
fcn: 'deleteKajmak',
args: [key, name, owner, animal, location, quantity, productionDate, expirationDate],
chainId: channel,
txId: tx_id
};
let results = await channel.sendTransactionProposal(request);
var proposalResponses = results[0];
var proposal = results[1];
let isProposalGood = false;
if (proposalResponses && proposalResponses[0].response && proposalResponses[0].response.status === 200) {
isProposalGood = true;
console.log('Transaction proposal was good');
} else {
console.error('Transaction proposal was bad');
}
if (isProposalGood) {
console.log(util.format('Successfully sent Proposal and received ProposalResponse: Status - %s, message - "%s"', proposalResponses[0].response.status, proposalResponses[0].response.message));
var promises = [];
let event_hubs = channel.getChannelEventHubsForOrg();
event_hubs.forEach((eh) => {
logger.debug('invokeDeleteKajmakEventPromise - setting up event');
console.log(eh);
let invokeEventPromise = new Promise((resolve, reject) => {
let regid = null;
let event_timeout = setTimeout(() => {
if(regid) {
let message = 'REQUEST_TIMEOUT:' + eh.getPeerAddr();
logger.error(message);
eh.unregisterChaincodeEvent(regid);
eh.disconnect();
}
reject();
}, 20000);
regid = eh.registerChaincodeEvent('kajmak-app', 'deleteEvent',(event, block_num, txnid, status) => {
console.log('Successfully got a chaincode event with transid:'+ txnid + ' with status:'+status);
let event_payload = event.payload.toString();
console.log(event_payload);
if(event_payload.indexOf(array[0]) > -1) {
clearTimeout(event_timeout);
//Chaincode event listeners are meant to run continuously
//Therefore the default to automatically unregister is false
//So in this case we want to shutdown the event listener once
// we see the event with the correct payload
eh.unregisterChaincodeEvent(regid);
console.log('Successfully received the chaincode event on block number '+ block_num);
resolve(event_payload);
} else {
console.log('Successfully got chaincode event ... just not the one we are looking for on block number '+ block_num);
}
}, (err) => {
clearTimeout(event_timeout);
logger.error(err);
reject(err);
}
//no options specified
//startBlock will default to latest
//endBlock will default to MAX
//unregister will default to false
//disconnect will default to false
);
eh.connect(true);
});
promises.push(invokeEventPromise);
console.log(eh.isconnected());
});

var requestMain = {
txId: tx_id,
proposalResponses: proposalResponses,
proposal: proposal
};
var sendPromise = channel.sendTransaction(requestMain);
promises.push(sendPromise);
let results = await Promise.all(promises);
logger.debug(util.format('------->>> R E S P O N S E : %j', results));
let response = results.pop(); // orderer results are last in the results
if (response.status === 'SUCCESS') {
logger.info('Successfully sent transaction to the orderer.');
} else {
error_message = util.format('Failed to order the transaction. Error code: %s',response.status);
logger.debug(error_message);
}

// now see what each of the event hubs reported
for(let i in results) {
let event_hub_result = results[i];
let event_hub = event_hubs[i];
logger.debug('Event results for event hub :%s',event_hub.getPeerAddr());
if(typeof event_hub_result === 'string') {
logger.debug(event_hub_result);
var rezultat = {event_payload : event_hub_result};
return rezultat;
} else {
if(!error_message) error_message = event_hub_result.toString();
logger.debug(event_hub_result.toString());
}
}
} else {
error_message = util.format('Failed to send Proposal and receive all good ProposalResponse');
logger.debug(error_message);
}
} catch(error) {
logger.error('Failed to delete kajmak with error: %s', error.toString());
}
};

Explanation of main methods used in the code above:

getChannelEventHubsForOrg(mspid)

Method from the Channel class. Returns a list (array) of ChannelEventHub instances based on the peers that are defined in this channel that are in the organization. ChannelEventHub object has two parameters:

If we omit optional mspid parameter which represents mspid of an organization, the current organization from currently set Fabric client will be used.

registerChaincodeEvent(ccid, eventname, onEvent, onError, options)

Method from the ChannelEventHub class. Registers a listener to receive chaincode events. Returns an object that should be treated as an opaque handle used to unregister chaincode event by using registerChaincodeEvent method. Parameters are:

If the last parameter options is not specified, default settings are used: unregister: false, disconnect: false and the startBlock will be the last block.

unregisterChaincodeEvent(listener_handle, throwError)

Method from the ChannelEventHub class. Unregisters the chaincode event listener represented by the listener_handle object returned by the registerChaincodeEvent method. Parameters are:

connect(full_block)

Method from the ChannelEventHub class. Establishes a connection with the peer event source. A parameter full_block is boolean type and it indicates that the connection with the peer will be sending full blocks or filtered blocks to this ChannelEventHub. We want to read payload that is sent from within the chaincode so we need to recieve unfiltered block and the full_block will have value of true.

disconnect()

Disconnects the event hub from the peer event source. Will close all event listeners and send an Error object with the message “ChannelEventHub has been shutdown” to all listeners that provided an “onError” callback.

setTimeout

This method calls a function or evaluates an expression after a specified number of milliseconds. For example, to display an alert box after 3 seconds (3000 milliseconds):

setTimeout(function(){ alert("Hello"); }, 3000);

clearTimeout

Prevent the function set with the setTimeout to execute.


Brief explanation of the deleteKajmak function in helper.js file after implementing the chaincode listener:

Proposal for ledger update is sent to the endorsing peers. If the transaction proposal is good, the Fabric Node.js client registers listener to listen to for events emitted from within the chaincode. The event timeout is set to 20 seconds. That means that the listener will wait 20 seconds and if the event is not received within that time, listener will unregister and disconnect the event hub from the peer source.

When new block is added to the ledger, Node.js client examines that block and searches for the event with the specified name (“deleteEvent”).If the event with the specified name is received from within the specified chaincode ("kajmak-app"), clearTimeout function is called to stop execution of the setTimeout function, listener is unregistered and event payload is returned (notification about kajmak deletion) to the server. The server then writes event payload to the local file called notifikacije.txt.


And that’s it.

As we mentioned in the previous article, we provided you final and fully developed version of the CryptoKajmak blockchain web application that can be found here.

CryptoKajmak — Hyperledger Fabric Web Application Tutorial

CryptoKajmak is Hyperledger Fabric Web Application developed using Node SDK. This software solution tries to eliminate problems for perishable goods (kajmak in our case) that exist in trade industry by using new blockchain technology. Application is owned by Comtrade Industry.

Majra Čaluk

Written by

Junior Software Developer @ Comtrade Industry, CCENT

CryptoKajmak — Hyperledger Fabric Web Application Tutorial

CryptoKajmak is Hyperledger Fabric Web Application developed using Node SDK. This software solution tries to eliminate problems for perishable goods (kajmak in our case) that exist in trade industry by using new blockchain technology. Application is owned by Comtrade Industry.