前面的文章都是在講解Fabric網絡的搭建和ChainCode的開發,那麼在ChainCode開發完畢後,咱們就須要使用Fabric SDK作應用程序的開發了。官方雖然提供了Node.JS,Java,Go,Python等多種語言的SDK,可是因爲整個Fabric太新了,不少SDK還不成熟和完善,因此我採用Node JS的SDK,畢竟這個是功能畢竟齊全,並且也是官方示例的時候使用的SDK。因爲我歷來沒有接觸過Node.JS的開發,對這個語言理解不深,因此講的比較膚淺,但願你們見諒。html
Node.js是一個跨平臺的語言,能夠在Linux,Window和Mac上安裝,咱們在開發的時候能夠在Windows下開發,最後生產環境通常都是Linux,因此咱們這裏就以Ubuntu爲例。Fabric Node SDK支持的Node版本是v6,不支持最新的v8版本。NodeJS官方給咱們提供了很方便的安裝方法,具體文檔在:https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributionsnode
咱們只須要執行如下命令便可安裝NodeJS的最新v6版本:linux
curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash - sudo apt-get install -y nodejs
安裝完成後咱們可使用如下兩個命令來查看安裝的Node版本和npm版本。
node –v
npm -v
關於NPM,這個是一個包管理器,我以爲很像VS裏面的NuGet,關於NPM的基礎知識,咱們能夠參考這篇博客:http://www.ruanyifeng.com/blog/2016/01/npm-install.html
只要安裝好node和npm,接下來咱們就能夠進行Fabric Node SDK Application的開發了。
因爲咱們想基於官方Example的e2e_cli裏面的Fabric網絡來寫程序,關於Fabric網絡的搭建我就很少說,你們能夠參考我以前的博客。總之結果就是咱們如今已經成功運行了e2e_cli這個網絡,也就是說Example02這個ChainCode已經安裝部署,而且測試經過了,咱們接下來只是換用Node SDK的方式進行查詢和調用。
咱們首先在當前用戶的根目錄創建一個nodeTest的文件夾,用於存放咱們關於node的相關項目文件,而後在其中新建一個包配置文件,package.json
mkdir ~/nodeTest cd ~/nodeTest vi package.json
在這個文件中,咱們能夠定義不少項目相關的屬性,這篇博客詳細的介紹了每一個屬性有什麼用,你們能夠參考:http://www.cnblogs.com/tzyy/p/5193811.html
總之,最後咱們在package.json中放入瞭如下內容:
{ "name": "nodeTest", "version": "1.0.0", "description": "Hyperledger Fabric Node SDK Test Application", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { "fabric-ca-client": "^1.0.0", "fabric-client": "^1.0.0" }, "author": "Devin Zeng", "license": "Apache-2.0", "keywords": [ "Hyperledger", "Fabric", "Test", "Application" ] }
最主要的就是dependencies,這裏咱們放了Fabric CA Client和Fabric Node SDK的Client,雖然本示例中沒用到CA Client,可是之後會用到,因此先放在這裏了。git
編輯保存好該文件後,咱們就能夠運行npm install命令來下載全部相關的依賴模塊,可是因爲npm服務器在國外,因此下載可能會很慢,感謝淘寶爲咱們提供了國內的npm鏡像,使得安裝npm模塊快不少。運行的命令是:
npm install --registry=https://registry.npm.taobao.org
運行完畢後咱們查看一下nodeTest目錄,能夠看到多了一個node_modules文件夾。這裏就是使用剛纔的命令下載下來的全部依賴包。
下面咱們新建一個query.js文件,開始咱們的Fabric Node SDK編碼工做。因爲代碼比較長,因此我就不分步講了,直接在代碼中增長註釋,將完整代碼貼出來:github
'use strict'; var hfc = require('fabric-client'); var path = require('path'); var sdkUtils = require('fabric-client/lib/utils') var fs = require('fs'); var options = { user_id: 'Admin@org1.example.com', msp_id:'Org1MSP', channel_id: 'mychannel', chaincode_id: 'mycc', network_url: 'grpcs://localhost:7051',//由於啓用了TLS,因此是grpcs,若是沒有啓用TLS,那麼就是grpc privateKeyFolder:'/home/studyzy/go/src/github.com/hyperledger/fabric/examples/e2e_cli/crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/keystore', signedCert:'/home/studyzy/go/src/github.com/hyperledger/fabric/examples/e2e_cli/crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/signcerts/Admin@org1.example.com-cert.pem', tls_cacerts:'/home/studyzy/go/src/github.com/hyperledger/fabric/examples/e2e_cli/crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt', server_hostname: "peer0.org1.example.com" }; var channel = {}; var client = null; const getKeyFilesInDir = (dir) => { //該函數用於找到keystore目錄下的私鑰文件的路徑 var files = fs.readdirSync(dir) var keyFiles = [] files.forEach((file_name) => { let filePath = path.join(dir, file_name) if (file_name.endsWith('_sk')) { keyFiles.push(filePath) } }) return keyFiles } Promise.resolve().then(() => { console.log("Load privateKey and signedCert"); client = new hfc(); var createUserOpt = { username: options.user_id, mspid: options.msp_id, cryptoContent: { privateKey: getKeyFilesInDir(options.privateKeyFolder)[0], signedCert: options.signedCert } } //以上代碼指定了當前用戶的私鑰,證書等基本信息 return sdkUtils.newKeyValueStore({ path: "/tmp/fabric-client-stateStore/" }).then((store) => { client.setStateStore(store) return client.createUser(createUserOpt) }) }).then((user) => { channel = client.newChannel(options.channel_id); let data = fs.readFileSync(options.tls_cacerts); let peer = client.newPeer(options.network_url, { pem: Buffer.from(data).toString(), 'ssl-target-name-override': options.server_hostname } ); peer.setName("peer0"); //由於啓用了TLS,因此上面的代碼就是指定TLS的CA證書 channel.addPeer(peer); return; }).then(() => { console.log("Make query"); var transaction_id = client.newTransactionID(); console.log("Assigning transaction_id: ", transaction_id._transaction_id); //構造查詢request參數 const request = { chaincodeId: options.chaincode_id, txId: transaction_id, fcn: 'query', args: ['a'] }; return channel.queryByChaincode(request); }).then((query_responses) => { console.log("returned from query"); if (!query_responses.length) { console.log("No payloads were returned from query"); } else { console.log("Query result count = ", query_responses.length) } if (query_responses[0] instanceof Error) { console.error("error from query = ", query_responses[0]); } console.log("Response is ", query_responses[0].toString());//打印返回的結果 }).catch((err) => { console.error("Caught Error", err); });
編寫完代碼,咱們想要測試一下咱們的代碼是否靠譜,直接運行npm
node query.js
便可,咱們能夠看到,a帳戶的餘額是90元。json
studyzy@ubuntu1:~/nodeTest$ node query.js Load privateKey and signedCert Make query Assigning transaction_id: ee3ac35d40d8510813546a2216ad9c0d91213b8e1bba9b7fe19cfeff3014e38a returned from query Query result count = 1 Response is 90
爲何a帳戶是90?由於咱們跑e2e_cli的Fabric網絡時,系統會自動安裝Example02的ChainCode,而後自動跑查詢,轉帳等操做。ubuntu
相比較於Query方法,Invoke方法要複雜的多,主要是由於Invoke須要和Orderer通訊,並且發起了Transaction以後,還要設置EventHub來接收消息。下面貼出invoke.js的所有內容,對於比較重要的部分我進行了註釋:promise
'use strict'; var hfc = require('fabric-client'); var path = require('path'); var util = require('util'); var sdkUtils = require('fabric-client/lib/utils') const fs = require('fs'); var options = { user_id: 'Admin@org1.example.com', msp_id:'Org1MSP', channel_id: 'mychannel', chaincode_id: 'mycc', peer_url: 'grpcs://localhost:7051',//由於啓用了TLS,因此是grpcs,若是沒有啓用TLS,那麼就是grpc event_url: 'grpcs://localhost:7053',//由於啓用了TLS,因此是grpcs,若是沒有啓用TLS,那麼就是grpc orderer_url: 'grpcs://localhost:7050',//由於啓用了TLS,因此是grpcs,若是沒有啓用TLS,那麼就是grpc privateKeyFolder:'/home/studyzy/go/src/github.com/hyperledger/fabric/examples/e2e_cli/crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/keystore', signedCert:'/home/studyzy/go/src/github.com/hyperledger/fabric/examples/e2e_cli/crypto-config/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp/signcerts/Admin@org1.example.com-cert.pem', peer_tls_cacerts:'/home/studyzy/go/src/github.com/hyperledger/fabric/examples/e2e_cli/crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt', orderer_tls_cacerts:'/home/studyzy/go/src/github.com/hyperledger/fabric/examples/e2e_cli/crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/tls/ca.crt', server_hostname: "peer0.org1.example.com" }; var channel = {}; var client = null; var targets = []; var tx_id = null; const getKeyFilesInDir = (dir) => { //該函數用於找到keystore目錄下的私鑰文件的路徑 const files = fs.readdirSync(dir) const keyFiles = [] files.forEach((file_name) => { let filePath = path.join(dir, file_name) if (file_name.endsWith('_sk')) { keyFiles.push(filePath) } }) return keyFiles } Promise.resolve().then(() => { console.log("Load privateKey and signedCert"); client = new hfc(); var createUserOpt = { username: options.user_id, mspid: options.msp_id, cryptoContent: { privateKey: getKeyFilesInDir(options.privateKeyFolder)[0], signedCert: options.signedCert } } //以上代碼指定了當前用戶的私鑰,證書等基本信息 return sdkUtils.newKeyValueStore({ path: "/tmp/fabric-client-stateStore/" }).then((store) => { client.setStateStore(store) return client.createUser(createUserOpt) }) }).then((user) => { channel = client.newChannel(options.channel_id); let data = fs.readFileSync(options.peer_tls_cacerts); let peer = client.newPeer(options.peer_url, { pem: Buffer.from(data).toString(), 'ssl-target-name-override': options.server_hostname } ); //由於啓用了TLS,因此上面的代碼就是指定Peer的TLS的CA證書 channel.addPeer(peer); //接下來鏈接Orderer的時候也啓用了TLS,也是一樣的處理方法 let odata = fs.readFileSync(options.orderer_tls_cacerts); let caroots = Buffer.from(odata).toString(); var orderer = client.newOrderer(options.orderer_url, { 'pem': caroots, 'ssl-target-name-override': "orderer.example.com" }); channel.addOrderer(orderer); targets.push(peer); return; }).then(() => { tx_id = client.newTransactionID(); console.log("Assigning transaction_id: ", tx_id._transaction_id); //發起轉帳行爲,將a->b 10元 var request = { targets: targets, chaincodeId: options.chaincode_id, fcn: 'invoke', args: ['a', 'b', '10'], chainId: options.channel_id, txId: tx_id }; return channel.sendTransactionProposal(request); }).then((results) => { var proposalResponses = results[0]; var proposal = results[1]; var header = results[2]; let isProposalGood = false; if (proposalResponses && proposalResponses[0].response && proposalResponses[0].response.status === 200) { isProposalGood = true; console.log('transaction proposal was good'); } else { console.error('transaction proposal was bad'); } if (isProposalGood) { console.log(util.format( '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)); var request = { proposalResponses: proposalResponses, proposal: proposal, header: header }; // set the transaction listener and set a timeout of 30sec // if the transaction did not get committed within the timeout period, // fail the test var transactionID = tx_id.getTransactionID(); var eventPromises = []; let eh = client.newEventHub(); //接下來設置EventHub,用於監聽Transaction是否成功寫入,這裏也是啓用了TLS let data = fs.readFileSync(options.peer_tls_cacerts); let grpcOpts = { pem: Buffer.from(data).toString(), 'ssl-target-name-override': options.server_hostname } eh.setPeerAddr(options.event_url,grpcOpts); eh.connect(); let txPromise = new Promise((resolve, reject) => { let handle = setTimeout(() => { eh.disconnect(); reject(); }, 30000); //向EventHub註冊事件的處理辦法 eh.registerTxEvent(transactionID, (tx, code) => { clearTimeout(handle); eh.unregisterTxEvent(transactionID); eh.disconnect(); if (code !== 'VALID') { console.error( 'The transaction was invalid, code = ' + code); reject(); } else { console.log( 'The transaction has been committed on peer ' + eh._ep._endpoint.addr); resolve(); } }); }); eventPromises.push(txPromise); var sendPromise = channel.sendTransaction(request); return Promise.all([sendPromise].concat(eventPromises)).then((results) => { console.log(' event promise all complete and testing complete'); return results[0]; // the first returned value is from the 'sendPromise' which is from the 'sendTransaction()' call }).catch((err) => { console.error( 'Failed to send transaction and get notifications within the timeout period.' ); return 'Failed to send transaction and get notifications within the timeout period.'; }); } else { console.error( 'Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...' ); return 'Failed to send Proposal or receive valid response. Response null or status is not 200. exiting...'; } }, (err) => { console.error('Failed to send proposal due to error: ' + err.stack ? err.stack : err); return 'Failed to send proposal due to error: ' + err.stack ? err.stack : err; }).then((response) => { if (response.status === 'SUCCESS') { console.log('Successfully sent transaction to the orderer.'); return tx_id.getTransactionID(); } else { console.error('Failed to order the transaction. Error code: ' + response.status); return 'Failed to order the transaction. Error code: ' + response.status; } }, (err) => { console.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; });
保存文件並退出,接下來測試一下咱們的代碼,運行:bash
node invoke.js
咱們能夠看到系統返回以下結果:
Load privateKey and signedCert Assigning transaction_id: 1adbf20ace0d1601b00cc2b9dfdd4a431cfff9a13f6a6f5e5e4a80c897e0f7a8 transaction proposal was good Successfully sent Proposal and received ProposalResponse: Status - 200, message - "OK", metadata - "", endorsement signature: 0D x��N��n�#���/�G���QD�w�����As� \]��FfWҡ�+������=m9I���� 6�i info: [EventHub.js]: _connect - options {"grpc.ssl_target_name_override":"peer0.org1.example.com","grpc.default_authority":"peer0.org1.example.com"} The transaction has been committed on peer localhost:7053 event promise all complete and testing complete Successfully sent transaction to the orderer.
從打印出的結果看,咱們的轉帳已經成功了,咱們能夠從新調用以前寫的query.js從新查詢,能夠看到a帳戶的餘額已經變少了10元。
咱們以上的query和Invoke都是參照了官方的fabcar示例,該示例在https://github.com/hyperledger/fabric-samples/tree/release/fabcar
這只是簡單的測試Node SDK是否可用,若是咱們要作項目,那麼就會複雜不少,能夠參考官方的兩個項目:
https://github.com/hyperledger/fabric-samples/tree/release/balance-transfer
https://github.com/IBM-Blockchain/marbles
我以前一直卡在怎麼基於某個用戶的私鑰和證書來設置當前的Context,後來感謝neswater的幫助,終於才解決了這個問題。還有就是TLS的問題,官方給出的fabcar是沒有TLS的,我搞了半天才搞定,原來除了制定TLS證書以外,咱們訪問Peer的URL也是不同的。
最後,你們若是想進一步探討Fabric或者使用中遇到什麼問題能夠加入QQ羣【494085548】你們一塊兒討論。