Skcript's Logo
Skcript's Logo

Setting up RESTful API Server for Hyperledger Fabric With NodeJS SDK

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

Subscribe to our awesome Newsletter.

Setting up RESTful API Server for Hyperledger Fabric With NodeJS SDK

Currently reading:

Setting up RESTful API Server for Hyperledger Fabric With NodeJS SDK


Subscribe to our Newsletter:


Share article on:

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.

1
2
3
4
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
'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

1
node enrollAdmin.js

The corresponding result should look like this,

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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.

Share article on

Comments and Discussions

Skcript


Stay Updated with Our Newsletter
SIGN UP