Setting up RESTful API Server for Hyperledger Fabric With NodeJS SDK

Written by varunraj22 | Published 2018/03/26
Tech Story Tags: nodejs | hyperledger | dlt | hyperledger-fabric | blockchain

TLDRvia the TL;DR App

Learn how to setup a NodeJS server for your Blockchain network to allow multiple users to interact with the chain easily.

In our previous article, we learnt about writing your first simple Hyperledger Fabric Chaincode in Go and how to set up your development environment for Hyperledger Fabric. This article focuses on how to build a NodeJS Server with ExpressJS for your Hyperledger Fabric Network.

PRE REQUISITES

Before I jump in any further into this article, I will assume that you’re familiar with setting up the network with multiple organisations and also assume that there is a network already running on your machine. (If not please follow our previous articles in completing them.)

It’s also great if you have prior experience in the following,

  • JavaScript OOP concepts
  • Javascript Promise
  • Server Routing and HTTP Methods.
  • ExpressJS

STEP 1: SETTING UP THE PROJECT

As the first step in any project, we will be creating the folder and the file structures.

mkdir myappcd myapptouch index.jsnpm install express fabric-ca-client fabric-client body-parser --save

The packages fabric-ca-client & fabric-client are the ones which help us to interact with the Fabric network and express is to create the web server for RESTFul API and finally body-parser to parse the data passed in the request body.

STEP 2: CREATE CONNECTION PROFILE AND CRYPTO CONFIG

After setting up we need to create a common connection profile that has information about the current organization’s peers, orderer and CA so that the client can communicate to corresponding services.

I’ve created a file under Config/ConnectionProfile.yml

name: "Org1 Client"version: "1.0"

client:  organization: Org1  credentialStore:    path: "./hfc-key-store"    cryptoStore:      path: "./hfc-key-store"

channels:  mychannel:    orderers:      - orderer.example.com    peers:      peer0.org1.example.com:        endorsingPeer: true        chaincodeQuery: true        ledgerQuery: true        eventSource: true      peer0.org2.example.com:        endorsingPeer: true        chaincodeQuery: false        ledgerQuery: true        eventSource: false

organizations:  Org1:    mspid: Org1MSP    peers:      - peer0.org1.example.com      - peer1.org1.example.com    certificateAuthorities:      - ca.org1.example.com    adminPrivateKey:      path: crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/keystore/1a11ffdebfb3bba13a7738dfa820a505002d29ba3e812657a127f27ba79345e5_sk    signedCert:      path: crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/signcerts/Admin@org1.example.com-cert.pem

orderers:  orderer.example.com:    url: grpcs://localhost:7050    grpcOptions:      ssl-target-name-override: orderer.example.com      grpc-max-send-message-length: 15    tlsCACerts:      path: crypto-config/ordererOrganizations/example.com/msp/tlscacerts/tlsca.example.com-cert.pem

peers:  peer0.org1.example.com:    url: grpcs://localhost:7051    eventUrl: grpcs://localhost:7053    grpcOptions:      ssl-target-name-override: peer0.org1.example.com      grpc.keepalive_time_ms: 600000    tlsCACerts:      path: crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/msp/tlscacerts/tlsca.org1.example.com-cert.pem

  peer1.org1.example.com:    url: grpcs://localhost:8051    eventUrl: grpcs://localhost:8053    grpcOptions:      ssl-target-name-override: peer1.org1.example.com      grpc.keepalive_time_ms: 600000    tlsCACerts:      path: crypto-config/peerOrganizations/org1.example.com/peers/peer1.org1.example.com/msp/tlscacerts/tlsca.org1.example.com-cert.pem

certificateAuthorities:  ca.org1.example.com:    url: https://localhost:7054    httpOptions:      verify: false    tlsCACerts:      path: crypto-config/peerOrganizations/org1.example.com/ca/ca.org1.example.com-cert.pem    registrar:      - enrollId: admin        enrollSecret: adminpw    caName: ca-org1

The important thing to look at here is the credentials store that the application will be using to store the keys and certificates.

For detailed explanation of the connection profile check out https://fabric-sdk-node.github.io/tutorial-network-config.html.

STEP 3. CREATE SCRIPT FOR GENERATING ADMIN CRYPTO MATERIALS.

In order to submit a transaction from the client, you need to set user content in the SDK. However, before that we need to enroll and get the certificates for the admin of the organization. Here’s a simple script to do that, ./enrollAdmin.js

'use strict';var fabricClient = require('./Config/FabricClient');var FabricCAClient = require('fabric-ca-client');

var connection = fabricClient;var fabricCAClient;var adminUser;

connection.initCredentialStores().then(() => {  fabricCAClient = connection.getCertificateAuthority();  return connection.getUserContext('admin', true);}).then((user) => {  if (user) {    throw new Error("Admin already exists");  } else {    return fabricCAClient.enroll({      enrollmentID: 'admin',      enrollmentSecret: 'adminpw',      attr_reqs: [          { name: "hf.Registrar.Roles" },          { name: "hf.Registrar.Attributes" }      ]    }).then((enrollment) => {      console.log('Successfully enrolled admin user "admin"');      return connection.createUser(          {username: 'admin',              mspid: 'Org1MSP',              cryptoContent: { privateKeyPEM: enrollment.key.toBytes(), signedCertPEM: enrollment.certificate }          });    }).then((user) => {      adminUser = user;      return connection.setUserContext(adminUser);    }).catch((err) => {      console.error('Failed to enroll and persist admin. Error: ' + err.stack ? err.stack : err);      throw new Error('Failed to enroll admin');    });  }}).then(() => {    console.log('Assigned the admin user to the fabric client ::' + adminUser.toString());}).catch((err) => {    console.error('Failed to enroll admin: ' + err);});

There’s also a file called ./Config/FabricClient that extends the functions of FabricClient SDK to provide enhanced features.

Now you can run the above script to generate an admin certificate and that will be stored in the crypto-store mentioned in the ConnectionProfile.yml

node enrollAdmin.js

The corresponding result should look like this,

var FabricClient = require('fabric-client');var fs = require('fs');var path = require('path');

var configFilePath = path.join(__dirname, './ConnectionProfile.yml');const CONFIG = fs.readFileSync(configFilePath, 'utf8')

class FBClient extends FabricClient {    constructor(props) {        super(props);    }

    submitTransaction(requestData) {        var returnData;        var _this = this;        var channel = this.getChannel();        var peers = this.getPeersForOrg();        var event_hub = this.getEventHub(peers[0].getName());        return channel.sendTransactionProposal(requestData).then(function (results) {            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 {                throw new Error(results[0][0].details);                console.error('Transaction proposal was bad');            }            returnData = proposalResponses[0].response.payload.toString();            returnData = JSON.parse(returnData);

            if (isProposalGood) {                console.log(                    'Successfully sent Proposal and received ProposalResponse: Status - %s, message - "%s"',                    proposalResponses[0].response.status, proposalResponses[0].response.message);

                var request = {                    proposalResponses: proposalResponses,                    proposal: proposal                };

                var transaction_id_string = requestData.txId.getTransactionID();                var promises = [];

                var sendPromise = channel.sendTransaction(request);                promises.push(sendPromise); 

                let txPromise = new Promise((resolve, reject) => {                    let handle = setTimeout(() => {                        event_hub.disconnect();                        resolve({ event_status: 'TIMEOUT' });                    }, 3000);                    event_hub.connect();

                    event_hub.registerTxEvent(transaction_id_string, (tx, code) => {                        clearTimeout(handle);                        event_hub.unregisterTxEvent(transaction_id_string);                        event_hub.disconnect();

                        var return_status = { event_status: code, tx_id: transaction_id_string };                        if (code !== 'VALID') {                            console.error('The transaction was invalid, code = ' + code);                            resolve(return_status);                        } else {                            console.log('The transaction has been committed on peer ' + event_hub._ep._endpoint.addr);                            resolve(return_status);                        }                    }, (err) => {                        console.log(err)                        reject(new Error('There was a problem with the eventhub ::' + err));                    });                });                promises.push(txPromise);

                return Promise.all(promises);            } else {                console.error('Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...');                throw new Error('Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...');            }        }).then((results) => {            console.log('Send transaction promise and event listener promise have completed');            if (results && results[0] && results[0].status === 'SUCCESS') {                console.log('Successfully sent transaction to the orderer.');            } else {                console.error('Failed to order the transaction. Error code: ' + response.status);            }

            if (results && results[1] && results[1].event_status === 'VALID') {                console.log('Successfully committed the change to the ledger by the peer');            } else {                console.log('Transaction failed to be committed to the ledger due to ::' + results[1].event_status);            }        }).then(function () {            return returnData;        })    }

    query(requestData) {        var channel = this.getChannel();        return channel.queryByChaincode(requestData).then((response_payloads) => {            var resultData = JSON.parse(response_payloads.toString('utf8'));            return resultData;        }).then(function(resultData) {            if (resultData.constructor === Array) {                resultData = resultData.map(function (item, index) {                    if (item.data) {                        return item.data                    } else {                        return item;                    }                })            }

            return resultData;        });    }}

var fabricClient = new FBClient();fabricClient.loadFromConfig(configFilePath);

module.exports = fabricClient;

In the above script, I’ve extended the base client and created a function to submit a transaction and query the data to clean it after retrieval. These will be used for future purposes.

STEP 4: CREATING BASIC ENDPOINTS

Now that all our basic things are ready, let’s start with an endpoint to submit a transaction called sell.

const express = require('express')const app = express()var bodyParser = require('body-parser')

//Attach the middlewareapp.use( bodyParser.json() );

app.post('/api/sell', function (req, res) {  // ...})

STEP 5. BUILD A MODEL CLASS

We’ll be creating a model class that will work like a library function to perform a set of application related actions that will be used by each route.

var fabricClient = require('./Config/FabricClient');var FabricCAClient = require('./Config/FabricCAClient');

class ExampleNetwork {

  constructor(userName) {    this.currentUser;    this.issuer;    this.userName = userName;    this.connection = fabricClient;  }

  init() {    var isAdmin = false;    if (this.userName == "admin") {      isAdmin = true;    }    return this.connection.initCredentialStores().then(() => {      return this.connection.getUserContext(this.userName, true)    }).then((user) => {      this.issuer = user;      if (isAdmin) {        return user;      }      return this.ping();    }).then((user) => {      this.currentUser = user;      return user;    })  }

   sell(data) {    var tx_id = this.connection.newTransactionID();    var requestData = {      fcn: 'createProduct',      args: [data.from, data.to, data.product, data.quantity],      txId: tx_id    };    var request = FabricModel.requestBuild(requestData);    return this.connection.submitTransaction(request);  }}

Here in the above code, you will notice that we’re again using the same fabricClientas previously used. Also, we have a function that submits the sell transaction proposal to the system.

Here the model takes the userName in the constructor and sets it as the context for the current instance of the client. In our case, it’s the admin who will be signing this transaction.

STEP 6: BRIDGING THE LIBRARY AND THE SERVER ENDPOINTS

Once we’ve created the library as well the server endpoints, let’s call the library from the server function as below,

const ExampleNetwork = require('./ExampleNetwork');app.post('/api/sell', function(req, res) {        var data = req.body.data;        var exampleNetwork = new ExampleNetwork('admin');

exampleNetwork.init().then(function(data) {          return exampleNetwork.sell(data)        }).then(function (data) {          res.status(200).json(data)        }).catch(function(err) {          res.status(500).json({error: err.toString()})        })})

If you notice, the above code used admin as the username to interact with the network. If you have multiple users you can call the network with the corresponding username provided the certificates are present in the store. In my next article, I’ll explain how to handle user management and session management for a multi-user scenario.


Published by HackerNoon on 2018/03/26