Introduction
My previous work on implementation of API Server was done on Fabric v1.4 one year ago. Certain changes have been seen since v2.0. Here is a rework on the implementation. I will skip most of the explanation as the overall implementation logic is the same as the previous one. Only the difference is highlighted in this article.
Implementation Setup
Here is the setup of our implementation. The overall design remains the same. The only difference in this setup is the use of Test Network instead of First Network. Many sample applications and scripts are rewritten to use Test Network, and so is fabcar. For those who wish to know more about the difference between Test Network and First Network, you can refer to these articles.
Similar to our previous setup, we have two hosts setup for this implementation. They are AWS EC2 instances.
API Server Code in JavaScript
Here I simply reuse the previous apiserver.js
with modification to reflect the difference in using Test Network and some change in SDK v2.2.
One major difference is the way to create wallet objects. In v1.4 it was handled by FileSystemWallet class. In v2.2 it is the class Wallets handling wallet in client applications. You can notice the change in the code (line 20, 65, 109 and 152).
Besides, the client applications in fabcar/javascript/
are modified. In v1.4 it was user1 as a directory, and inside the directory we see the private key, public key and certificates. Now In v2.2 it is appUser.id, which is an object file containing the private key and certificate. In terms of functionality they are the same. Our API server code is updated to reflect this change.
Here is the code.
const express = require('express'); const bodyParser = require('body-parser'); const app = express(); app.use(bodyParser.json()); // Setting for Hyperledger Fabric const { Wallets, Gateway } = require('fabric-network'); const fs = require('fs'); const path = require('path'); const ccpPath = path.resolve(__dirname, '.', 'connection-org1.json'); app.get('/api/queryallcars', async function (req, res) { try { let ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8')); // Create a new file system based wallet for managing identities. const walletPath = path.join(process.cwd(), 'wallet'); const wallet = await Wallets.newFileSystemWallet(walletPath); console.log(`Wallet path: ${walletPath}`); // Check to see if we've already enrolled the user. const identity = await wallet.get('appUser'); if (!identity) { console.log('An identity for the user "appUser" does not exist in the wallet'); console.log('Run the registerUser.js application before retrying'); return; } // Create a new gateway for connecting to our peer node. const gateway = new Gateway(); await gateway.connect(ccp, { wallet, identity: 'appUser', discovery: { enabled: true, asLocalhost: false } }); // Get the network (channel) our contract is deployed to. const network = await gateway.getNetwork('mychannel'); // Get the contract from the network. const contract = network.getContract('fabcar'); // Evaluate the specified transaction. // queryCar transaction - requires 1 argument, ex: ('queryCar', 'CAR4') // queryAllCars transaction - requires no arguments, ex: ('queryAllCars') const result = await contract.evaluateTransaction('queryAllCars'); console.log(`Transaction has been evaluated, result is: ${result.toString()}`); res.status(200).json({response: result.toString()}); // Disconnect from the gateway. await gateway.disconnect(); } catch (error) { console.error(`Failed to evaluate transaction: ${error}`); res.status(500).json({error: error}); process.exit(1); } }); app.get('/api/query/:car_index', async function (req, res) { try { let ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8')); // Create a new file system based wallet for managing identities. const walletPath = path.join(process.cwd(), 'wallet'); const wallet = await Wallets.newFileSystemWallet(walletPath); console.log(`Wallet path: ${walletPath}`); // Check to see if we've already enrolled the user. const identity = await wallet.get('appUser'); if (!identity) { console.log('An identity for the user "appUser" does not exist in the wallet'); console.log('Run the registerUser.js application before retrying'); return; } // Create a new gateway for connecting to our peer node. const gateway = new Gateway(); await gateway.connect(ccp, { wallet, identity: 'appUser', discovery: { enabled: true, asLocalhost: false } }); // Get the network (channel) our contract is deployed to. const network = await gateway.getNetwork('mychannel'); // Get the contract from the network. const contract = network.getContract('fabcar'); // Evaluate the specified transaction. // queryCar transaction - requires 1 argument, ex: ('queryCar', 'CAR4') // queryAllCars transaction - requires no arguments, ex: ('queryAllCars') const result = await contract.evaluateTransaction('queryCar', req.params.car_index); console.log(`Transaction has been evaluated, result is: ${result.toString()}`); res.status(200).json({response: result.toString()}); // Disconnect from the gateway. await gateway.disconnect(); } catch (error) { console.error(`Failed to evaluate transaction: ${error}`); res.status(500).json({error: error}); process.exit(1); } }); app.post('/api/addcar/', async function (req, res) { try { let ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8')); // Create a new file system based wallet for managing identities. const walletPath = path.join(process.cwd(), 'wallet'); const wallet = await Wallets.newFileSystemWallet(walletPath); console.log(`Wallet path: ${walletPath}`); // Check to see if we've already enrolled the user. const identity = await wallet.get('appUser'); if (!identity) { console.log('An identity for the user "appUser" does not exist in the wallet'); console.log('Run the registerUser.js application before retrying'); return; } // Create a new gateway for connecting to our peer node. const gateway = new Gateway(); await gateway.connect(ccp, { wallet, identity: 'appUser', discovery: { enabled: true, asLocalhost: false } }); // Get the network (channel) our contract is deployed to. const network = await gateway.getNetwork('mychannel'); // Get the contract from the network. const contract = network.getContract('fabcar'); // Submit the specified transaction. // createCar transaction - requires 5 argument, ex: ('createCar', 'CAR12', 'Honda', 'Accord', 'Black', 'Tom') // changeCarOwner transaction - requires 2 args , ex: ('changeCarOwner', 'CAR10', 'Dave') await contract.submitTransaction('createCar', req.body.carid, req.body.make, req.body.model, req.body.colour, req.body.owner); console.log('Transaction has been submitted'); res.send('Transaction has been submitted'); // Disconnect from the gateway. await gateway.disconnect(); } catch (error) { console.error(`Failed to submit transaction: ${error}`); process.exit(1); } }) app.put('/api/changeowner/:car_index', async function (req, res) { try { let ccp = JSON.parse(fs.readFileSync(ccpPath, 'utf8')); // Create a new file system based wallet for managing identities. const walletPath = path.join(process.cwd(), 'wallet'); const wallet = await Wallets.newFileSystemWallet(walletPath); console.log(`Wallet path: ${walletPath}`); // Check to see if we've already enrolled the user. const identity = await wallet.get('appUser'); if (!identity) { console.log('An identity for the user "appUser" does not exist in the wallet'); console.log('Run the registerUser.js application before retrying'); return; } // Create a new gateway for connecting to our peer node. const gateway = new Gateway(); await gateway.connect(ccp, { wallet, identity: 'appUser', discovery: { enabled: true, asLocalhost: false } }); // Get the network (channel) our contract is deployed to. const network = await gateway.getNetwork('mychannel'); // Get the contract from the network. const contract = network.getContract('fabcar'); // Submit the specified transaction. // createCar transaction - requires 5 argument, ex: ('createCar', 'CAR12', 'Honda', 'Accord', 'Black', 'Tom') // changeCarOwner transaction - requires 2 args , ex: ('changeCarOwner', 'CAR10', 'Dave') await contract.submitTransaction('changeCarOwner', req.params.car_index, req.body.owner); console.log('Transaction has been submitted'); res.send('Transaction has been submitted'); // Disconnect from the gateway. await gateway.disconnect(); } catch (error) { console.error(`Failed to submit transaction: ${error}`); process.exit(1); } }) app.listen(8080);
Demonstration Step
On Fabric Host
(screenshots are in black colour background)
Step 1: Bring up Test Network and deploy fabcar chaincode
We use the script to perform this step.
cd fabric-samples/fabcar/ ./startFabric.sh
Step 2: Generate user appUser for API server
By default, the fabcar uses SDK to generate a user appUser for client application (API server). we first install the packages. Then we execute the two scripts enrollAdmin.js
and registerUser.js
.
Upon success we will see the wallet/
directory and appUser.id
is generated.
For a quick test, we can use appUser to perform a chaincode function for querying all cars. We run node query.js
, which performs the queryAllCars function. We will see the ten car records preloaded with initLedger function done in the script.
On API Server Host
(screenshots are in blue colour background)
Now we move to API Server. We use a Ubuntu 18.04 LTS server.
Step 3: Install NodeJS environment
sudo apt-get update sudo apt install curl curl -sL https://deb.nodesource.com/setup_10.x | sudo bash - sudo apt install -y nodejs sudo apt-get install build-essentialnode -v npm -v
Step 4: Copy files from Fabric Host
Create the directory in API Server Host.
mkdir apiserver
cd apiserver
The files we need on API Server are
- wallet (identity):
fabcar/javascript/wallet/appUser.id
- node package file:
fabcar/javascript/package.json
- connection profile:
test-network/organizations/peerOrganizations/org1.example.com/connection-org1.json
Again we are using localhost as a bridge for my two AWS EC2 instances. For well structure we move the appUser.id
to wallet/appUser.id
Here is the result.
Step 5: Copy apiserver.js
to this directory.
Step 6: Modify connection profile
The connection profile when generated is using localhost. We change it from localhost to IP of Fabric Host (change to yours).
sed -i 's/localhost/34.203.38.3/g' connection-org1.json
Step 7: Update /etc/hosts
to reach Fabric Host
Finally we let API Server Host reach Fabric Host by resource names we are using. Here is the result.
Now configuration is ready.
Step 8: Install packages on API Server Host
npm install
npm install express body-parser --save
Step 9: Run API Server
node apiserver.js
As we have kept the console.log in our code, we will see console.log result in apiserver.js
when accessing the APIs.
Testing: on my localhost
(screenshots are in red colour background)
Now the API Server is exposing the API we can access the fabcar application. We follow the same test suite in our previous articles.
Query All Cars
curl
Add a new car and query the new car
curl -d '{"carid":"CAR12","make":"Honda","model":"Accord","colour":"black","owner":"Tom"}' -H "Content-Type: application/json" -X POST curl
Change owner for a car and query that car again
curl curl -d '{"owner":"KC"}' -H "Content-Type: application/json" -X PUT curl
The API Server is working well.