Setting up RESTful API Server for Hyperledger Fabric With NodeJS SDK


(S//Bot) #1

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 myapp
cd myapp
touch index.js
npm 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 middleware
app.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 fabricClient as 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 trucerts.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.


This is a companion discussion topic for the original entry at https://www.skcript.com/svr/setting-up-restful-api-server-for-hyperledger-fabric-with-nodejs-sdk/

(Philip) #2

Hi, I’m getting this error when I run ’ node enrollAdmin.js ’

module.js:549
throw err;
^
Error: Cannot find module ‘./Config/FabricClient’
at Function.Module._resolveFilename (module.js:547:15)
at Function.Module._load (module.js:474:25)
at Module.require (module.js:596:17)
at require (internal/module.js:11:18)
at Object. (/home/philip/Desktop/blockchain/enrollAdmin.js:2:20)
at Module._compile (module.js:652:30)
at Object.Module._extensions…js (module.js:663:10)
at Module.load (module.js:565:32)
at tryModuleLoad (module.js:505:12)
at Function.Module._load (module.js:497:3)

This is because the mentioned file (/Config/FabricClient) is not there. Following your steps, there isn’t any mention about creating this file. Kindly help! Thanks!!!


(Praveen Kakumanu) #3

You need to create file FabricClient.js under ./Config/

They mentioned in this link but not above file.

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


(Saeedi) #4

Can you elaborate more on this ? Do i need to create a folder ./Config/ ? Because i cannot find any. What would be the contents of FabricClient.js ? TIA