From c4465100c4236ded0f56ffa1de8fe96aac1774dc Mon Sep 17 00:00:00 2001 From: Kapil Sachdeva Date: Tue, 10 Oct 2017 10:25:38 -0500 Subject: [PATCH] [FAB-6550] Sample app written in typescript This is a sample application that demonstrates usage of Fabric SDK typings. Change-Id: I5b9b42c666de51a490043cafe0faac29e4f4a0a4 Signed-off-by: Kapil Sachdeva --- balance-transfer/typescript/.gitignore | 4 + balance-transfer/typescript/README.md | 303 +++++++++ balance-transfer/typescript/api/chaincode.ts | 89 +++ balance-transfer/typescript/api/channel.ts | 261 ++++++++ balance-transfer/typescript/api/index.ts | 27 + balance-transfer/typescript/api/users.ts | 69 ++ balance-transfer/typescript/api/utils.ts | 23 + balance-transfer/typescript/app.ts | 92 +++ balance-transfer/typescript/app_config.json | 13 + balance-transfer/typescript/artifacts | 1 + balance-transfer/typescript/config.ts | 30 + balance-transfer/typescript/interfaces.ts | 23 + balance-transfer/typescript/lib/chaincode.ts | 148 +++++ balance-transfer/typescript/lib/channel.ts | 599 ++++++++++++++++++ balance-transfer/typescript/lib/helper.ts | 311 +++++++++ .../typescript/lib/network-config.json | 55 ++ balance-transfer/typescript/package.json | 37 ++ balance-transfer/typescript/runApp.sh | 71 +++ balance-transfer/typescript/testAPIs.sh | 197 ++++++ balance-transfer/typescript/tsconfig.json | 27 + balance-transfer/typescript/tslint.json | 38 ++ .../types/fabric-ca-client/index.d.ts | 18 + .../typescript/types/fabric-client/index.d.ts | 312 +++++++++ 23 files changed, 2748 insertions(+) create mode 100644 balance-transfer/typescript/.gitignore create mode 100644 balance-transfer/typescript/README.md create mode 100644 balance-transfer/typescript/api/chaincode.ts create mode 100644 balance-transfer/typescript/api/channel.ts create mode 100644 balance-transfer/typescript/api/index.ts create mode 100644 balance-transfer/typescript/api/users.ts create mode 100644 balance-transfer/typescript/api/utils.ts create mode 100644 balance-transfer/typescript/app.ts create mode 100644 balance-transfer/typescript/app_config.json create mode 120000 balance-transfer/typescript/artifacts create mode 100644 balance-transfer/typescript/config.ts create mode 100644 balance-transfer/typescript/interfaces.ts create mode 100644 balance-transfer/typescript/lib/chaincode.ts create mode 100644 balance-transfer/typescript/lib/channel.ts create mode 100644 balance-transfer/typescript/lib/helper.ts create mode 100644 balance-transfer/typescript/lib/network-config.json create mode 100644 balance-transfer/typescript/package.json create mode 100755 balance-transfer/typescript/runApp.sh create mode 100755 balance-transfer/typescript/testAPIs.sh create mode 100644 balance-transfer/typescript/tsconfig.json create mode 100644 balance-transfer/typescript/tslint.json create mode 100644 balance-transfer/typescript/types/fabric-ca-client/index.d.ts create mode 100644 balance-transfer/typescript/types/fabric-client/index.d.ts diff --git a/balance-transfer/typescript/.gitignore b/balance-transfer/typescript/.gitignore new file mode 100644 index 0000000000..9f22bacf15 --- /dev/null +++ b/balance-transfer/typescript/.gitignore @@ -0,0 +1,4 @@ +node_modules +package-lock.json +dist +types/fabric-client \ No newline at end of file diff --git a/balance-transfer/typescript/README.md b/balance-transfer/typescript/README.md new file mode 100644 index 0000000000..6a1828d76c --- /dev/null +++ b/balance-transfer/typescript/README.md @@ -0,0 +1,303 @@ +## Balance transfer + +This is a sample Node.js application written using typescript which demonstrates +the **__fabric-client__** and **__fabric-ca-client__** Node.js SDK APIs for typescript. + +### Prerequisites and setup: + +* [Docker](https://www.docker.com/products/overview) - v1.12 or higher +* [Docker Compose](https://docs.docker.com/compose/overview/) - v1.8 or higher +* [Git client](https://git-scm.com/downloads) - needed for clone commands +* **Node.js** v6.9.0 - 6.10.0 ( __Node v7+ is not supported__ ) +* [Download Docker images](http://hyperledger-fabric.readthedocs.io/en/latest/samples.html#binaries) + +``` +cd fabric-samples/balance-transfer/ +``` + +Once you have completed the above setup, you will have provisioned a local network with the following docker container configuration: + +* 2 CAs +* A SOLO orderer +* 4 peers (2 peers per Org) + +#### Artifacts + +* Crypto material has been generated using the **cryptogen** tool from Hyperledger Fabric and mounted to all peers, the orderering node and CA containers. More details regarding the cryptogen tool are available [here](http://hyperledger-fabric.readthedocs.io/en/latest/build_network.html#crypto-generator). + +* An Orderer genesis block (genesis.block) and channel configuration transaction (mychannel.tx) has been pre generated using the **configtxgen** tool from Hyperledger Fabric and placed within the artifacts folder. More details regarding the configtxgen tool are available [here](http://hyperledger-fabric.readthedocs.io/en/latest/build_network.html#configuration-transaction-generator). + +## Running the sample program + +There are two options available for running the balance-transfer sample as shown below. + +### Option 1 + +##### Terminal Window 1 + +``` +cd fabric-samples/balance-transfer/typescript + +./runApp.sh + +``` + +This performs the following steps: +* lauches the required network on your local machine +* installs the fabric-client and fabric-ca-client node modules +* starts the node app on PORT 4000 + +##### Terminal Window 2 + +NOTE: In order for the following shell script to properly parse the JSON, you must install ``jq``. + +See instructions at [https://stedolan.github.io/jq/](https://stedolan.github.io/jq/). + +Test the APIs as follows: +``` +cd fabric-samples/balance-transfer/typescript + +./testAPIs.sh + +``` + +### Option 2 is a more manual approach + +##### Terminal Window 1 + +* Launch the network using docker-compose + +``` +docker-compose -f artifacts/docker-compose.yaml up +``` +##### Terminal Window 2 + +* Install the fabric-client and fabric-ca-client node modules + +``` +npm install +``` + +*** NOTE - If running this before the new version of the node SDK is published which includes the typescript definition files, you will need to do the following: + +``` +cp types/fabric-client/index.d.tx node_modules/fabric-client/index.d.ts +cp types/fabric-ca-client/index.d.tx node_modules/fabric-ca-client/index.d.ts +``` + +* Start the node app on PORT 4000 + +``` +PORT=4000 ts-node app.ts +``` + +##### Terminal Window 3 + +* Execute the REST APIs from the section [Sample REST APIs Requests](https://github.com/hyperledger/fabric-samples/tree/master/balance-transfer#sample-rest-apis-requests) + +## Sample REST APIs Requests + +### Login Request + +* Register and enroll new users in Organization - **Org1**: + +`curl -s -X POST http://localhost:4000/users -H "content-type: application/x-www-form-urlencoded" -d 'username=Jim&orgName=org1'` + +**OUTPUT:** + +``` +{ + "success": true, + "secret": "RaxhMgevgJcm", + "message": "Jim enrolled Successfully", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ4NjU1OTEsInVzZXJuYW1lIjoiSmltIiwib3JnTmFtZSI6Im9yZzEiLCJpYXQiOjE0OTQ4NjE5OTF9.yWaJhFDuTvMQRaZIqg20Is5t-JJ_1BP58yrNLOKxtNI" +} +``` + +The response contains the success/failure status, an **enrollment Secret** and a **JSON Web Token (JWT)** that is a required string in the Request Headers for subsequent requests. + +### Create Channel request + +``` +curl -s -X POST \ + http://localhost:4000/channels \ + -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ4NjU1OTEsInVzZXJuYW1lIjoiSmltIiwib3JnTmFtZSI6Im9yZzEiLCJpYXQiOjE0OTQ4NjE5OTF9.yWaJhFDuTvMQRaZIqg20Is5t-JJ_1BP58yrNLOKxtNI" \ + -H "content-type: application/json" \ + -d '{ + "channelName":"mychannel", + "channelConfigPath":"../artifacts/channel/mychannel.tx" +}' +``` + +Please note that the Header **authorization** must contain the JWT returned from the `POST /users` call + +### Join Channel request + +``` +curl -s -X POST \ + http://localhost:4000/channels/mychannel/peers \ + -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ4NjU1OTEsInVzZXJuYW1lIjoiSmltIiwib3JnTmFtZSI6Im9yZzEiLCJpYXQiOjE0OTQ4NjE5OTF9.yWaJhFDuTvMQRaZIqg20Is5t-JJ_1BP58yrNLOKxtNI" \ + -H "content-type: application/json" \ + -d '{ + "peers": ["peer1","peer2"] +}' +``` +### Install chaincode + +``` +curl -s -X POST \ + http://localhost:4000/chaincodes \ + -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ4NjU1OTEsInVzZXJuYW1lIjoiSmltIiwib3JnTmFtZSI6Im9yZzEiLCJpYXQiOjE0OTQ4NjE5OTF9.yWaJhFDuTvMQRaZIqg20Is5t-JJ_1BP58yrNLOKxtNI" \ + -H "content-type: application/json" \ + -d '{ + "peers": ["peer1","peer2"], + "chaincodeName":"mycc", + "chaincodePath":"github.com/example_cc", + "chaincodeVersion":"v0" +}' +``` + +### Instantiate chaincode + +``` +curl -s -X POST \ + http://localhost:4000/channels/mychannel/chaincodes \ + -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ4NjU1OTEsInVzZXJuYW1lIjoiSmltIiwib3JnTmFtZSI6Im9yZzEiLCJpYXQiOjE0OTQ4NjE5OTF9.yWaJhFDuTvMQRaZIqg20Is5t-JJ_1BP58yrNLOKxtNI" \ + -H "content-type: application/json" \ + -d '{ + "chaincodeName":"mycc", + "chaincodeVersion":"v0", + "args":["a","100","b","200"] +}' +``` + +### Invoke request + +``` +curl -s -X POST \ + http://localhost:4000/channels/mychannel/chaincodes/mycc \ + -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ4NjU1OTEsInVzZXJuYW1lIjoiSmltIiwib3JnTmFtZSI6Im9yZzEiLCJpYXQiOjE0OTQ4NjE5OTF9.yWaJhFDuTvMQRaZIqg20Is5t-JJ_1BP58yrNLOKxtNI" \ + -H "content-type: application/json" \ + -d '{ + "fcn":"move", + "args":["a","b","10"] +}' +``` +**NOTE:** Ensure that you save the Transaction ID from the response in order to pass this string in the subsequent query transactions. + +### Chaincode Query + +``` +curl -s -X GET \ + "http://localhost:4000/channels/mychannel/chaincodes/mycc?peer=peer1&fcn=query&args=%5B%22a%22%5D" \ + -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ4NjU1OTEsInVzZXJuYW1lIjoiSmltIiwib3JnTmFtZSI6Im9yZzEiLCJpYXQiOjE0OTQ4NjE5OTF9.yWaJhFDuTvMQRaZIqg20Is5t-JJ_1BP58yrNLOKxtNI" \ + -H "content-type: application/json" +``` + +### Query Block by BlockNumber + +``` +curl -s -X GET \ + "http://localhost:4000/channels/mychannel/blocks/1?peer=peer1" \ + -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ4NjU1OTEsInVzZXJuYW1lIjoiSmltIiwib3JnTmFtZSI6Im9yZzEiLCJpYXQiOjE0OTQ4NjE5OTF9.yWaJhFDuTvMQRaZIqg20Is5t-JJ_1BP58yrNLOKxtNI" \ + -H "content-type: application/json" +``` + +### Query Transaction by TransactionID + +``` +curl -s -X GET http://localhost:4000/channels/mychannel/transactions/TRX_ID?peer=peer1 \ + -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ4NjU1OTEsInVzZXJuYW1lIjoiSmltIiwib3JnTmFtZSI6Im9yZzEiLCJpYXQiOjE0OTQ4NjE5OTF9.yWaJhFDuTvMQRaZIqg20Is5t-JJ_1BP58yrNLOKxtNI" \ + -H "content-type: application/json" +``` +**NOTE**: Here the TRX_ID can be from any previous invoke transaction + + +### Query ChainInfo + +``` +curl -s -X GET \ + "http://localhost:4000/channels/mychannel?peer=peer1" \ + -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ4NjU1OTEsInVzZXJuYW1lIjoiSmltIiwib3JnTmFtZSI6Im9yZzEiLCJpYXQiOjE0OTQ4NjE5OTF9.yWaJhFDuTvMQRaZIqg20Is5t-JJ_1BP58yrNLOKxtNI" \ + -H "content-type: application/json" +``` + +### Query Installed chaincodes + +``` +curl -s -X GET \ + "http://localhost:4000/chaincodes?peer=peer1&type=installed" \ + -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ4NjU1OTEsInVzZXJuYW1lIjoiSmltIiwib3JnTmFtZSI6Im9yZzEiLCJpYXQiOjE0OTQ4NjE5OTF9.yWaJhFDuTvMQRaZIqg20Is5t-JJ_1BP58yrNLOKxtNI" \ + -H "content-type: application/json" +``` + +### Query Instantiated chaincodes + +``` +curl -s -X GET \ + "http://localhost:4000/chaincodes?peer=peer1&type=instantiated" \ + -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ4NjU1OTEsInVzZXJuYW1lIjoiSmltIiwib3JnTmFtZSI6Im9yZzEiLCJpYXQiOjE0OTQ4NjE5OTF9.yWaJhFDuTvMQRaZIqg20Is5t-JJ_1BP58yrNLOKxtNI" \ + -H "content-type: application/json" +``` + +### Query Channels + +``` +curl -s -X GET \ + "http://localhost:4000/channels?peer=peer1" \ + -H "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0OTQ4NjU1OTEsInVzZXJuYW1lIjoiSmltIiwib3JnTmFtZSI6Im9yZzEiLCJpYXQiOjE0OTQ4NjE5OTF9.yWaJhFDuTvMQRaZIqg20Is5t-JJ_1BP58yrNLOKxtNI" \ + -H "content-type: application/json" +``` + +### Network configuration considerations + +You have the ability to change configuration parameters by either directly editing the network-config.json file or provide an additional file for an alternative target network. The app uses an optional environment variable "TARGET_NETWORK" to control the configuration files to use. For example, if you deployed the target network on Amazon Web Services EC2, you can add a file "network-config-aws.json", and set the "TARGET_NETWORK" environment to 'aws'. The app will pick up the settings inside the "network-config-aws.json" file. + +#### IP Address** and PORT information + +If you choose to customize your docker-compose yaml file by hardcoding IP Addresses and PORT information for your peers and orderer, then you MUST also add the identical values into the network-config.json file. The paths shown below will need to be adjusted to match your docker-compose yaml file. + +``` + "orderer": { + "url": "grpcs://x.x.x.x:7050", + "server-hostname": "orderer0", + "tls_cacerts": "../artifacts/tls/orderer/ca-cert.pem" + }, + "org1": { + "ca": "http://x.x.x.x:7054", + "peer1": { + "requests": "grpcs://x.x.x.x:7051", + "events": "grpcs://x.x.x.x:7053", + ... + }, + "peer2": { + "requests": "grpcs://x.x.x.x:7056", + "events": "grpcs://x.x.x.x:7058", + ... + } + }, + "org2": { + "ca": "http://x.x.x.x:8054", + "peer1": { + "requests": "grpcs://x.x.x.x:8051", + "events": "grpcs://x.x.x.x:8053", + ... }, + "peer2": { + "requests": "grpcs://x.x.x.x:8056", + "events": "grpcs://x.x.x.x:8058", + ... + } + } + +``` + +#### Discover IP Address + +To retrieve the IP Address for one of your network entities, issue the following command: + +``` +# The following will return the IP Address for peer0 +docker inspect peer0 | grep IPAddress +``` + +Creative Commons License
This work is licensed under a Creative Commons Attribution 4.0 International License. diff --git a/balance-transfer/typescript/api/chaincode.ts b/balance-transfer/typescript/api/chaincode.ts new file mode 100644 index 0000000000..4c5fda6bd1 --- /dev/null +++ b/balance-transfer/typescript/api/chaincode.ts @@ -0,0 +1,89 @@ +/** + * Copyright 2017 Kapil Sachdeva All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as express from 'express'; +import log4js = require('log4js'); +const logger = log4js.getLogger('SampleWebApp'); +import hfc = require('fabric-client'); +import * as jwt from 'jsonwebtoken'; +import * as helper from '../lib/helper'; +import * as channelApi from '../lib/channel'; +import * as chainCodeApi from '../lib/chaincode'; +import { RequestEx } from '../interfaces'; +import { getErrorMessage } from './utils'; + +export default function chainCodeHandlers(app: express.Application) { + + async function installChainCode(req: RequestEx, res: express.Response) { + logger.debug('==================== INSTALL CHAINCODE =================='); + + const peers = req.body.peers; + const chaincodeName = req.body.chaincodeName; + const chaincodePath = req.body.chaincodePath; + const chaincodeVersion = req.body.chaincodeVersion; + + logger.debug('peers : ' + peers); // target peers list + logger.debug('chaincodeName : ' + chaincodeName); + logger.debug('chaincodePath : ' + chaincodePath); + logger.debug('chaincodeVersion : ' + chaincodeVersion); + + if (!peers || peers.length === 0) { + res.json(getErrorMessage('\'peers\'')); + return; + } + if (!chaincodeName) { + res.json(getErrorMessage('\'chaincodeName\'')); + return; + } + if (!chaincodePath) { + res.json(getErrorMessage('\'chaincodePath\'')); + return; + } + if (!chaincodeVersion) { + res.json(getErrorMessage('\'chaincodeVersion\'')); + return; + } + + const message = await chainCodeApi.installChaincode( + peers, chaincodeName, chaincodePath, chaincodeVersion, req.username, req.orgname); + + res.send(message); + } + + async function queryChainCode(req: RequestEx, res: express.Response) { + const peer = req.query.peer; + const installType = req.query.type; + // TODO: add Constnats + if (installType === 'installed') { + logger.debug( + '================ GET INSTALLED CHAINCODES ======================'); + } else { + logger.debug( + '================ GET INSTANTIATED CHAINCODES ======================'); + } + + const message = await chainCodeApi.getInstalledChaincodes( + peer, installType, req.username, req.orgname); + + res.send(message); + } + + const API_ENDPOINT_CHAINCODE_INSTALL = '/chaincodes'; + const API_ENDPOINT_CHAINCODE_QUERY = '/chaincodes'; + + app.post(API_ENDPOINT_CHAINCODE_INSTALL, installChainCode); + app.get(API_ENDPOINT_CHAINCODE_QUERY, queryChainCode); +} diff --git a/balance-transfer/typescript/api/channel.ts b/balance-transfer/typescript/api/channel.ts new file mode 100644 index 0000000000..49a79c7212 --- /dev/null +++ b/balance-transfer/typescript/api/channel.ts @@ -0,0 +1,261 @@ +/** + * Copyright 2017 Kapil Sachdeva All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as express from 'express'; +import log4js = require('log4js'); +const logger = log4js.getLogger('SampleWebApp'); +import hfc = require('fabric-client'); +import * as jwt from 'jsonwebtoken'; +import * as helper from '../lib/helper'; +import * as channelApi from '../lib/channel'; +import { RequestEx } from '../interfaces'; +import { getErrorMessage } from './utils'; + +export default function channelHandlers(app: express.Application) { + + async function createNewChannel(req: RequestEx, res: express.Response) { + logger.info('<<<<<<<<<<<<<<<<< C R E A T E C H A N N E L >>>>>>>>>>>>>>>>>'); + logger.debug('End point : /channels'); + + const channelName = req.body.channelName; + const channelConfigPath = req.body.channelConfigPath; + + logger.debug('Channel name : ' + channelName); + // ../artifacts/channel/mychannel.tx + logger.debug('channelConfigPath : ' + channelConfigPath); + + if (!channelName) { + res.json(getErrorMessage('\'channelName\'')); + return; + } + if (!channelConfigPath) { + res.json(getErrorMessage('\'channelConfigPath\'')); + return; + } + + const response = await channelApi.createChannel( + channelName, channelConfigPath, req.username, req.orgname); + + res.send(response); + } + + async function joinChannel(req: RequestEx, res: express.Response) { + logger.info('<<<<<<<<<<<<<<<<< J O I N C H A N N E L >>>>>>>>>>>>>>>>>'); + + const channelName = req.params.channelName; + const peers = req.body.peers; + logger.debug('channelName : ' + channelName); + logger.debug('peers : ' + peers); + if (!channelName) { + res.json(getErrorMessage('\'channelName\'')); + return; + } + if (!peers || peers.length === 0) { + res.json(getErrorMessage('\'peers\'')); + return; + } + + const message = await channelApi.joinChannel(channelName, peers, req.username, req.orgname); + res.send(message); + } + + async function instantiateChainCode(req: RequestEx, res: express.Response) { + logger.debug('==================== INSTANTIATE CHAINCODE =================='); + const chaincodeName = req.body.chaincodeName; + const chaincodeVersion = req.body.chaincodeVersion; + const channelName = req.params.channelName; + const fcn = req.body.fcn; + const args = req.body.args; + logger.debug('channelName : ' + channelName); + logger.debug('chaincodeName : ' + chaincodeName); + logger.debug('chaincodeVersion : ' + chaincodeVersion); + logger.debug('fcn : ' + fcn); + logger.debug('args : ' + args); + if (!chaincodeName) { + res.json(getErrorMessage('\'chaincodeName\'')); + return; + } + if (!chaincodeVersion) { + res.json(getErrorMessage('\'chaincodeVersion\'')); + return; + } + if (!channelName) { + res.json(getErrorMessage('\'channelName\'')); + return; + } + if (!args) { + res.json(getErrorMessage('\'args\'')); + return; + } + + const message = await channelApi.instantiateChainCode( + channelName, chaincodeName, chaincodeVersion, fcn, args, req.username, req.orgname); + res.send(message); + } + + async function invokeChainCode(req: RequestEx, res: express.Response) { + logger.debug('==================== INVOKE ON CHAINCODE =================='); + const peers = req.body.peers; + const chaincodeName = req.params.chaincodeName; + const channelName = req.params.channelName; + const fcn = req.body.fcn; + const args = req.body.args; + logger.debug('channelName : ' + channelName); + logger.debug('chaincodeName : ' + chaincodeName); + logger.debug('fcn : ' + fcn); + logger.debug('args : ' + args); + if (!chaincodeName) { + res.json(getErrorMessage('\'chaincodeName\'')); + return; + } + if (!channelName) { + res.json(getErrorMessage('\'channelName\'')); + return; + } + if (!fcn) { + res.json(getErrorMessage('\'fcn\'')); + return; + } + if (!args) { + res.json(getErrorMessage('\'args\'')); + return; + } + + const message = await channelApi.invokeChaincode( + peers, channelName, chaincodeName, fcn, args, req.username, req.orgname); + + res.send(message); + } + + async function queryChainCode(req: RequestEx, res: express.Response) { + const channelName = req.params.channelName; + const chaincodeName = req.params.chaincodeName; + let args = req.query.args; + const fcn = req.query.fcn; + const peer = req.query.peer; + + logger.debug('channelName : ' + channelName); + logger.debug('chaincodeName : ' + chaincodeName); + logger.debug('fcn : ' + fcn); + logger.debug('args : ' + args); + + if (!chaincodeName) { + res.json(getErrorMessage('\'chaincodeName\'')); + return; + } + if (!channelName) { + res.json(getErrorMessage('\'channelName\'')); + return; + } + if (!fcn) { + res.json(getErrorMessage('\'fcn\'')); + return; + } + if (!args) { + res.json(getErrorMessage('\'args\'')); + return; + } + + args = args.replace(/'/g, '"'); + args = JSON.parse(args); + logger.debug(args); + + const message = await channelApi.queryChaincode( + peer, channelName, chaincodeName, args, fcn, req.username, req.orgname); + + res.send(message); + } + + async function queryByBlockNumber(req: RequestEx, res: express.Response) { + logger.debug('==================== GET BLOCK BY NUMBER =================='); + const blockId = req.params.blockId; + const peer = req.query.peer; + logger.debug('channelName : ' + req.params.channelName); + logger.debug('BlockID : ' + blockId); + logger.debug('Peer : ' + peer); + if (!blockId) { + res.json(getErrorMessage('\'blockId\'')); + return; + } + + const message = await channelApi.getBlockByNumber(peer, blockId, req.username, req.orgname); + res.send(message); + } + + async function queryByTransactionId(req: RequestEx, res: express.Response) { + logger.debug( + '================ GET TRANSACTION BY TRANSACTION_ID ======================' + ); + logger.debug('channelName : ' + req.params.channelName); + const trxnId = req.params.trxnId; + const peer = req.query.peer; + if (!trxnId) { + res.json(getErrorMessage('\'trxnId\'')); + return; + } + + const message = await channelApi.getTransactionByID( + peer, trxnId, req.username, req.orgname); + + res.send(message); + } + + async function queryChannelInfo(req: RequestEx, res: express.Response) { + logger.debug( + '================ GET CHANNEL INFORMATION ======================'); + logger.debug('channelName : ' + req.params.channelName); + const peer = req.query.peer; + + const message = await channelApi.getChainInfo(peer, req.username, req.orgname); + + res.send(message); + } + + async function queryChannels(req: RequestEx, res: express.Response) { + logger.debug('================ GET CHANNELS ======================'); + logger.debug('peer: ' + req.query.peer); + const peer = req.query.peer; + if (!peer) { + res.json(getErrorMessage('\'peer\'')); + return; + } + + const message = await channelApi.getChannels(peer, req.username, req.orgname); + res.send(message); + } + + const API_ENDPOINT_CHANNEL_CREATE = '/channels'; + const API_ENDPOINT_CHANNEL_JOIN = '/channels/:channelName/peers'; + const API_ENDPOINT_CHANNEL_INSTANTIATE_CHAINCODE = '/channels/:channelName/chaincodes'; + const API_ENDPOINT_CHANNEL_INVOKE_CHAINCODE = + '/channels/:channelName/chaincodes/:chaincodeName'; + const API_ENDPOINT_CHANNEL_QUERY_CHAINCODE = '/channels/:channelName/chaincodes/:chaincodeName'; + const API_ENDPOINT_CHANNEL_QUERY_BY_BLOCKNUMBER = '/channels/:channelName/blocks/:blockId'; + const API_ENDPOINT_CHANNEL_QUERY_BY_TRANSACTIONID + = '/channels/:channelName/transactions/:trxnId'; + const API_ENDPOINT_CHANNEL_INFO = '/channels/:channelName'; + const API_ENDPOINT_CHANNEL_QUERY = '/channels'; + + app.post(API_ENDPOINT_CHANNEL_CREATE, createNewChannel); + app.post(API_ENDPOINT_CHANNEL_JOIN, joinChannel); + app.post(API_ENDPOINT_CHANNEL_INSTANTIATE_CHAINCODE, instantiateChainCode); + app.post(API_ENDPOINT_CHANNEL_INVOKE_CHAINCODE, invokeChainCode); + app.get(API_ENDPOINT_CHANNEL_QUERY_CHAINCODE, queryChainCode); + app.get(API_ENDPOINT_CHANNEL_QUERY_BY_BLOCKNUMBER, queryByBlockNumber); + app.get(API_ENDPOINT_CHANNEL_QUERY_BY_TRANSACTIONID, queryByTransactionId); + app.get(API_ENDPOINT_CHANNEL_INFO, queryChannelInfo); + app.get(API_ENDPOINT_CHANNEL_QUERY, queryChannels); +} diff --git a/balance-transfer/typescript/api/index.ts b/balance-transfer/typescript/api/index.ts new file mode 100644 index 0000000000..f49700e086 --- /dev/null +++ b/balance-transfer/typescript/api/index.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2017 Kapil Sachdeva All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as express from 'express'; +import userHandlers from './users'; +import channelHandlers from './channel'; +import chainCodeHandlers from './chaincode'; + +export default function entryPoint(app: express.Application) { + // various handlers + userHandlers(app); + channelHandlers(app); + chainCodeHandlers(app); +} diff --git a/balance-transfer/typescript/api/users.ts b/balance-transfer/typescript/api/users.ts new file mode 100644 index 0000000000..d9462555dc --- /dev/null +++ b/balance-transfer/typescript/api/users.ts @@ -0,0 +1,69 @@ +/** + * Copyright 2017 Kapil Sachdeva All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RequestEx } from '../interfaces'; +import * as express from 'express'; +import log4js = require('log4js'); +const logger = log4js.getLogger('SampleWebApp'); +import hfc = require('fabric-client'); +import * as jwt from 'jsonwebtoken'; +import * as helper from '../lib/helper'; +import { getErrorMessage } from './utils'; + +export default function userHandlers(app: express.Application) { + + async function registerUser(req: RequestEx, res: express.Response) { + const username = req.body.username; + const orgName = req.body.orgName; + + logger.debug('End point : /users'); + logger.debug('User name : ' + username); + logger.debug('Org name : ' + orgName); + + if (!username) { + res.json(getErrorMessage('\'username\'')); + return; + } + if (!orgName) { + res.json(getErrorMessage('\'orgName\'')); + return; + } + const token = jwt.sign({ + exp: Math.floor(Date.now() / 1000) + parseInt( + hfc.getConfigSetting('jwt_expiretime'), 10), + username, + orgName + }, app.get('secret')); + + const response = await helper.getRegisteredUsers(username, orgName); + + if (response && typeof response !== 'string') { + res.json({ + success: true, + token + }); + } else { + res.json({ + success: false, + message: response + }); + } + } + + const API_ENDPOINT_REGISTER_USER = '/users'; + + app.post(API_ENDPOINT_REGISTER_USER, registerUser); +} diff --git a/balance-transfer/typescript/api/utils.ts b/balance-transfer/typescript/api/utils.ts new file mode 100644 index 0000000000..128545fe91 --- /dev/null +++ b/balance-transfer/typescript/api/utils.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2017 Kapil Sachdeva All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function getErrorMessage(field: string) { + const response = { + success: false, + message: field + ' field is missing or Invalid in the request' + }; + return response; +} diff --git a/balance-transfer/typescript/app.ts b/balance-transfer/typescript/app.ts new file mode 100644 index 0000000000..66e8680bf1 --- /dev/null +++ b/balance-transfer/typescript/app.ts @@ -0,0 +1,92 @@ +/** + * Copyright 2017 Kapil Sachdeva All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import log4js = require('log4js'); +import * as util from 'util'; +import * as http from 'http'; +import * as express from 'express'; +import * as jwt from 'jsonwebtoken'; +import * as bodyParser from 'body-parser'; +import expressJWT = require('express-jwt'); +// tslint:disable-next-line:no-var-requires +const bearerToken = require('express-bearer-token'); +import cors = require('cors'); +import hfc = require('fabric-client'); +import * as helper from './lib/helper'; +import { RequestEx } from './interfaces'; +import api from './api'; + +helper.init(); + +const SERVER_HOST = process.env.HOST || hfc.getConfigSetting('host'); +const SERVER_PORT = process.env.PORT || hfc.getConfigSetting('port'); + +const logger = log4js.getLogger('SampleWebApp'); + +// create express App +const app = express(); + +app.options('*', cors()); +app.use(cors()); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ + extended: false +})); +app.set('secret', 'thisismysecret'); +app.use(expressJWT({ + secret: 'thisismysecret' +}).unless({ + path: ['/users'] +})); +app.use(bearerToken()); + +app.use((req: RequestEx, res, next) => { + if (req.originalUrl.indexOf('/users') >= 0) { + return next(); + } + + const token = req.token; + jwt.verify(token, app.get('secret'), (err: Error, decoded: any) => { + if (err) { + res.send({ + success: false, + message: 'Failed to authenticate token. Make sure to include the ' + + 'token returned from /users call in the authorization header ' + + ' as a Bearer token' + }); + return; + } else { + // add the decoded user name and org name to the request object + // for the downstream code to use + req.username = decoded.username; + req.orgname = decoded.orgName; + logger.debug( + util.format('Decoded from JWT token: username - %s, orgname - %s', + decoded.username, decoded.orgName)); + return next(); + } + }); +}); + +// configure various routes +api(app); + +const server = http.createServer(app); +server.listen(SERVER_PORT); + +logger.info('****************** SERVER STARTED ************************'); +logger.info('************** http://' + SERVER_HOST + ':' + SERVER_PORT + ' ******************'); +server.timeout = 240000; diff --git a/balance-transfer/typescript/app_config.json b/balance-transfer/typescript/app_config.json new file mode 100644 index 0000000000..6406d66f22 --- /dev/null +++ b/balance-transfer/typescript/app_config.json @@ -0,0 +1,13 @@ +{ + "host": "localhost", + "port": "4000", + "jwt_expiretime": "36000", + "channelName": "mychannel", + "CC_SRC_PATH": "../artifacts", + "keyValueStore": "/tmp/fabric-client-kvs", + "eventWaitTime": "30000", + "admins": [{ + "username": "admin", + "secret": "adminpw" + }] +} diff --git a/balance-transfer/typescript/artifacts b/balance-transfer/typescript/artifacts new file mode 120000 index 0000000000..70f9aabc31 --- /dev/null +++ b/balance-transfer/typescript/artifacts @@ -0,0 +1 @@ +../artifacts \ No newline at end of file diff --git a/balance-transfer/typescript/config.ts b/balance-transfer/typescript/config.ts new file mode 100644 index 0000000000..1277eee48e --- /dev/null +++ b/balance-transfer/typescript/config.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2017 Kapil Sachdeva All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as util from 'util'; + +let file = 'network-config%s.json'; + +const env = process.env.TARGET_NETWORK; +if (env) { + file = util.format(file, '-' + env); +} else { + file = util.format(file, ''); +} + +export default { + networkConfigFile: file +}; diff --git a/balance-transfer/typescript/interfaces.ts b/balance-transfer/typescript/interfaces.ts new file mode 100644 index 0000000000..6acd2b1b6b --- /dev/null +++ b/balance-transfer/typescript/interfaces.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2017 Kapil Sachdeva All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as express from 'express'; + +export interface RequestEx extends express.Request { + username?: any; + orgname?: any; + token?: any; +} diff --git a/balance-transfer/typescript/lib/chaincode.ts b/balance-transfer/typescript/lib/chaincode.ts new file mode 100644 index 0000000000..70915abd07 --- /dev/null +++ b/balance-transfer/typescript/lib/chaincode.ts @@ -0,0 +1,148 @@ +/** + * Copyright 2017 Kapil Sachdeva All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as util from 'util'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as helper from './helper'; + +// tslint:disable-next-line:no-var-requires +const config = require('../app_config.json'); +const logger = helper.getLogger('ChaincodeApi'); + +function buildTarget(peer: string, org: string): Peer { + let target: Peer = null; + if (typeof peer !== 'undefined') { + const targets: Peer[] = helper.newPeers([peer], org); + if (targets && targets.length > 0) { + target = targets[0]; + } + } + + return target; +} + +export async function installChaincode( + peers: string[], chaincodeName: string, chaincodePath: string, + chaincodeVersion: string, username: string, org: string) { + + logger.debug( + '\n============ Install chaincode on organizations ============\n'); + + helper.setupChaincodeDeploy(); + + const channel = helper.getChannelForOrg(org); + const client = helper.getClientForOrg(org); + + const admin = await helper.getOrgAdmin(org); + + const request = { + targets: helper.newPeers(peers, org), + chaincodePath, + chaincodeId: chaincodeName, + chaincodeVersion + }; + + try { + + const results = await client.installChaincode(request); + + const proposalResponses = results[0]; + const proposal = results[1]; + let allGood = true; + + proposalResponses.forEach((pr) => { + let oneGood = false; + if (pr.response && pr.response.status === 200) { + oneGood = true; + logger.info('install proposal was good'); + } else { + logger.error('install proposal was bad'); + } + allGood = allGood && oneGood; + }); + + if (allGood) { + logger.info(util.format( + 'Successfully sent install Proposal and received ProposalResponse: Status - %s', + proposalResponses[0].response.status)); + logger.debug('\nSuccessfully Installed chaincode on organization ' + org + + '\n'); + return 'Successfully Installed chaincode on organization ' + org; + } else { + logger.error( + // tslint:disable-next-line:max-line-length + 'Failed to send install Proposal or receive valid response. Response null or status is not 200. exiting...' + ); + // tslint:disable-next-line:max-line-length + return 'Failed to send install Proposal or receive valid response. Response null or status is not 200. exiting...'; + } + + } catch (err) { + logger.error('Failed to send install proposal due to error: ' + err.stack ? + err.stack : err); + throw new Error('Failed to send install proposal due to error: ' + err.stack ? + err.stack : err); + } +} + +export async function getInstalledChaincodes( + peer: string, type: string, username: string, org: string) { + + const target = buildTarget(peer, org); + const channel = helper.getChannelForOrg(org); + const client = helper.getClientForOrg(org); + + const user = await helper.getOrgAdmin(org); + + try { + + let response: ChaincodeQueryResponse = null; + + if (type === 'installed') { + response = await client.queryInstalledChaincodes(target); + } else { + response = await channel.queryInstantiatedChaincodes(target); + } + + if (response) { + if (type === 'installed') { + logger.debug('<<< Installed Chaincodes >>>'); + } else { + logger.debug('<<< Instantiated Chaincodes >>>'); + } + + const details: string[] = []; + response.chaincodes.forEach((c) => { + logger.debug('name: ' + c.name + ', version: ' + + c.version + ', path: ' + c.path + ); + details.push('name: ' + c.name + ', version: ' + + c.version + ', path: ' + c.path + ); + }); + + return details; + } else { + logger.error('response is null'); + return 'response is null'; + } + + } catch (err) { + logger.error('Failed to query with error:' + err.stack ? err.stack : err); + return 'Failed to query with error:' + err.stack ? err.stack : err; + } +} \ No newline at end of file diff --git a/balance-transfer/typescript/lib/channel.ts b/balance-transfer/typescript/lib/channel.ts new file mode 100644 index 0000000000..2cc474aa5f --- /dev/null +++ b/balance-transfer/typescript/lib/channel.ts @@ -0,0 +1,599 @@ +/** + * Copyright 2017 Kapil Sachdeva All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as util from 'util'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as helper from './helper'; +const logger = helper.getLogger('ChannelApi'); +// tslint:disable-next-line:no-var-requires +const config = require('../app_config.json'); + +const allEventhubs: EventHub[] = []; + +function buildTarget(peer: string, org: string): Peer { + let target: Peer = null; + if (typeof peer !== 'undefined') { + const targets: Peer[] = helper.newPeers([peer], org); + if (targets && targets.length > 0) { + target = targets[0]; + } + } + + return target; +} + +// Attempt to send a request to the orderer with the sendCreateChain method +export async function createChannel( + channelName: string, channelConfigPath: string, username: string, orgName: string) { + + logger.debug('\n====== Creating Channel \'' + channelName + '\' ======\n'); + + const client = helper.getClientForOrg(orgName); + const channel = helper.getChannelForOrg(orgName); + + // read in the envelope for the channel config raw bytes + const envelope = fs.readFileSync(path.join(__dirname, channelConfigPath)); + // extract the channel config bytes from the envelope to be signed + const channelConfig = client.extractChannelConfig(envelope); + + // Acting as a client in the given organization provided with "orgName" param + const admin = await helper.getOrgAdmin(orgName); + + logger.debug(util.format('Successfully acquired admin user for the organization "%s"', + orgName)); + + // sign the channel config bytes as "endorsement", this is required by + // the orderer's channel creation policy + const signature = client.signChannelConfig(channelConfig); + + const request = { + config: channelConfig, + signatures: [signature], + name: channelName, + orderer: channel.getOrderers()[0], + txId: client.newTransactionID() + }; + + try { + const response = await client.createChannel(request); + + if (response && response.status === 'SUCCESS') { + logger.debug('Successfully created the channel.'); + return { + success: true, + message: 'Channel \'' + channelName + '\' created Successfully' + }; + } else { + logger.error('\n!!!!!!!!! Failed to create the channel \'' + channelName + + '\' !!!!!!!!!\n\n'); + throw new Error('Failed to create the channel \'' + channelName + '\''); + } + + } catch (err) { + logger.error('\n!!!!!!!!! Failed to create the channel \'' + channelName + + '\' !!!!!!!!!\n\n'); + throw new Error('Failed to create the channel \'' + channelName + '\''); + } +} + +export async function joinChannel( + channelName: string, peers: string[], username: string, org: string) { + + // on process exit, always disconnect the event hub + const closeConnections = (isSuccess: boolean) => { + if (isSuccess) { + logger.debug('\n============ Join Channel is SUCCESS ============\n'); + } else { + logger.debug('\n!!!!!!!! ERROR: Join Channel FAILED !!!!!!!!\n'); + } + logger.debug(''); + + allEventhubs.forEach((hub) => { + console.log(hub); + if (hub && hub.isconnected()) { + hub.disconnect(); + } + }); + }; + + // logger.debug('\n============ Join Channel ============\n') + logger.info(util.format( + 'Calling peers in organization "%s" to join the channel', org)); + + const client = helper.getClientForOrg(org); + const channel = helper.getChannelForOrg(org); + + const admin = await helper.getOrgAdmin(org); + + logger.info(util.format('received member object for admin of the organization "%s": ', org)); + const request = { + txId: client.newTransactionID() + }; + + const genesisBlock = await channel.getGenesisBlock(request); + + const request2 = { + targets: helper.newPeers(peers, org), + txId: client.newTransactionID(), + block: genesisBlock + }; + + const eventhubs = helper.newEventHubs(peers, org); + eventhubs.forEach((eh) => { + eh.connect(); + allEventhubs.push(eh); + }); + + const eventPromises: Array> = []; + eventhubs.forEach((eh) => { + const txPromise = new Promise((resolve, reject) => { + const handle = setTimeout(reject, parseInt(config.eventWaitTime, 10)); + eh.registerBlockEvent((block: any) => { + clearTimeout(handle); + // in real-world situations, a peer may have more than one channels so + // we must check that this block came from the channel we asked the peer to join + if (block.data.data.length === 1) { + // Config block must only contain one transaction + const channel_header = block.data.data[0].payload.header.channel_header; + if (channel_header.channel_id === channelName) { + resolve(); + } else { + reject(); + } + } + }); + }); + eventPromises.push(txPromise); + }); + + const sendPromise = channel.joinChannel(request2); + const results = await Promise.all([sendPromise].concat(eventPromises)); + + logger.debug(util.format('Join Channel R E S P O N S E : %j', results)); + if (results[0] && results[0][0] && results[0][0].response && results[0][0] + .response.status === 200) { + logger.info(util.format( + 'Successfully joined peers in organization %s to the channel \'%s\'', + org, channelName)); + closeConnections(true); + const response = { + success: true, + message: util.format( + 'Successfully joined peers in organization %s to the channel \'%s\'', + org, channelName) + }; + return response; + } else { + logger.error(' Failed to join channel'); + closeConnections(false); + throw new Error('Failed to join channel'); + } +} + +export async function instantiateChainCode( + channelName: string, chaincodeName: string, chaincodeVersion: string, + functionName: string, args: string[], username: string, org: string) { + + logger.debug('\n============ Instantiate chaincode on organization ' + org + + ' ============\n'); + + const channel = helper.getChannelForOrg(org); + const client = helper.getClientForOrg(org); + + const admin = await helper.getOrgAdmin(org); + await channel.initialize(); + + const txId = client.newTransactionID(); + // send proposal to endorser + const request = { + chaincodeId: chaincodeName, + chaincodeVersion, + args, + txId, + fcn: functionName + }; + + try { + + const results = await channel.sendInstantiateProposal(request); + + const proposalResponses = results[0]; + const proposal = results[1]; + + let allGood = true; + + proposalResponses.forEach((pr) => { + let oneGood = false; + if (pr.response && pr.response.status === 200) { + oneGood = true; + logger.info('install proposal was good'); + } else { + logger.error('install proposal was bad'); + } + allGood = allGood && oneGood; + }); + + if (allGood) { + logger.info(util.format( + // tslint:disable-next-line:max-line-length + 'Successfully sent Proposal and received ProposalResponse: Status - %s, message - "%s", metadata - "%s", endorsement signature: %s', + proposalResponses[0].response.status, proposalResponses[0].response.message, + proposalResponses[0].response.payload, proposalResponses[0].endorsement + .signature)); + + const request2 = { + proposalResponses, + proposal + }; + // set the transaction listener and set a timeout of 30sec + // if the transaction did not get committed within the timeout period, + // fail the test + const deployId = txId.getTransactionID(); + const ORGS = helper.getOrgs(); + + const eh = client.newEventHub(); + const data = fs.readFileSync(path.join(__dirname, ORGS[org].peers['peer1'][ + 'tls_cacerts' + ])); + + eh.setPeerAddr(ORGS[org].peers['peer1']['events'], { + 'pem': Buffer.from(data).toString(), + 'ssl-target-name-override': ORGS[org].peers['peer1']['server-hostname'] + }); + eh.connect(); + + const txPromise: Promise = new Promise((resolve, reject) => { + const handle = setTimeout(() => { + eh.disconnect(); + reject(); + }, 30000); + + eh.registerTxEvent(deployId, (tx, code) => { + // logger.info( + // 'The chaincode instantiate transaction has been committed on peer ' + + // eh._ep._endpoint.addr); + + clearTimeout(handle); + eh.unregisterTxEvent(deployId); + eh.disconnect(); + + if (code !== 'VALID') { + logger.error( + 'The chaincode instantiate transaction was invalid, code = ' + code); + reject(); + } else { + logger.info('The chaincode instantiate transaction was valid.'); + resolve(); + } + }); + }); + + const sendPromise = channel.sendTransaction(request2); + const transactionResults = await Promise.all([sendPromise].concat([txPromise])); + + const response = transactionResults[0]; + if (response.status === 'SUCCESS') { + logger.info('Successfully sent transaction to the orderer.'); + return 'Chaincode Instantiation is SUCCESS'; + } else { + logger.error('Failed to order the transaction. Error code: ' + response.status); + return 'Failed to order the transaction. Error code: ' + response.status; + } + + } else { + logger.error( + // tslint:disable-next-line:max-line-length + 'Failed to send instantiate Proposal or receive valid response. Response null or status is not 200. exiting...' + ); + // tslint:disable-next-line:max-line-length + return 'Failed to send instantiate Proposal or receive valid response. Response null or status is not 200. exiting...'; + } + + } catch (err) { + logger.error('Failed to send instantiate due to error: ' + err.stack ? err + .stack : err); + return 'Failed to send instantiate due to error: ' + err.stack ? err.stack : + err; + } +} + +export async function invokeChaincode( + peerNames: string[], channelName: string, + chaincodeName: string, fcn: string, args: string[], username: string, org: string) { + + logger.debug( + util.format('\n============ invoke transaction on organization %s ============\n', org)); + + const client = helper.getClientForOrg(org); + const channel = helper.getChannelForOrg(org); + const targets = (peerNames) ? helper.newPeers(peerNames, org) : undefined; + + const user = await helper.getRegisteredUsers(username, org); + + const txId = client.newTransactionID(); + logger.debug(util.format('Sending transaction "%j"', txId)); + // send proposal to endorser + const request: ChaincodeInvokeRequest = { + chaincodeId: chaincodeName, + fcn, + args, + txId + }; + + if (targets) { + request.targets = targets; + } + + try { + + const results = await channel.sendTransactionProposal(request); + + const proposalResponses = results[0]; + const proposal = results[1]; + let allGood = true; + + proposalResponses.forEach((pr) => { + let oneGood = false; + if (pr.response && pr.response.status === 200) { + oneGood = true; + logger.info('transaction proposal was good'); + } else { + logger.error('transaction proposal was bad'); + } + allGood = allGood && oneGood; + }); + + if (allGood) { + logger.debug(util.format( + // tslint:disable-next-line:max-line-length + 'Successfully sent Proposal and received ProposalResponse: Status - %s, message - "%s", metadata - "%s", endorsement signature: %s', + proposalResponses[0].response.status, proposalResponses[0].response.message, + proposalResponses[0].response.payload, proposalResponses[0].endorsement + .signature)); + + const request2 = { + proposalResponses, + proposal + }; + + // set the transaction listener and set a timeout of 30sec + // if the transaction did not get committed within the timeout period, + // fail the test + const transactionID = txId.getTransactionID(); + const eventPromises: Array> = []; + + if (!peerNames) { + peerNames = channel.getPeers().map((peer) => { + return peer.getName(); + }); + } + + const eventhubs = helper.newEventHubs(peerNames, org); + + eventhubs.forEach((eh: EventHub) => { + eh.connect(); + + const txPromise = new Promise((resolve, reject) => { + const handle = setTimeout(() => { + eh.disconnect(); + reject(); + }, 30000); + + eh.registerTxEvent(transactionID, (tx: string, code: string) => { + clearTimeout(handle); + eh.unregisterTxEvent(transactionID); + eh.disconnect(); + + if (code !== 'VALID') { + logger.error( + 'The balance transfer transaction was invalid, code = ' + code); + reject(); + } else { + // logger.info( + // 'The balance transfer transaction has been committed on peer ' + + // eh._ep._endpoint.addr); + resolve(); + } + }); + }); + eventPromises.push(txPromise); + }); + + const sendPromise = channel.sendTransaction(request2); + const results2 = await Promise.all([sendPromise].concat(eventPromises)); + + logger.debug(' event promise all complete and testing complete'); + + if (results2[0].status === 'SUCCESS') { + logger.info('Successfully sent transaction to the orderer.'); + return txId.getTransactionID(); + } else { + logger.error('Failed to order the transaction. Error code: ' + results2[0].status); + return 'Failed to order the transaction. Error code: ' + results2[0].status; + } + } else { + logger.error( + // tslint:disable-next-line:max-line-length + 'Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...' + ); + // tslint:disable-next-line:max-line-length + return 'Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...'; + } + + } catch (err) { + logger.error('Failed to send transaction due to error: ' + err.stack ? err + .stack : err); + return 'Failed to send transaction due to error: ' + err.stack ? err.stack : + err; + } +} + +export async function queryChaincode( + peer: string, channelName: string, chaincodeName: string, + args: string[], fcn: string, username: string, org: string) { + + const channel = helper.getChannelForOrg(org); + const client = helper.getClientForOrg(org); + const target = buildTarget(peer, org); + + const user = await helper.getRegisteredUsers(username, org); + + const txId = client.newTransactionID(); + // send query + const request: ChaincodeQueryRequest = { + chaincodeId: chaincodeName, + txId, + fcn, + args + }; + + if (target) { + request.targets = [target]; + } + + try { + const responsePayloads = await channel.queryByChaincode(request); + + if (responsePayloads) { + + responsePayloads.forEach((rp) => { + logger.info(args[0] + ' now has ' + rp.toString('utf8') + + ' after the move'); + return args[0] + ' now has ' + rp.toString('utf8') + + ' after the move'; + }); + + } else { + logger.error('response_payloads is null'); + return 'response_payloads is null'; + } + } catch (err) { + logger.error('Failed to send query due to error: ' + err.stack ? err.stack : + err); + return 'Failed to send query due to error: ' + err.stack ? err.stack : err; + } +} + +export async function getBlockByNumber( + peer: string, blockNumber: string, username: string, org: string) { + + const target = buildTarget(peer, org); + const channel = helper.getChannelForOrg(org); + + const user = await helper.getRegisteredUsers(username, org); + + try { + + const responsePayloads = await channel.queryBlock(parseInt(blockNumber, 10), target); + + if (responsePayloads) { + logger.debug(responsePayloads); + return responsePayloads; // response_payloads.data.data[0].buffer; + } else { + logger.error('response_payloads is null'); + return 'response_payloads is null'; + } + + } catch (err) { + logger.error('Failed to query with error:' + err.stack ? err.stack : err); + return 'Failed to query with error:' + err.stack ? err.stack : err; + } +} + +export async function getTransactionByID( + peer: string, trxnID: string, username: string, org: string) { + + const target = buildTarget(peer, org); + const channel = helper.getChannelForOrg(org); + + const user = await helper.getRegisteredUsers(username, org); + + try { + + const responsePayloads = await channel.queryTransaction(trxnID, target); + + if (responsePayloads) { + logger.debug(responsePayloads); + return responsePayloads; + } else { + logger.error('response_payloads is null'); + return 'response_payloads is null'; + } + + } catch (err) { + logger.error('Failed to query with error:' + err.stack ? err.stack : err); + return 'Failed to query with error:' + err.stack ? err.stack : err; + } +} + +export async function getChainInfo(peer: string, username: string, org: string) { + + const target = buildTarget(peer, org); + const channel = helper.getChannelForOrg(org); + + const user = await helper.getRegisteredUsers(username, org); + + try { + + const blockChainInfo = await channel.queryInfo(target); + + if (blockChainInfo) { + // FIXME: Save this for testing 'getBlockByHash' ? + logger.debug('==========================================='); + logger.debug(blockChainInfo.currentBlockHash); + logger.debug('==========================================='); + // logger.debug(blockchainInfo); + return blockChainInfo; + } else { + logger.error('blockChainInfo is null'); + return 'blockChainInfo is null'; + } + + } catch (err) { + logger.error('Failed to query with error:' + err.stack ? err.stack : err); + return 'Failed to query with error:' + err.stack ? err.stack : err; + } +} + +export async function getChannels(peer: string, username: string, org: string) { + const target = buildTarget(peer, org); + const channel = helper.getChannelForOrg(org); + const client = helper.getClientForOrg(org); + + const user = await helper.getRegisteredUsers(username, org); + + try { + + const response = await client.queryChannels(target); + + if (response) { + logger.debug('<<< channels >>>'); + const channelNames: string[] = []; + response.channels.forEach((ci) => { + channelNames.push('channel id: ' + ci.channel_id); + }); + return response; + } else { + logger.error('response_payloads is null'); + return 'response_payloads is null'; + } + + } catch (err) { + logger.error('Failed to query with error:' + err.stack ? err.stack : err); + return 'Failed to query with error:' + err.stack ? err.stack : err; + } +} diff --git a/balance-transfer/typescript/lib/helper.ts b/balance-transfer/typescript/lib/helper.ts new file mode 100644 index 0000000000..b2497aab2e --- /dev/null +++ b/balance-transfer/typescript/lib/helper.ts @@ -0,0 +1,311 @@ +/** + * Copyright 2017 Kapil Sachdeva All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import log4js = require('log4js'); +import * as path from 'path'; +import * as fs from 'fs'; +import * as util from 'util'; +import config from '../config'; +import hfc = require('fabric-client'); +// tslint:disable-next-line:no-var-requires +const copService = require('fabric-ca-client'); + +const logger = log4js.getLogger('Helper'); +logger.setLevel('DEBUG'); +hfc.setLogger(logger); + +let ORGS: any; +const clients = {}; +const channels = {}; +const caClients = {}; + +function readAllFiles(dir: string) { + const files = fs.readdirSync(dir); + const certs: any = []; + files.forEach((fileName) => { + const filePath = path.join(dir, fileName); + const data = fs.readFileSync(filePath); + certs.push(data); + }); + return certs; +} + +function getKeyStoreForOrg(org: string) { + return hfc.getConfigSetting('keyValueStore') + '_' + org; +} + +function setupPeers(channel: any, org: string, client: Client) { + for (const key in ORGS[org].peers) { + if (key) { + const data = fs.readFileSync( + path.join(__dirname, ORGS[org].peers[key]['tls_cacerts'])); + const peer = client.newPeer( + ORGS[org].peers[key].requests, + { + 'pem': Buffer.from(data).toString(), + 'ssl-target-name-override': ORGS[org].peers[key]['server-hostname'] + } + ); + peer.setName(key); + + channel.addPeer(peer); + } + } +} + +function newOrderer(client: Client) { + const caRootsPath = ORGS.orderer.tls_cacerts; + const data = fs.readFileSync(path.join(__dirname, caRootsPath)); + const caroots = Buffer.from(data).toString(); + return client.newOrderer(ORGS.orderer.url, { + 'pem': caroots, + 'ssl-target-name-override': ORGS.orderer['server-hostname'] + }); +} + +function getOrgName(org: string) { + return ORGS[org].name; +} + +function getMspID(org: string) { + logger.debug('Msp ID : ' + ORGS[org].mspid); + return ORGS[org].mspid; +} + +function newRemotes(names: string[], forPeers: boolean, userOrg: string) { + const client = getClientForOrg(userOrg); + + const targets: any[] = []; + // find the peer that match the names + names.forEach((n) => { + if (ORGS[userOrg].peers[n]) { + // found a peer matching the name + const data = fs.readFileSync( + path.join(__dirname, ORGS[userOrg].peers[n]['tls_cacerts'])); + const grpcOpts = { + 'pem': Buffer.from(data).toString(), + 'ssl-target-name-override': ORGS[userOrg].peers[n]['server-hostname'] + }; + + if (forPeers) { + targets.push(client.newPeer(ORGS[userOrg].peers[n].requests, grpcOpts)); + } else { + const eh = client.newEventHub(); + eh.setPeerAddr(ORGS[userOrg].peers[n].events, grpcOpts); + targets.push(eh); + } + } + }); + + if (targets.length === 0) { + logger.error(util.format('Failed to find peers matching the names %s', names)); + } + + return targets; +} + +async function getAdminUser(userOrg: string): Promise { + const users = hfc.getConfigSetting('admins'); + const username = users[0].username; + const password = users[0].secret; + + const client = getClientForOrg(userOrg); + + const store = await hfc.newDefaultKeyValueStore({ + path: getKeyStoreForOrg(getOrgName(userOrg)) + }); + + client.setStateStore(store); + + const user = await client.getUserContext(username, true); + + if (user && user.isEnrolled()) { + logger.info('Successfully loaded member from persistence'); + return user; + } + + const caClient = caClients[userOrg]; + + const enrollment = await caClient.enroll({ + enrollmentID: username, + enrollmentSecret: password + }); + + logger.info('Successfully enrolled user \'' + username + '\''); + const userOptions: UserOptions = { + username, + mspid: getMspID(userOrg), + cryptoContent: { + privateKeyPEM: enrollment.key.toBytes(), + signedCertPEM: enrollment.certificate + } + }; + + const member = await client.createUser(userOptions); + return member; +} + +export function newPeers(names: string[], org: string) { + return newRemotes(names, true, org); +} + +export function newEventHubs(names: string[], org: string) { + return newRemotes(names, false, org); +} + +export function setupChaincodeDeploy() { + process.env.GOPATH = path.join(__dirname, hfc.getConfigSetting('CC_SRC_PATH')); +} + +export function getOrgs() { + return ORGS; +} + +export function getClientForOrg(org: string): Client { + return clients[org]; +} + +export function getChannelForOrg(org: string): Channel { + return channels[org]; +} + +export function init() { + + hfc.addConfigFile(path.join(__dirname, config.networkConfigFile)); + hfc.addConfigFile(path.join(__dirname, '../app_config.json')); + + ORGS = hfc.getConfigSetting('network-config'); + + // set up the client and channel objects for each org + for (const key in ORGS) { + if (key.indexOf('org') === 0) { + const client = new hfc(); + + const cryptoSuite = hfc.newCryptoSuite(); + // TODO: Fix it up as setCryptoKeyStore is only available for s/w impl + (cryptoSuite as any).setCryptoKeyStore( + hfc.newCryptoKeyStore({ + path: getKeyStoreForOrg(ORGS[key].name) + })); + + client.setCryptoSuite(cryptoSuite); + + const channel = client.newChannel(hfc.getConfigSetting('channelName')); + channel.addOrderer(newOrderer(client)); + + clients[key] = client; + channels[key] = channel; + + setupPeers(channel, key, client); + + const caUrl = ORGS[key].ca; + caClients[key] = new copService( + caUrl, null /*defautl TLS opts*/, '' /* default CA */, cryptoSuite); + } + } +} + +export async function getRegisteredUsers( + username: string, userOrg: string): Promise { + + const client = getClientForOrg(userOrg); + + const store = await hfc.newDefaultKeyValueStore({ + path: getKeyStoreForOrg(getOrgName(userOrg)) + }); + + client.setStateStore(store); + const user = await client.getUserContext(username, true); + + if (user && user.isEnrolled()) { + logger.info('Successfully loaded member from persistence'); + return user; + } + + logger.info('Using admin to enroll this user ..'); + + // get the Admin and use it to enroll the user + const adminUser = await getAdminUser(userOrg); + + const caClient = caClients[userOrg]; + const secret = await caClient.register({ + enrollmentID: username, + affiliation: userOrg + '.department1' + }, adminUser); + + logger.debug(username + ' registered successfully'); + + const message = await caClient.enroll({ + enrollmentID: username, + enrollmentSecret: secret + }); + + if (message && typeof message === 'string' && message.includes( + 'Error:')) { + logger.error(username + ' enrollment failed'); + } + logger.debug(username + ' enrolled successfully'); + + const userOptions: UserOptions = { + username, + mspid: getMspID(userOrg), + cryptoContent: { + privateKeyPEM: message.key.toBytes(), + signedCertPEM: message.certificate + } + }; + + const member = await client.createUser(userOptions); + return member; +} + +export function getLogger(moduleName: string) { + const moduleLogger = log4js.getLogger(moduleName); + moduleLogger.setLevel('DEBUG'); + return moduleLogger; +} + +export async function getOrgAdmin(userOrg: string): Promise { + const admin = ORGS[userOrg].admin; + const keyPath = path.join(__dirname, admin.key); + const keyPEM = Buffer.from(readAllFiles(keyPath)[0]).toString(); + const certPath = path.join(__dirname, admin.cert); + const certPEM = readAllFiles(certPath)[0].toString(); + + const client = getClientForOrg(userOrg); + const cryptoSuite = hfc.newCryptoSuite(); + + if (userOrg) { + (cryptoSuite as any).setCryptoKeyStore( + hfc.newCryptoKeyStore({ path: getKeyStoreForOrg(getOrgName(userOrg)) })); + client.setCryptoSuite(cryptoSuite); + } + + const store = await hfc.newDefaultKeyValueStore({ + path: getKeyStoreForOrg(getOrgName(userOrg)) + }); + + client.setStateStore(store); + + return client.createUser({ + username: 'peer' + userOrg + 'Admin', + mspid: getMspID(userOrg), + cryptoContent: { + privateKeyPEM: keyPEM, + signedCertPEM: certPEM + } + }); +} diff --git a/balance-transfer/typescript/lib/network-config.json b/balance-transfer/typescript/lib/network-config.json new file mode 100644 index 0000000000..2ec10ac91c --- /dev/null +++ b/balance-transfer/typescript/lib/network-config.json @@ -0,0 +1,55 @@ +{ + "network-config": { + "orderer": { + "url": "grpcs://localhost:7050", + "server-hostname": "orderer.example.com", + "tls_cacerts": "../artifacts/channel/crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/tls/ca.crt" + }, + "org1": { + "name": "peerOrg1", + "mspid": "Org1MSP", + "ca": "https://localhost:7054", + "peers": { + "peer1": { + "requests": "grpcs://localhost:7051", + "events": "grpcs://localhost:7053", + "server-hostname": "peer0.org1.example.com", + "tls_cacerts": "../artifacts/channel/crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt" + }, + "peer2": { + "requests": "grpcs://localhost:7056", + "events": "grpcs://localhost:7058", + "server-hostname": "peer1.org1.example.com", + "tls_cacerts": "../artifacts/channel/crypto-config/peerOrganizations/org1.example.com/peers/peer1.org1.example.com/tls/ca.crt" + } + }, + "admin": { + "key": "../artifacts/channel/crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/keystore", + "cert": "../artifacts/channel/crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/signcerts" + } + }, + "org2": { + "name": "peerOrg2", + "mspid": "Org2MSP", + "ca": "https://localhost:8054", + "peers": { + "peer1": { + "requests": "grpcs://localhost:8051", + "events": "grpcs://localhost:8053", + "server-hostname": "peer0.org2.example.com", + "tls_cacerts": "../artifacts/channel/crypto-config/peerOrganizations/org2.example.com/peers/peer0.org2.example.com/tls/ca.crt" + }, + "peer2": { + "requests": "grpcs://localhost:8056", + "events": "grpcs://localhost:8058", + "server-hostname": "peer1.org2.example.com", + "tls_cacerts": "../artifacts/channel/crypto-config/peerOrganizations/org2.example.com/peers/peer1.org2.example.com/tls/ca.crt" + } + }, + "admin": { + "key": "../artifacts/channel/crypto-config/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp/keystore", + "cert": "../artifacts/channel/crypto-config/peerOrganizations/org2.example.com/users/Admin@org2.example.com/msp/signcerts" + } + } + } +} diff --git a/balance-transfer/typescript/package.json b/balance-transfer/typescript/package.json new file mode 100644 index 0000000000..69a6536911 --- /dev/null +++ b/balance-transfer/typescript/package.json @@ -0,0 +1,37 @@ +{ + "name": "balance-transfer-typescript", + "version": "0.1.0", + "description": "The balance transfer sample written using typescript", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Kapil Sachdeva", + "license": "Apache-2.0", + "devDependencies": { + "@types/body-parser": "^1.16.5", + "@types/cors": "^2.8.1", + "@types/express-jwt": "0.0.37", + "@types/express-session": "^1.15.3", + "@types/jsonwebtoken": "^7.2.3", + "@types/log4js": "0.0.33", + "@types/node": "^8.0.33", + "express-bearer-token": "^2.1.0", + "jsonwebtoken": "^8.1.0", + "ts-node": "^3.3.0", + "tslint": "^5.6.0", + "tslint-microsoft-contrib": "^5.0.1", + "typescript": "^2.5.3" + }, + "dependencies": { + "body-parser": "^1.18.2", + "cookie-parser": "^1.4.3", + "cors": "^2.8.4", + "express": "^4.16.1", + "express-jwt": "^5.3.0", + "express-session": "^1.15.6", + "fabric-ca-client": "^1.0.2", + "fabric-client": "^1.0.2", + "log4js": "^0.6.38" + } +} diff --git a/balance-transfer/typescript/runApp.sh b/balance-transfer/typescript/runApp.sh new file mode 100755 index 0000000000..be79b9d4ac --- /dev/null +++ b/balance-transfer/typescript/runApp.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +function dkcl(){ + CONTAINER_IDS=$(docker ps -aq) + echo + if [ -z "$CONTAINER_IDS" -o "$CONTAINER_IDS" = " " ]; then + echo "========== No containers available for deletion ==========" + else + docker rm -f $CONTAINER_IDS + fi + echo +} + +function dkrm(){ + DOCKER_IMAGE_IDS=$(docker images | grep "dev\|none\|test-vp\|peer[0-9]-" | awk '{print $3}') + echo + if [ -z "$DOCKER_IMAGE_IDS" -o "$DOCKER_IMAGE_IDS" = " " ]; then + echo "========== No images available for deletion ===========" + else + docker rmi -f $DOCKER_IMAGE_IDS + fi + echo +} + +function restartNetwork() { + echo + + #teardown the network and clean the containers and intermediate images + docker-compose -f ../artifacts/docker-compose.yaml down + dkcl + dkrm + + #Cleanup the material + rm -rf /tmp/hfc-test-kvs_peerOrg* $HOME/.hfc-key-store/ /tmp/fabric-client-kvs_peerOrg* + + #Start the network + docker-compose -f ../artifacts/docker-compose.yaml up -d + echo +} + +function installNodeModules() { + echo + if [ -d node_modules ]; then + echo "============== node modules installed already =============" + else + echo "============== Installing node modules =============" + npm install + fi + copyIndex fabric-client/index.d.ts + copyIndex fabric-ca-client/index.d.ts + echo +} + +function copyIndex() { + if [ ! -f node_modules/$1 ]; then + cp types/$1 node_modules/$1 + fi +} + +restartNetwork + +installNodeModules + + + +PORT=4000 ts-node app.ts diff --git a/balance-transfer/typescript/testAPIs.sh b/balance-transfer/typescript/testAPIs.sh new file mode 100755 index 0000000000..a720e53378 --- /dev/null +++ b/balance-transfer/typescript/testAPIs.sh @@ -0,0 +1,197 @@ +#!/bin/bash +# +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +jq --version > /dev/null 2>&1 +if [ $? -ne 0 ]; then + echo "Please Install 'jq' https://stedolan.github.io/jq/ to execute this script" + echo + exit 1 +fi +starttime=$(date +%s) + +echo "POST request Enroll on Org1 ..." +echo +ORG1_TOKEN=$(curl -s -X POST \ + http://localhost:4000/users \ + -H "content-type: application/x-www-form-urlencoded" \ + -d 'username=Jim&orgName=org1') +echo $ORG1_TOKEN +ORG1_TOKEN=$(echo $ORG1_TOKEN | jq ".token" | sed "s/\"//g") +echo +echo "ORG1 token is $ORG1_TOKEN" +echo +echo "POST request Enroll on Org2 ..." +echo +ORG2_TOKEN=$(curl -s -X POST \ + http://localhost:4000/users \ + -H "content-type: application/x-www-form-urlencoded" \ + -d 'username=Barry&orgName=org2') +echo $ORG2_TOKEN +ORG2_TOKEN=$(echo $ORG2_TOKEN | jq ".token" | sed "s/\"//g") +echo +echo "ORG2 token is $ORG2_TOKEN" +echo +echo +echo "POST request Create channel ..." +echo +curl -s -X POST \ + http://localhost:4000/channels \ + -H "authorization: Bearer $ORG1_TOKEN" \ + -H "content-type: application/json" \ + -d '{ + "channelName":"mychannel", + "channelConfigPath":"../artifacts/channel/mychannel.tx" +}' +echo +echo +sleep 5 +echo "POST request Join channel on Org1" +echo +curl -s -X POST \ + http://localhost:4000/channels/mychannel/peers \ + -H "authorization: Bearer $ORG1_TOKEN" \ + -H "content-type: application/json" \ + -d '{ + "peers": ["peer1","peer2"] +}' +echo +echo + +echo "POST request Join channel on Org2" +echo +curl -s -X POST \ + http://localhost:4000/channels/mychannel/peers \ + -H "authorization: Bearer $ORG2_TOKEN" \ + -H "content-type: application/json" \ + -d '{ + "peers": ["peer1","peer2"] +}' +echo +echo + +echo "POST Install chaincode on Org1" +echo +curl -s -X POST \ + http://localhost:4000/chaincodes \ + -H "authorization: Bearer $ORG1_TOKEN" \ + -H "content-type: application/json" \ + -d '{ + "peers": ["peer1", "peer2"], + "chaincodeName":"mycc", + "chaincodePath":"github.com/example_cc", + "chaincodeVersion":"v0" +}' +echo +echo + + +echo "POST Install chaincode on Org2" +echo +curl -s -X POST \ + http://localhost:4000/chaincodes \ + -H "authorization: Bearer $ORG2_TOKEN" \ + -H "content-type: application/json" \ + -d '{ + "peers": ["peer1","peer2"], + "chaincodeName":"mycc", + "chaincodePath":"github.com/example_cc", + "chaincodeVersion":"v0" +}' +echo +echo + +echo "POST instantiate chaincode on peer1 of Org1" +echo +curl -s -X POST \ + http://localhost:4000/channels/mychannel/chaincodes \ + -H "authorization: Bearer $ORG1_TOKEN" \ + -H "content-type: application/json" \ + -d '{ + "chaincodeName":"mycc", + "chaincodeVersion":"v0", + "args":["a","100","b","200"] +}' +echo +echo + +echo "POST invoke chaincode on peers of Org1 and Org2" +echo +TRX_ID=$(curl -s -X POST \ + http://localhost:4000/channels/mychannel/chaincodes/mycc \ + -H "authorization: Bearer $ORG1_TOKEN" \ + -H "content-type: application/json" \ + -d '{ + "fcn":"move", + "args":["a","b","10"] +}') +echo "Transacton ID is $TRX_ID" +echo +echo + +echo "GET query chaincode on peer1 of Org1" +echo +curl -s -X GET \ + "http://localhost:4000/channels/mychannel/chaincodes/mycc?peer=peer1&fcn=query&args=%5B%22a%22%5D" \ + -H "authorization: Bearer $ORG1_TOKEN" \ + -H "content-type: application/json" +echo +echo + +echo "GET query Block by blockNumber" +echo +curl -s -X GET \ + "http://localhost:4000/channels/mychannel/blocks/1?peer=peer1" \ + -H "authorization: Bearer $ORG1_TOKEN" \ + -H "content-type: application/json" +echo +echo + +echo "GET query Transaction by TransactionID" +echo +curl -s -X GET http://localhost:4000/channels/mychannel/transactions/$TRX_ID?peer=peer1 \ + -H "authorization: Bearer $ORG1_TOKEN" \ + -H "content-type: application/json" +echo +echo + +echo "GET query ChainInfo" +echo +curl -s -X GET \ + "http://localhost:4000/channels/mychannel?peer=peer1" \ + -H "authorization: Bearer $ORG1_TOKEN" \ + -H "content-type: application/json" +echo +echo + +echo "GET query Installed chaincodes" +echo +curl -s -X GET \ + "http://localhost:4000/chaincodes?peer=peer1&type=installed" \ + -H "authorization: Bearer $ORG1_TOKEN" \ + -H "content-type: application/json" +echo +echo + +echo "GET query Instantiated chaincodes" +echo +curl -s -X GET \ + "http://localhost:4000/chaincodes?peer=peer1&type=instantiated" \ + -H "authorization: Bearer $ORG1_TOKEN" \ + -H "content-type: application/json" +echo +echo + +echo "GET query Channels" +echo +curl -s -X GET \ + "http://localhost:4000/channels?peer=peer1" \ + -H "authorization: Bearer $ORG1_TOKEN" \ + -H "content-type: application/json" +echo +echo + +echo "Total execution time : $(($(date +%s)-starttime)) secs ..." diff --git a/balance-transfer/typescript/tsconfig.json b/balance-transfer/typescript/tsconfig.json new file mode 100644 index 0000000000..7d6de87516 --- /dev/null +++ b/balance-transfer/typescript/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "removeComments": false, + "preserveConstEnums": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": true, + "declaration": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "suppressImplicitAnyIndexErrors": true, + "moduleResolution": "node", + "module": "commonjs", + "target": "es6", + "outDir": "dist", + "baseUrl": ".", + "typeRoots": [ + "types", + "node_modules/@types" + ] + }, + "formatCodeOptions": { + "indentSize": 2, + "tabSize": 2 + } +} \ No newline at end of file diff --git a/balance-transfer/typescript/tslint.json b/balance-transfer/typescript/tslint.json new file mode 100644 index 0000000000..9064616325 --- /dev/null +++ b/balance-transfer/typescript/tslint.json @@ -0,0 +1,38 @@ +{ + "extends": "tslint:recommended", + "rulesDirectory": [ + "tslint-microsoft-contrib" + ], + "rules": { + "trailing-comma": [false, { + "multiline": "always", + "singleline": "never" + }], + "interface-name": [false, "always-prefix"], + "no-console": [true, + "time", + "timeEnd", + "trace" + ], + "max-line-length": [ + true, + 100 + ], + "no-string-literal": false, + "no-use-before-declare": true, + "object-literal-sort-keys": false, + "ordered-imports": [false], + "quotemark": [ + true, + "single", + "avoid-escape" + ], + "variable-name": [ + true, + "allow-leading-underscore", + "allow-pascal-case", + "ban-keywords", + "check-format" + ] + } +} \ No newline at end of file diff --git a/balance-transfer/typescript/types/fabric-ca-client/index.d.ts b/balance-transfer/typescript/types/fabric-ca-client/index.d.ts new file mode 100644 index 0000000000..e5c21a9134 --- /dev/null +++ b/balance-transfer/typescript/types/fabric-ca-client/index.d.ts @@ -0,0 +1,18 @@ +/** + * Copyright 2017 Kapil Sachdeva All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +declare module 'fabric-ca-client' { +} \ No newline at end of file diff --git a/balance-transfer/typescript/types/fabric-client/index.d.ts b/balance-transfer/typescript/types/fabric-client/index.d.ts new file mode 100644 index 0000000000..db17494df3 --- /dev/null +++ b/balance-transfer/typescript/types/fabric-client/index.d.ts @@ -0,0 +1,312 @@ +/** + * Copyright 2017 Kapil Sachdeva All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +declare enum Status { + UNKNOWN = 0, + SUCCESS = 200, + BAD_REQUEST = 400, + FORBIDDEN = 403, + NOT_FOUND = 404, + REQUEST_ENTITY_TOO_LARGE = 413, + INTERNAL_SERVER_ERROR = 500, + SERVICE_UNAVAILABLE = 503 +} + +type ChaicodeType = "golang" | "car" | "java"; + +interface ProtoBufObject { + toBuffer(): Buffer; +} + +interface KeyOpts { + ephemeral: boolean; +} + +interface ConnectionOptions { + +} + +interface ConfigSignature extends ProtoBufObject { + signature_header: Buffer; + signature: Buffer; +} + +interface ICryptoKey { + getSKI(): string; + isSymmetric(): boolean; + isPrivate(): boolean; + getPublicKey(): ICryptoKey; + toBytes(): string; +} + +interface ICryptoKeyStore { + getKey(ski: string): Promise; + putKey(key: ICryptoKey): Promise; +} + +interface IKeyValueStore { + getValue(name: string): Promise; + setValue(name: string, value: string): Promise; +} + +interface IdentityFiles { + privateKey: string; + signedCert: string; +} + +interface IdentityPEMs { + privateKeyPEM: string; + signedCertPEM: string; +} + +interface UserOptions { + username: string; + mspid: string; + cryptoContent: IdentityFiles | IdentityPEMs; +} + +interface ICryptoSuite { + decrypt(key: ICryptoKey, cipherText: Buffer, opts: any): Buffer; + deriveKey(key: ICryptoKey): ICryptoKey; + encrypt(key: ICryptoKey, plainText: Buffer, opts: any): Buffer; + getKey(ski: string): Promise; + generateKey(opts: KeyOpts): Promise; + hash(msg: string, opts: any): string; + importKey(pem: string, opts: KeyOpts): ICryptoKey | Promise; + sign(key: ICryptoKey, digest: Buffer): Buffer; + verify(key: ICryptoKey, signature: Buffer, digest: Buffer): boolean; +} + +interface ChannelRequest { + name: string; + orderer: Orderer; + envelope?: Buffer; + config?: Buffer; + txId?: TransactionId; + signatures: ConfigSignature[]; +} + +interface TransactionRequest { + proposalResponses: ProposalResponse[]; + proposal: Proposal; +} + +interface BroadcastResponse { + status: string; +} + +interface IIdentity { + serialize(): Buffer; + getMSPId(): string; + isValid(): boolean; + getOrganizationUnits(): string; + verify(msg: Buffer, signature: Buffer, opts: any): boolean; +} + +interface ISigningIdentity { + sign(msg: Buffer, opts: any): Buffer; +} + +interface ChaincodeInstallRequest { + targets: Peer[]; + chaincodePath: string; + chaincodeId: string; + chaincodeVersion: string; + chaincodePackage?: Buffer; + chaincodeType?: ChaicodeType; +} + +interface ChaincodeInstantiateUpgradeRequest { + targets?: Peer[]; + chaincodeType?: string; + chaincodeId: string; + chaincodeVersion: string; + txId: TransactionId; + fcn?: string; + args?: string[]; + 'endorsement-policy'?: any; +} + +interface ChaincodeInvokeRequest { + targets?: Peer[]; + chaincodeId: string; + txId: TransactionId; + fcn?: string; + args: string[]; +} + +interface ChaincodeQueryRequest { + targets?: Peer[]; + chaincodeId: string; + txId: TransactionId; + fcn?: string; + args: string[]; +} + +interface ChaincodeInfo { + name: string; + version: string; + path: string; + input: string; + escc: string; + vscc: string; +} + +interface ChannelInfo { + channel_id: string; +} + +interface ChaincodeQueryResponse { + chaincodes: ChaincodeInfo[]; +} + +interface ChannelQueryResponse { + channels: ChannelInfo[]; +} + +interface OrdererRequest { + txId: TransactionId; +} + +interface JoinChannelRequest { + txId: TransactionId; + targets: Peer[]; + block: Buffer; +} + +interface ResponseObject { + status: Status; + message: string; + payload: Buffer; +} + +interface Proposal { + header: ByteBuffer; + payload: ByteBuffer; + extension: ByteBuffer; +} + +interface Header { + channel_header: ByteBuffer; + signature_header: ByteBuffer; +} + +interface ProposalResponse { + version: number; + timestamp: Date; + response: ResponseObject; + payload: Buffer; + endorsement: any; +} + +type ProposalResponseObject = [Array, Proposal, Header]; + +declare class Orderer { +} + +declare class Peer { + setName(name: string): void; + getName(): string; +} + +declare class EventHub { + connect(): void; + disconnect(): void; + getPeerAddr(): string; + setPeerAddr(url: string, opts: ConnectionOptions): void; + isconnected(): boolean; + registerBlockEvent(onEvent: (b: any) => void, onError?: (err: Error) => void): number; + registerTxEvent(txId: string, onEvent: (txId: any, code: string) => void, onError?: (err: Error) => void): void; + unregisterTxEvent(txId: string): void; +} + +declare class Channel { + initialize(): Promise; + addOrderer(orderer: Orderer): void; + addPeer(peer: Peer): void; + getGenesisBlock(request: OrdererRequest): Promise; + getChannelConfig(): Promise; + joinChannel(request: JoinChannelRequest): Promise; + sendInstantiateProposal(request: ChaincodeInstantiateUpgradeRequest): Promise; + sendTransactionProposal(request: ChaincodeInvokeRequest): Promise; + sendTransaction(request: TransactionRequest): Promise; + queryByChaincode(request: ChaincodeQueryRequest): Promise; + queryBlock(blockNumber: number, target: Peer): Promise; + queryTransaction(txId: string, target: Peer): Promise; + queryInstantiatedChaincodes(target: Peer): Promise; + queryInfo(target: Peer): Promise; + getOrderers(): Orderer[]; + getPeers(): Peer[]; +} + +declare abstract class BaseClient { + static setLogger(logger: any): void; + static addConfigFile(path: string): void; + static getConfigSetting(name: string, default_value?: any): any; + static newCryptoSuite(): ICryptoSuite; + static newCryptoKeyStore(obj?: { path: string }): ICryptoKeyStore; + static newDefaultKeyValueStore(obj?: { path: string }): Promise; + setCryptoSuite(suite: ICryptoSuite): void; + getCryptoSuite(): ICryptoSuite; +} + +declare class TransactionId { + getTransactionID(): string; +} + +interface UserConfig { + enrollmentID: string; + name: string + roles?: string[]; + affiliation?: string; +} + +declare class User { + isEnrolled(): boolean; + getName(): string; + getRoles(): string[]; + setRoles(roles: string[]): void; + getAffiliation(): string; + setAffiliation(affiliation: string): void; + getIdentity(): IIdentity; + getSigningIdentity(): ISigningIdentity; + setCryptoSuite(suite: ICryptoSuite): void; + setEnrollment(privateKey: ICryptoKey, certificate: string, mspId: string): Promise; +} + +declare class Client extends BaseClient { + isDevMode(): boolean; + getUserContext(name: string, checkPersistence: boolean): Promise | User; + setUserContext(user: User, skipPersistence?: boolean): Promise; + setDevMode(mode: boolean): void; + newOrderer(url: string, opts: ConnectionOptions): Orderer; + newChannel(name: string): Channel; + newPeer(url: string, opts: ConnectionOptions): Peer; + newEventHub(): EventHub; + newTransactionID(): TransactionId; + extractChannelConfig(envelope: Buffer): Buffer; + createChannel(request: ChannelRequest): Promise; + createUser(opts: UserOptions): Promise; + signChannelConfig(config: Buffer): ConfigSignature; + setStateStore(store: IKeyValueStore): void; + installChaincode(request: ChaincodeInstallRequest): Promise; + queryInstalledChaincodes(target: Peer): Promise; + queryChannels(target: Peer): Promise; +} + +declare module 'fabric-client' { + export = Client; +}