├── .gitignore ├── .jshintignore ├── .jshintrc ├── LICENSE ├── README.md ├── index.js ├── lib ├── helper.js └── rest.js ├── package.json └── test ├── ValidLoadTest.js ├── cb_readyTest.js └── sdkfunctiontest.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .project 3 | node_modules -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | /client/ 2 | /node_modules/ 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "camelcase": false, 6 | "eqeqeq": false, 7 | "eqnull": true, 8 | "immed": true, 9 | "latedef": "nofunc", 10 | "newcap": true, 11 | "noarg": true, 12 | "quotmark": "single", 13 | "regexp": true, 14 | "undef": true, 15 | "unused": "vars", 16 | "trailing": true, 17 | "sub": true, 18 | "shadow": "inner", 19 | "funcscope": false, 20 | "maxlen": 160, 21 | "maxdepth": 7, 22 | "maxerr": 100, 23 | "maxparams": 6, 24 | "nonew": false 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ibm-blockchain-js - [Deprecated] 2 | This is a Node.js library for REST based interaction with [Hyperledger](https://github.com/hyperledger/fabric) chaincode. 3 | All `ibm-blockchain-js` documentation is on this page. 4 | 5 | *7/22/2016 Update! there is a new gRPC based SDK called [HFC](https://www.npmjs.com/package/hfc). I will continue to maintain this SDK for as long as REST exists.* 6 | 7 | *12/15/2016 Update! Hyperledger Fabric v0.7+ no longer has REST apis. Therefore you should use the HFC module instead of this one.* 8 | 9 | **4/28/2017 Update! There will only be bare minimum support for this SDK.** *For Hyperledger Fabric v1.0 it has been replaced by [Fabric Client](https://www.npmjs.com/package/fabric-client). For Hyperledger Fabric v0.6 it has been replaced by [HFC](https://www.npmjs.com/package/hfc)* 10 | 11 | Table Of Contents: 12 | 13 | 1. [v1.0.0 Migration!](#migrate) 14 | 1. [IBC-js Function Documentation](#ibcjs) 15 | 1. [Chaincode Functions](#ccfunc) 16 | 1. [Object Formats](#formats) 17 | 1. [Chaincode Summary File](#ccsf) 18 | 1. [FAQ](#faq) 19 | 20 | *** 21 | 22 | ## Installation 23 | 24 | ``` 25 | npm install ibm-blockchain-js 26 | ``` 27 | 28 | *** 29 | 30 | ## Usage Steps! 31 | (example code also provided below) 32 | 33 | 1. Require this module 34 | 1. Pass network + chaincode parameters to ibc.load(options, my_cb): 35 | 1. Receive chaincode object from callback to ibc.load(). ie: my_cb(e, chaincode) 36 | 1. You can now deploy your chaincode (if needed) with chaincode.deploy(func, args, null, cb) 37 | 1. Use dot notation on chaincode to call any of your chaincode functions ie: 38 | 39 | ```js 40 | // The functions below need to exist in your actual chaincode GoLang file(s) 41 | chaincode.query.read(['a'], cb); //will read variable "a" from current chaincode state 42 | chaincode.invoke.write(['a', 'test'], cb); //will write to variable "a" 43 | chaincode.invoke.remove(['a'], cb); //will delete variable "a" 44 | chaincode.invoke.init_marbles([ARGS], cb); //calls my custom chaincode function init_marbles() and passes it ARGS 45 | ``` 46 | 47 | ## Example 48 | 49 | ```js 50 | // Step 1 ================================== 51 | var Ibc1 = require('ibm-blockchain-js'); 52 | var ibc = new Ibc1(/*logger*/); //you can pass a logger such as winston here - optional 53 | var chaincode = {}; 54 | 55 | // ================================== 56 | // configure ibc-js sdk 57 | // ================================== 58 | var options = { 59 | network:{ 60 | peers: [{ 61 | "api_host": "xxx.xxx.xxx.xxx", 62 | "api_port": xxx, 63 | "api_port_tls": xxx, 64 | "id": "xxxxxx-xxxx-xxx-xxx-xxxxxxxxxxxx_vpx" 65 | }], 66 | users: [{ 67 | "enrollId": "user1", 68 | "enrollSecret": "xxxxxxxx" 69 | }], 70 | options: { //this is optional 71 | quiet: true, 72 | timeout: 60000 73 | } 74 | }, 75 | chaincode:{ 76 | zip_url: 'https://github.com/ibm-blockchain/marbles-chaincode/archive/master.zip', 77 | unzip_dir: 'marbles-chaincode-master/part2_v1.0.0', 78 | git_url: 'https://github.com/ibm-blockchain/marbles-chaincode/part2_v1.0.0' 79 | } 80 | }; 81 | 82 | // Step 2 ================================== 83 | ibc.load(options, cb_ready); 84 | 85 | // Step 3 ================================== 86 | function cb_ready(err, cc){ //response has chaincode functions 87 | app1.setup(ibc, cc); 88 | app2.setup(ibc, cc); 89 | 90 | // Step 4 ================================== 91 | if(cc.details.deployed_name === ""){ //decide if I need to deploy or not 92 | cc.deploy('init', ['99'], null, cb_deployed); 93 | } 94 | else{ 95 | console.log('chaincode summary file indicates chaincode has been previously deployed'); 96 | cb_deployed(); 97 | } 98 | } 99 | 100 | // Step 5 ================================== 101 | function cb_deployed(err){ 102 | console.log('sdk has deployed code and waited'); 103 | chaincode.query.read(['a']); 104 | } 105 | ``` 106 | 107 | 108 | *** 109 | ## Migrating from v0.0.x to v1.x.x 110 | The interface to your chaincode functions has changed in v1.0.0 from v0.0.13! 111 | It is only a minor syntax change that should make it more clear to newcomers. 112 | All invocation functions can now be found under `chaincode.invoke` and all query functions can be found under `chaincode.query`. 113 | 114 | Examples: 115 | 116 | **query changes** - name change 117 | ```js 118 | //old code 119 | chaincode.read('a'); 120 | 121 | //new code 122 | chaincode.query.read(['a']); 123 | ``` 124 | 125 | **invoke changes** - name change 126 | ```js 127 | //old code 128 | chaincode.init_marble(args); 129 | chaincode.remove(args); 130 | chaincode.write(name, value); 131 | 132 | //new code 133 | chaincode.invoke.init_marble(args); 134 | chaincode.invoke.remove(args); 135 | chaincode.invoke.write(args); 136 | ``` 137 | 138 | **deploy changes** - added options parameter 139 | ```js 140 | //old code 141 | chaincode.deploy('init', ['99'], './cc_summaries', cb_deployed); 142 | 143 | //new code 144 | chaincode.deploy('init', ['99'], {save_path: './cc_summaries', delay_ms: 60000}, cb_deployed); 145 | ``` 146 | 147 | **register changes** - added new parameter 148 | ```js 149 | //old code 150 | ibc.register(i, enrollId, enrollSecret, [callback]); 151 | 152 | //new code 153 | ibc.register(i, enrollId, enrollSecret, maxRetry, [callback]); 154 | 155 | ``` 156 | 157 | *** 158 | 159 | ## IBM-Blockchain-JS Documentation 160 | ### Usage 161 | 162 | Example with standard console logging: 163 | ```js 164 | var Ibc1 = require('ibm-blockchain-js'); 165 | var ibc = new Ibc1(); 166 | ``` 167 | 168 | Example with [Winston](https://www.npmjs.com/package/winston) logging: 169 | ```js 170 | var winston = require('winston'); 171 | var logger = new (winston.Logger)({ 172 | transports: [ 173 | new (winston.transports.Console)(), 174 | new (winston.transports.File)({ filename: 'somefile.log' }) 175 | ] 176 | }); 177 | var Ibc1 = require('ibm-blockchain-js'); 178 | var ibc = new Ibc1(logger); //you can pass a logger such as winston here - optional 179 | ``` 180 | 181 | ### ibc.load(options, [callback]) 182 | This is a function that wraps a typical startup using a standard Bluemix IBM Blockchain network. 183 | Take a look at how this function works, especially how it uses the register() function. 184 | If this is not applicable for your network (ie you have a custom IBM Blockchain network) you can easily create your own version of `ibc.load()` for your needs. 185 | It will run in order: 186 | 187 | 1. ibc.network(options.network.peers, options.network.options) *check out other options in [ibc.network()](#ibcnetwork)* 188 | 1. ibc.register(...) 189 | - It will register the first peer with the first enrollId, the 2nd peer against the 2nd enrollId and so on. 190 | - This function only runs if users are found in options.network.users. 191 | - Any errors in register will stop execution and run callback(err). 192 | 1. ibc.load_chaincode(options.chaincode, [callback]) 193 | 1. callback(err, cc) 194 | 195 | Options: 196 | - **maxRetry** = integer - number of times to retry `ibc.register()` before giving up 197 | - [more] - same options as the function [ibc.network()](#ibcnetwork), click for details 198 | 199 | Ex: 200 | 201 | ```js 202 | var options = { 203 | network:{ 204 | peers: [{ 205 | "api_host": "xxx.xxx.xxx.xxx", 206 | "api_port": xxx, 207 | "api_port_tls": xxx, 208 | "id": "xxxxxx-xxxx-xxx-xxx-xxxxxxxxxxxx_vpx" 209 | }], 210 | users: [{ 211 | "enrollId": "user1", 212 | "enrollSecret": "xxxxxxxx" 213 | }], 214 | options: { //this is optional, gets passed to ibc.network(peers, options); 215 | quiet: true, 216 | timeout: 60000, 217 | tls: true, 218 | maxRetry: 3 219 | } 220 | }, 221 | chaincode:{ 222 | zip_url: 'https://github.com/ibm-blockchain/marbles-chaincode/archive/master.zip', //http/https of a link to download zip 223 | unzip_dir: 'marbles-chaincode-master/part2_v1.0.0', //name/path to folder that contains the chaincode you want to deploy (path relative to unzipped root) 224 | git_url: 'https://github.com/ibm-blockchain/marbles-chaincode/part2_v1.0.0', //git https URL. should point to the desired chaincode repo AND directory 225 | 226 | deployed_name: null //[optional] this is the hashed name of a deployed chaincode. if you want to run with chaincode that is already deployed set it now, else it will be set when you deploy with the sdk 227 | } 228 | }; 229 | 230 | ibc.load(options, function(err, data){ 231 | //callback here 232 | }); 233 | ``` 234 | 235 | ### ibc.load_chaincode(options, [callback]) 236 | Load the chaincode you want to use. 237 | It will be downloaded and parsed. 238 | The callback will receive (e, obj) where `e` is the error format and `obj` is the chaincode object. 239 | "e" is null when there are no errors. 240 | The chaincode object will have dot notation to the functions in the your chaincode. 241 | 242 | Ex: 243 | 244 | ```js 245 | var options = { 246 | zip_url: 'https://github.com/ibm-blockchain/marbles-chaincode/archive/master.zip', //http/https of a link to download zip 247 | unzip_dir: 'marbles-chaincode-master/part2_v1.0.0', //name/path to folder that contains the chaincode you want to deploy (path relative to unzipped root) 248 | git_url: 'https://github.com/ibm-blockchain/marbles-chaincode/part2_v1.0.0', //git https URL. should point to the desired chaincode repo AND directory 249 | 250 | deployed_name: null //[optional] this is the hashed name of a deployed chaincode. if you want to run with chaincode that is already deployed set it now, else it will be set when you deploy with the sdk 251 | }; 252 | ibc.load_chaincode(options, cb_ready); 253 | ``` 254 | 255 | ### ibc.network(arrayPeers, [options]) 256 | Set the information about the peers in the network. 257 | This should be an array of peer objects. 258 | The options parameter is optional. 259 | Each field in `options` is also optional. 260 | 261 | Options: 262 | - **quiet** = boolean - when true will print out only minimal HTTP debug information. Defaults `true`. 263 | - **timeout** = integer - time in ms to wait for a http response. Defaults `60000`. 264 | - **tls** = boolean - when `false` will use HTTP instead of HTTPS. Defaults `true`. 265 | 266 | Ex: 267 | 268 | ```js 269 | var peers = [ 270 | { 271 | "api_host": "xxx.xxx.xxx.xxx", //ip or hostname of api for this peer 272 | "api_port": xxx, //port for api, non tls (integer) 273 | "api_port_tls": xxx, //port for api with tls. (integer) 274 | "id": "xxxxxx-xxxx-xxx-xxx-xxxxxxxxxxxx_vpx" //unique id of peer (string) 275 | } 276 | ] 277 | ibc.network(peers, {quiet: false, timeout: 120000}); //can pass config options 278 | ``` 279 | 280 | Note **only** the field names you see above (`api_host`, `api_port`, `api_port_tls`, `id`) are required. 281 | If you are using a Bluemix network you will see lots of other fields in the credentials JSON blob, but they are not needed. 282 | Its also fine to include the extra fields. 283 | You can ommit the field `api_port_tls` if your network does not support tls. 284 | Make sure the `options.tls` is `false`. 285 | 286 | ### ibc.save(path [callback]) 287 | Save the [Chaincode Summary File](#ccsf) to a path. 288 | 289 | Ex: 290 | 291 | ```js 292 | ibc.save('./'); 293 | ``` 294 | 295 | ### ibc.clear([callback]) 296 | Clear any loaded chaincode files including the downloaded chaincode repo, and [Chaincode Summary File](#ccsf). 297 | 298 | Ex: 299 | 300 | ```js 301 | ibc.clear(); 302 | ``` 303 | 304 | ### ibc.chain_stats([callback]) 305 | Get statistics on the network's chain. 306 | 307 | Ex: 308 | 309 | ```js 310 | ibc.chain_stats(function(e, stats){ 311 | console.log('got some stats', stats); 312 | }); 313 | ``` 314 | 315 | Example Chain Stats: 316 | 317 | ```js 318 | { 319 | "height": 10, 320 | "currentBlockHash": "n7uMlNMiOSUM8s02cslTRzZQQlVfm8wKT9FtL54o0ywy6BkvPMwSzN5R1tpquvqOwFFHyLSoW44n6rkFyvAsBw==", 321 | "previousBlockHash": "OESGPzacJO2Xc+5PB2zpmYVM8XlrwnEky0L2Ghok9oK1Lr/DWoxuBo2WwBca5zzJGq0fOeRQ7aOHgCjMupfL+Q==" 322 | } 323 | ``` 324 | 325 | ### ibc.block_stats(id, [callback]) 326 | Get statistics on a particular block in the chain. 327 | 328 | Ex: 329 | 330 | ```js 331 | ibc.block_stats(function(e, stats){ 332 | console.log('got some stats', stats); 333 | }); 334 | ``` 335 | 336 | Example Response: 337 | 338 | ```js 339 | { 340 | "transactions": [ 341 | { 342 | "type": 3, 343 | "chaincodeID": "EoABNWUzNGJmNWI1MWM1MWZiYzhlMWFmOThkYThhZDg0MGM2OWFjOWM5YTg4ODVlM2U0ZDBlNjNiM2I4MDc0ZWU2NjY2OWFjOTAzNTg4MzE1YTZjOGQ4ODY4M2Y1NjM0MThlMzMwNzQ3ZmVhZmU3ZWYyMGExY2Q1NGZmNzY4NWRhMTk=", 344 | "payload": "CrABCAESgwESgAE1ZTM0YmY1YjUxYzUxZmJjOGUxYWY5OGRhOGFkODQwYzY5YWM5YzlhODg4NWUzZTRkMGU2M2IzYjgwNzRlZTY2NjY5YWM5MDM1ODgzMTVhNmM4ZDg4NjgzZjU2MzQxOGUzMzA3NDdmZWFmZTdlZjIwYTFjZDU0ZmY3Njg1ZGExORomCgtpbml0X21hcmJsZRIHcng2YXRzcBIFZ3JlZW4SAjM1EgNCb2I=", 345 | "uuid": "b3da1d08-19b8-4d8c-a116-b46defb07a7c", 346 | "timestamp": { 347 | "seconds": 1453997627, 348 | "nanos": 856894462 349 | } 350 | } 351 | ], 352 | "stateHash": "81ci8IAOeDh0ZwFM6hE/b3SfXt4tnZFemib7sI95cOsNcYMmtRxBWRBA7qnjPOCGU6snBRsFVnAliZXUigQ03w==", 353 | "previousBlockHash": "tpjUh4sgbaUQFO8wm8S8nrm7yCrBa4rphIiujfaYAlEVfzI8IZ0mjYMf+GiOZ6CZRNWPmf+5bekmGIfr0H6zdw==", 354 | "nonHashData": { 355 | "localLedgerCommitTimestamp": { 356 | "seconds": 1453997627, 357 | "nanos": 868868790 358 | } 359 | } 360 | } 361 | ``` 362 | 363 | ### ibc.switchPeer(peerIndex) 364 | The SDK will default to use peer[0]. This function will switch the default peer to another index. 365 | 366 | Ex: 367 | 368 | ```js 369 | ibc.switchPeer(2); 370 | ``` 371 | 372 | ### ibc.register(peerIndex, enrollId, enrollsecret, maxRetry, [callback]) 373 | Only applicable on a network with security enabled. 374 | `register()` will register against peer[peerIndex] with the provided credentials. 375 | If successful, the peer will now use this `enrollId` to perform any http requests. 376 | - **peerIndex** = integer - position of peer in peers array (the one you fed ibc.networks()) you want to register against. 377 | - **enrollId** = string - name of secure context user. 378 | - **enrollSecret** = string - password/secret/api key of secure context user. 379 | - **maxRetry** = integer - number of times to retry this call before giving up. 380 | 381 | Ex: 382 | 383 | ```js 384 | ibc.register(3, 'user1', 'xxxxxx', 3, my_cb); 385 | ``` 386 | 387 | ### ibc.monitor_blockheight(callback) 388 | This will call your callback function whenever the block height has changed. 389 | ie. whenever a new block has been written to the chain. 390 | It will also pass you the same response as in `chain_stats()`. 391 | 392 | Ex: 393 | 394 | ```js 395 | ibc.monitor_blockheight(my_callback); 396 | function my_callback(e, chainstats){ 397 | console.log('got a new block!', chainstats); 398 | } 399 | ``` 400 | 401 | ### ibc.get_transaction(udid, [callback]) 402 | Get information about a particular transaction ID. 403 | 404 | Ex: 405 | 406 | ```js 407 | ibc.get_transaction('d30a1445-185f-4853-b4d6-ee7b4dfa5534', function(err, data){ 408 | console.log('found trans', err, data); 409 | }); 410 | ``` 411 | 412 | *** 413 | *** 414 | 415 | ##Chaincode Functions 416 | - Chaincode functions are dependent on actually be found inside your Go chaincode 417 | - My advice is to build your chaincode off of the Marble Application one. This way you get the basic CRUD functions below: 418 | 419 | ### chaincode.deploy(func, args, [options], [enrollId], [callback]) 420 | Deploy the chaincode. 421 | Call GoLang function named 'func' and feed it 'args'. 422 | Usually "args" is an array of strings. 423 | The `enrollId` parameter should be the desired secure context enrollId that has already been registered against the selected peer. 424 | If left `null` the SDK will use a known enrollId for the selected peer. (this is only relevant in a permissioned network) 425 | 426 | Options: 427 | - **save_path** = save the [Chaincode Summary File](#ccsf) to 'save_path'. 428 | - **delay_ms** = time in milliseconds to postpone the callback after deploy. Default is `40000` 429 | 430 | Ex: 431 | 432 | ```js 433 | chaincode.deploy('init', ['99'], {delay_ms: 60000}, cb_deployed); 434 | ``` 435 | 436 | ### chaincode.query.CUSTOM_FUNCTION_NAME(args, [enrollId], [callback]) 437 | Will invoke your Go function CUSTOM_FUNCTION_NAME and pass it `args`. 438 | Usually `args` is an array of strings. 439 | The `enrollId` parameter should be the desired secure context enrollId that has already been registered against the selected peer. 440 | If left `null` the SDK will use a known enrollId for the selected peer. (this is only relevant in a permissioned network) 441 | 442 | Ex: 443 | 444 | ```js 445 | chaincode.query.read(['abc'], function(err, data){ 446 | console.log('read abc:', data, err); 447 | }); 448 | ``` 449 | 450 | ### chaincode.invoke.CUSTOM_FUNCTION_NAME(args, [enrollId], [callback]) 451 | Will query your Go function CUSTOM_FUNCTION_NAME and pass it `args`. 452 | Usually `args` is an array of strings. 453 | The `enrollId` parameter should be the desired secure context enrollId that has already been registered against the selected peer. 454 | If left `null` the SDK will use a known enrollId for the selected peer. (this is only relevant in a permissioned network) 455 | 456 | Ex: 457 | 458 | ```js 459 | chaincode.invoke.init_marbles([args], function(err, data){ 460 | console.log('create marble response:', data, err); 461 | }); 462 | ``` 463 | 464 | ### chaincode.query.read(name, [enrollId], [callback]) *depreciated 4/1/2016* 465 | *This function is only here to help people transition from ibc v0.0.x to v1.x.x.* 466 | *You should create your own read() function in your chaincode which will overwrite this prebuilt one.* 467 | *This function will put the `name` argument into `args[0]` and set `function` to `query`.* 468 | *These are passed to the chaincode function `Query(stub *shim.ChaincodeStub, function string, args []string)`.* 469 | 470 | Read variable named name from chaincode state. 471 | This will call the `Query()` function in the Go chaincode. 472 | The `enrollId` parameter should be the desired secure context enrollId that has already been registered against the selected peer. 473 | If left `null` the SDK will use a known enrollId for the selected peer. (this is only relevant in a permissioned network) 474 | 475 | *** 476 | *** 477 | 478 | ## Formats 479 | ### Chaincode Object 480 | This is the main guy. 481 | It is returned in the callback to load_chaincode() and contains all your cc functions + some of the setup/input data. 482 | 483 | ```js 484 | chaincode = 485 | { 486 | query: { 487 | CUSTOM_FUNCTION_NAME1: function(args, cb){etc...}; //call chaincode function and pass it args 488 | CUSTOM_FUNCTION_NAME2: function(args, cb){etc...}; 489 | ^^ etc... 490 | } 491 | invoke: { 492 | CUSTOM_FUNCTION_NAME1: function(args, cb){etc...}; //call chaincode function and pass it args 493 | CUSTOM_FUNCTION_NAME2: function(args, cb){etc...}; 494 | ^^ etc... 495 | } 496 | deploy: function(func, args, path, cb), //deploy loaded chaincode 497 | details:{ //input options get stored here, sometimes its handy 498 | deployed_name: '', //hash of deployed chaincode 499 | func: { 500 | invoke: [], //array of function names found 501 | query: [] //array of function names found 502 | }, 503 | git_url: '', 504 | peers: [], //peer list provided in network() 505 | timestamp: 0, //utc unix timestamp in ms of parsing 506 | users: [], //users provided in load() 507 | unzip_dir: '', 508 | zip_url: '', 509 | } 510 | }; 511 | ``` 512 | 513 | ### Error Format 514 | 515 | ```js 516 | { 517 | name: "input error", //short name of error 518 | code: 400, //http error status code, integer 519 | details: {msg: "did not provide git_url"} //description of error, obj of unknown makeup 520 | }; 521 | ``` 522 | 523 | ### Chaincode Summary File 524 | This file is used internally when debugging. 525 | It is created in ibc.load_chaincode() and updated with chaincode.deploy(). 526 | A copy can be saved elsewhere with ibc.save(path). 527 | I found it handy in niche cases, but it will probably be unhelpful to most developers. 528 | 529 | ```js 530 | { 531 | "details": { 532 | "deployed_name": "f6c084c42b3bde90c03f214ac6e0426e3e594807901fb1464287f2c3a18ade717bc495298958287594f81bb0d0cfdd3b4346d438d3b587d4fc73cf78ae8f7dfe", 533 | "func": { 534 | "invoke": ["init", "delete", "write", "init_marble", "set_user", "open_trade", "perform_trade"], 535 | }, 536 | { 537 | "query": [] 538 | }, 539 | "git_url": 'https://github.com/ibm-blockchain/marbles-chaincode/part2_v1.0.0' 540 | "peers": [{ 541 | "name": "vp1-xxx.xxx.xxx.xxx", 542 | "api_host": "xxx.xxx.xxx.xxx", 543 | "api_port": xxx, 544 | "id": "xxxxx_vp1", 545 | "tls": false, 546 | "enrollId": "user1" 547 | }], 548 | "timestamp": 1459779181971, 549 | "users": [{ 550 | "enrollId": "xxx", 551 | "enrollSecret": "xxx" 552 | }], 553 | "unzip_dir": 'marbles-chaincode-master/part2_v1.0.0', 554 | "zip_url": 'https://github.com/ibm-blockchain/marbles-chaincode/archive/master.zip', 555 | "options": {} 556 | } 557 | } 558 | ``` 559 | 560 | # FAQ 561 | 562 | *Do you have any examples that use this?* 563 | 564 | - Yes! Head over to the [Marbles Node.js Demo](https://github.com/IBM-Blockchain/marbles) 565 | 566 | *How exactly do I write chaincode?* 567 | 568 | - We have a "hello world" like tutorial for chaincode over at [Learn Chaincode](https://github.com/IBM-Blockchain/learn-chaincode) 569 | 570 | *I'm getting error code 2 in my deploy response?* 571 | 572 | - Your chaincode has build issues and is not compiling. Manually build it in your local machine to get details. 573 | 574 | *I'm getting error code 1!* 575 | 576 | - The shim version your chaincode import has is not the same as the shim the peer is running. ie you are probably running 'Hyperledger' peer code and sending it chaincode with a shim pointing to "OBC-Peer". 577 | 578 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global __dirname */ 3 | /******************************************************************************* 4 | * Copyright (c) 2016 IBM Corp. 5 | * 6 | * All rights reserved. 7 | * 8 | *******************************************************************************/ 9 | 10 | //Load modules 11 | var fs = require('fs'); 12 | var path = require('path'); 13 | var http = require('http'); 14 | var https = require('https'); 15 | var async = require('async'); 16 | var rest = require(__dirname + '/lib/rest.js'); 17 | var helper = require(__dirname + '/lib/helper.js'); 18 | var AdmZip = require('adm-zip'); 19 | var logger = {log: console.log, error: console.error, debug: console.log, warn: console.log}; 20 | 21 | function ibc(log_outputs) { 22 | if(log_outputs && log_outputs.info) logger.log = log_outputs.info; //send normal logs here 23 | if(log_outputs && log_outputs.error) logger.error = log_outputs.error; //send error logs here 24 | if(log_outputs && log_outputs.warn) logger.warn = log_outputs.warn; //send warn logs here 25 | if(log_outputs && log_outputs.debug) logger.debug = log_outputs.debug; //send debug logs here 26 | } 27 | ibc.chaincode = { 28 | query: {}, 29 | invoke: {}, 30 | deploy: null, 31 | details:{ 32 | deployed_name: '', 33 | func: { 34 | invoke: [], 35 | query: [] 36 | }, 37 | git_url: '', 38 | options: {}, 39 | peers: [], 40 | timestamp: 0, 41 | users: [], 42 | unzip_dir: '', 43 | version: '', 44 | zip_url: '', 45 | } 46 | }; 47 | ibc.selectedPeer = 0; 48 | ibc.q = []; //array of unix timestamps, 1 for each unsettled action 49 | ibc.lastPoll = 0; //unix timestamp of the last time we polled 50 | ibc.lastBlock = 0; //last blockheight found 51 | var tempDirectory = path.join(__dirname, './temp'); // =./temp - temp directory name 52 | 53 | 54 | // ============================================================================================================================ 55 | // EXTERNAL - load() - wrapper on a standard startup flow. 56 | // 1. load network peer data 57 | // 2. register users with security (if present) 58 | // 3. load chaincode and parse 59 | // ============================================================================================================================ 60 | ibc.prototype.load = function(options, cb){ 61 | var errors = []; 62 | if(!options.network || !options.network.peers) errors.push('the option "network.peers" is required'); 63 | 64 | if(!options.chaincode || !options.chaincode.zip_url) errors.push('the option "chaincode.zip_url" is required'); 65 | if(!options.chaincode || !options.chaincode.unzip_dir) errors.push('the option "chaincode.unzip_dir" is required'); 66 | if(!options.chaincode || !options.chaincode.git_url) errors.push('the option "chaincode.git_url" is required'); 67 | if(errors.length > 0){ //check for input errors 68 | logger.error('! [ibc-js] Input Error - ibc.load()', errors); 69 | if(cb) cb(helper.eFmt('load() input error', 400, errors)); 70 | return; //get out of dodge 71 | } 72 | 73 | ibc.chaincode = { //empty it all 74 | query: { 75 | read: read 76 | }, 77 | invoke: {}, 78 | deploy: null, 79 | details:{ 80 | deployed_name: '', 81 | func: { 82 | invoke: [], 83 | query: [] 84 | }, 85 | git_url: '', 86 | options: options.network.options, 87 | peers: [], 88 | timestamp: 0, 89 | users: [], 90 | unzip_dir: '', 91 | version: '', 92 | zip_url: '', 93 | } 94 | }; 95 | 96 | // Step 1 97 | ibc.prototype.network(options.network.peers, options.network.options); 98 | 99 | // Step 2 - optional - only for secure networks 100 | if(options.network.users && options.network.users.length > 0){ 101 | ibc.chaincode.details.users = options.network.users; 102 | var arr = []; 103 | for(var i in ibc.chaincode.details.peers){ 104 | arr.push(i); //build the list of indexes 105 | } 106 | async.each(arr, function(i, a_cb) { 107 | if(options.network.users[i]){ //make sure we still have a enrollId for this network 108 | var maxRetry = 2; 109 | if(options.network.options && options.network.options.maxRetry) maxRetry = options.network.options.maxRetry; 110 | ibc.prototype.register(i, options.network.users[i].enrollId, options.network.users[i].enrollSecret, maxRetry, a_cb); 111 | } 112 | else a_cb(); 113 | }, function(err, data){ 114 | if(err && cb) return cb(err); //error already formated 115 | else load_cc(); 116 | }); 117 | } 118 | else{ 119 | ibc.chaincode.details.users = []; 120 | logger.log('[ibc-js] No membership users found, assuming this is a network w/o membership'); 121 | load_cc(); 122 | } 123 | 124 | // Step 3 125 | function load_cc(){ 126 | ibc.prototype.load_chaincode(options.chaincode, cb); //download/parse and load chaincode 127 | } 128 | }; 129 | 130 | // ============================================================================================================================ 131 | // EXTERNAL - load_chaincode() - load the chaincode and parssssssssse 132 | // 0. Load the github or zip 133 | // 1. Unzip & scan directory for files 134 | // 2. Iter over go files 135 | // 2a. Find what shim version 136 | // 2b. Find the boundaries for Invoke() in the cc 137 | // 2c. Grab function names that need to be exported 138 | // 2d. Create JS invoke functions for golang functions 139 | // 2e. Find the boundaries for Query() in the cc 140 | // 2f. Grab function names that need to be exported 141 | // 2g. Create JS query functions for golang functions 142 | // 2h. Find the boundaries for Init() in the cc 143 | // 2i. Record function names that need to be exported 144 | // 3. Call callback() 145 | // ============================================================================================================================ 146 | ibc.prototype.load_chaincode = function(options, cb) { 147 | var errors = []; 148 | if(!options.zip_url) errors.push('the option "zip_url" is required'); 149 | if(!options.unzip_dir) errors.push('the option "unzip_dir" is required'); 150 | if(!options.git_url) errors.push('the option "git_url" is required'); 151 | if(errors.length > 0){ //check for input errors 152 | logger.error('! [ibc-js] Input Error - ibc.load_chaincode()', errors); 153 | if(cb) cb(helper.eFmt('load_chaincode() input error', 400, errors)); 154 | return; //get out of dodge 155 | } 156 | 157 | var go_funcs = [], cc_suspects = [], cc_invocations = [], cc_queries = [], cc_inits = []; 158 | var found_query = false, found_invoke = false; 159 | var zip_dest = path.join(tempDirectory, '/file.zip'); // =./temp/file.zip 160 | var unzip_dest = path.join(tempDirectory, '/unzip'); // =./temp/unzip 161 | var unzip_cc_dest = path.join(unzip_dest, '/', options.unzip_dir); // =./temp/unzip/DIRECTORY 162 | ibc.chaincode.details.zip_url = options.zip_url; 163 | ibc.chaincode.details.unzip_dir = options.unzip_dir; 164 | ibc.chaincode.details.git_url = options.git_url; 165 | ibc.chaincode.details.deployed_name = options.deployed_name; 166 | 167 | if(!options.deployed_name || options.deployed_name === ''){ //lets clear and re-download 168 | ibc.prototype.clear(cb_ready); 169 | } 170 | else{ 171 | cb_ready(); 172 | } 173 | 174 | // check if we already have the chaincode in the local filesystem, else download it 175 | function cb_ready(){ 176 | try{fs.mkdirSync(tempDirectory);} 177 | catch(e){ } 178 | fs.access(unzip_cc_dest, cb_file_exists); //check if files exist yet 179 | function cb_file_exists(e){ 180 | if(e != null){ 181 | download_it(options.zip_url); //nope, go download it 182 | } 183 | else{ 184 | logger.log('[ibc-js] Found chaincode in local file system'); 185 | fs.readdir(unzip_cc_dest, cb_got_names); //yeppers, go use it 186 | } 187 | } 188 | } 189 | 190 | // Step 0. 191 | function download_it(download_url){ 192 | logger.log('[ibc-js] Downloading zip'); 193 | var file = fs.createWriteStream(zip_dest); 194 | var handleResponse = function(response) { //download a .zip of the repo 195 | response.pipe(file); 196 | file.on('finish', function() { 197 | if(response.headers.status === '302 Found'){ 198 | logger.log('redirect...', response.headers.location); 199 | file.close(); 200 | download_it(response.headers.location); 201 | } 202 | else{ 203 | file.close(cb_downloaded); //close() is async 204 | } 205 | }); 206 | }; 207 | var handleError = function(err) { 208 | logger.error('! [ibc-js] Download error'); 209 | fs.unlink(zip_dest); //delete the file async 210 | if (cb) cb(helper.eFmt('doad_chaincode() download error', 500, err.message), ibc.chaincode); 211 | }; 212 | 213 | var protocol = download_url.split('://')[0]; 214 | if(protocol === 'https') { //choose http or https 215 | https.get(download_url, handleResponse).on('error', handleError); 216 | } 217 | else{ 218 | http.get(download_url, handleResponse).on('error', handleError); 219 | } 220 | } 221 | 222 | // Step 1. 223 | function cb_downloaded(){ 224 | logger.log('[ibc-js] Unzipping zip'); 225 | try{ 226 | var zip = new AdmZip(zip_dest); //unzip the zip we downloaded 227 | zip.extractAllTo(unzip_dest, /*overwrite*/true); 228 | } 229 | catch (err){ 230 | return cb(helper.eFmt('download repo error', 400, err), null); 231 | } 232 | logger.log('[ibc-js] Unzip done'); 233 | fs.readdir(unzip_cc_dest, cb_got_names); 234 | fs.unlink(zip_dest, function(err) {}); //remove zip file, never used again 235 | } 236 | 237 | // Step 2. 238 | function cb_got_names(err, obj){ 239 | logger.log('[ibc-js] Scanning files', obj); 240 | var foundGo = false; 241 | if(err != null) logger.log('! [ibc-js] fs readdir Error', err); 242 | else{ 243 | for(var i in obj){ 244 | if(obj[i].indexOf('.go') >= 0){ //look for GoLang files 245 | if(!found_invoke || !found_query){ 246 | foundGo = true; 247 | var file = fs.readFileSync(path.join(unzip_cc_dest, obj[i]), 'utf8'); 248 | 249 | // Step 2a. 250 | ibc.chaincode.details.version = find_shim(file); 251 | if(ibc.chaincode.details.version !== ''){ //we can't search for functions until we identify the shim version 252 | parse_for_invoke(obj[i], file); 253 | parse_for_query(obj[i], file); 254 | parse_for_init(obj[i], file); 255 | } 256 | } 257 | } 258 | } 259 | } 260 | 261 | // done - look for errors/warnings 262 | var msg = ''; 263 | if(!foundGo){ //error no go files 264 | msg = 'did not find any *.go files, cannot continue'; 265 | logger.error('! [ibc-js] Error - ', msg); 266 | if(cb) return cb(helper.eFmt('load_chaincode() no chaincode', 400, msg), null); 267 | } 268 | else{ 269 | 270 | if(!found_invoke){ //warning no run/invoke functions 271 | logger.warn('! [ibc-js] Warning - did not find any invoke functions in chaincode\'s "Invoke()", building a generic "invoke"'); 272 | build_invoke_func('invoke'); //this will make chaincode.invoke.invokce(args) 273 | } 274 | 275 | if(!found_query){ //warning no query functions 276 | logger.warn('! [ibc-js] Warning - did not find any query functions in chaincode\'s "Query()", building a generic "query"'); 277 | build_query_func('query'); //this will make chaincode.query.query(args) 278 | } 279 | 280 | // Step 3. success! 281 | logger.log('[ibc-js] load_chaincode() finished'); 282 | ibc.chaincode.details.timestamp = Date.now(); 283 | ibc.chaincode.deploy = deploy; 284 | if(cb) return cb(null, ibc.chaincode); //all done, send it to callback 285 | } 286 | } 287 | 288 | //regex to find the shim version for this chaincode 289 | function find_shim(file){ 290 | var ret = ''; 291 | if(file == null) logger.error('! [ibc-js] fs readfile Error'); 292 | else{ 293 | logger.log('[ibc-js] Parsing file for shim version'); 294 | 295 | var shim_regex = /github.com\/\S+\/shim/g; //find chaincode's shim version 296 | var result = file.match(shim_regex); 297 | if(result[0]){ 298 | logger.log('[ibc-js] Found shim version:', result[0]); 299 | ret = result[0]; 300 | } 301 | } 302 | return ret; 303 | } 304 | 305 | //look for Invokes 306 | function parse_for_invoke(name, str){ 307 | if(str == null) logger.error('! [ibc-js] fs readfile Error'); 308 | else{ 309 | logger.log('[ibc-js] Parsing file for invoke functions -', name); 310 | 311 | // Step 2a. 312 | var go_func_regex = /func\s+\(\w+\s+\*SimpleChaincode\)\s+(\w+)/g; //find chaincode's go lang functions 313 | var result; 314 | while ( (result = go_func_regex.exec(str)) ) { 315 | go_funcs.push({name: result[1], pos: result.index}); 316 | } 317 | var i_start = 0; 318 | var i_stop = 0; 319 | var invokeFunctionName = 'Run'; //use Run for obc peer adn Invoke for hyperledger 320 | if(ibc.chaincode.details.version.indexOf('hyperledger/fabric/core/chaincode/shim') >= 0) invokeFunctionName = 'Invoke'; 321 | 322 | for(var i in go_funcs){ 323 | if(go_funcs[i].name === invokeFunctionName){ 324 | i_start = go_funcs[i].pos; //find start and stop positions around the "Invoke()" function 325 | if(go_funcs[Number(i) + 1] == null) i_stop = i_start * 2; //invoke is the last function.. so uhhhh just make up a high number 326 | else i_stop = go_funcs[Number(i) + 1].pos; 327 | break; 328 | } 329 | } 330 | 331 | if(i_start > 0 && i_stop > 0){ 332 | // Step 2c. 333 | var regex = /function\s+.=\s+["'](\w+)["']/g; //find the exposed chaincode functions in "Invoke()"" 334 | var result2; 335 | while ( (result2 = regex.exec(str)) ) { 336 | cc_suspects.push({name: result2[1], index: result2.index}); //store this for future parsing like query & init 337 | if(result2.index > i_start && result2.index < i_stop){ //make sure its inside Invoke() 338 | cc_invocations.push(result2[1]); //build a list of function names 339 | } 340 | } 341 | 342 | if(cc_invocations.length > 0){ 343 | found_invoke = true; 344 | 345 | // Step 2d. 346 | ibc.chaincode.details.func.invoke = []; 347 | for(i in cc_invocations){ //build the rest call for each function 348 | build_invoke_func(cc_invocations[i]); 349 | } 350 | } 351 | } 352 | } 353 | } 354 | 355 | //look for Queries 356 | function parse_for_query(name, str){ 357 | if(str == null) logger.error('! [ibc-js] fs readfile Error'); 358 | else{ 359 | logger.log('[ibc-js] Parsing file for query functions -', name); 360 | 361 | // Step 2e. 362 | var q_start = 0; 363 | var q_stop = 0; 364 | for(var i in go_funcs){ 365 | if(go_funcs[i].name === 'Query'){ 366 | q_start = go_funcs[i].pos; //find start and stop positions around the "Query()" function 367 | if(go_funcs[Number(i) + 1] == null) q_stop = q_start * 2; //query is the last function.. so uhhhh just make up a high number 368 | else q_stop = go_funcs[Number(i) + 1].pos; 369 | break; 370 | } 371 | } 372 | 373 | if(q_start > 0 && q_stop > 0){ 374 | // Step 2f. 375 | for(i in cc_suspects){ 376 | if(cc_suspects[i].index > q_start && cc_suspects[i].index < q_stop){//make sure its inside Query() 377 | cc_queries.push(cc_suspects[i].name); //build a list of function names 378 | } 379 | } 380 | 381 | if(cc_queries.length > 0){ 382 | found_query = true; 383 | 384 | // Step 2g. 385 | ibc.chaincode.details.func.query = []; 386 | for(i in cc_queries){ //build the rest call for each function 387 | build_query_func(cc_queries[i]); 388 | } 389 | } 390 | } 391 | } 392 | } 393 | 394 | //look for Inits 395 | function parse_for_init(name, str){ 396 | if(str == null) logger.error('! [ibc-js] fs readfile Error'); 397 | else{ 398 | //logger.log('[ibc-js] Parsing file for init functions -', name); 399 | 400 | // Step 2h. 401 | var q_start = 0; 402 | var q_stop = 0; 403 | for(var i in go_funcs){ 404 | if(go_funcs[i].name === 'Init'){ 405 | q_start = go_funcs[i].pos; //find start and stop positions around the "Init()" function 406 | if(go_funcs[Number(i) + 1] == null) q_stop = q_start * 2; //init is the last function.. so uhhhh just make up a high number 407 | else q_stop = go_funcs[Number(i) + 1].pos; 408 | break; 409 | } 410 | } 411 | 412 | if(q_start > 0 && q_stop > 0){ 413 | for(i in cc_suspects){ 414 | if(cc_suspects[i].index > q_start && cc_suspects[i].index < q_stop){//make sure its inside Init() 415 | cc_inits.push(cc_suspects[i].name); //build a list of function names 416 | } 417 | } 418 | 419 | if(cc_inits.length > 0){ 420 | 421 | // Step 2i. 422 | ibc.chaincode.details.func.init = []; 423 | for(i in cc_inits){ //no rest call to build, just remember it in 'details' 424 | ibc.chaincode.details.func.init.push(name); 425 | } 426 | } 427 | } 428 | } 429 | } 430 | }; 431 | 432 | // ============================================================================================================================ 433 | // EXTERNAL - network() - setup network configuration to hit a rest peer 434 | // ============================================================================================================================ 435 | ibc.prototype.network = function(arrayPeers, options){ 436 | var errors = []; 437 | ibc.chaincode.details.options = {quiet: true, timeout: 60000, tls: true}; //defaults 438 | 439 | if(!arrayPeers || arrayPeers.constructor !== Array) errors.push('network input arg should be array of peer objects'); 440 | 441 | if(options){ 442 | if(options.quiet === true || options.quiet === false) ibc.chaincode.details.options.quiet = options.quiet; //optional fields 443 | if(!isNaN(options.timeout)) ibc.chaincode.details.options.timeout = Number(options.timeout); 444 | if(options.tls === true || options.tls === false) ibc.chaincode.details.options.tls = options.tls; 445 | } 446 | 447 | for(var i in arrayPeers){ //check for errors in peers input obj 448 | if(!arrayPeers[i].id) errors.push('peer ' + i + ' is missing the field id'); 449 | if(!arrayPeers[i].api_host) errors.push('peer ' + i + ' is missing the field api_host'); 450 | if(options && options.tls === false){ 451 | if(!arrayPeers[i].api_port) errors.push('peer ' + i + ' is missing the field api_port'); 452 | } 453 | else{ 454 | if(!arrayPeers[i].api_port_tls) errors.push('peer ' + i + ' is missing the field api_port_tls'); 455 | } 456 | } 457 | 458 | if(errors.length > 0){ //check for input errors 459 | logger.error('! [ibc-js] Input Error - ibc.network()', errors); 460 | } 461 | else{ 462 | ibc.chaincode.details.peers = []; 463 | for(i in arrayPeers){ 464 | var pos = arrayPeers[i].id.indexOf('_') + 1; 465 | var temp = { 466 | name: arrayPeers[i].id.substring(pos) + '-' + arrayPeers[i].id.substring(0, 12) + '...:' + arrayPeers[i].api_port_tls, 467 | api_host: arrayPeers[i].api_host, 468 | api_port: arrayPeers[i].api_port, 469 | api_port_tls: arrayPeers[i].api_port_tls, 470 | id: arrayPeers[i].id, 471 | tls: ibc.chaincode.details.options.tls 472 | }; 473 | if(options && options.tls === false){ //if not tls rebuild a few things 474 | temp.name = arrayPeers[i].id.substring(pos) + '-' + arrayPeers[i].id.substring(0, 12) + '...:' + arrayPeers[i].api_port; 475 | } 476 | 477 | logger.log('[ibc-js] Peer: ', temp.name); //print the friendly name 478 | ibc.chaincode.details.peers.push(temp); 479 | } 480 | 481 | rest.init({ //load default values for rest call to peer 482 | host: ibc.chaincode.details.peers[0].api_host, 483 | port: pick_port(0), 484 | headers: { 485 | 'Content-Type': 'application/json', 486 | 'Accept': 'application/json', 487 | }, 488 | ssl: ibc.chaincode.details.peers[0].tls, 489 | timeout: ibc.chaincode.details.options.timeout, 490 | quiet: ibc.chaincode.details.options.quiet 491 | }, logger); 492 | } 493 | }; 494 | 495 | //pick tls or non-tls port based on the tls setting 496 | function pick_port(pos){ 497 | var port = ibc.chaincode.details.peers[pos].api_port_tls; 498 | if(ibc.chaincode.details.peers[pos].tls === false) port = ibc.chaincode.details.peers[pos].api_port; 499 | return port; 500 | } 501 | 502 | 503 | // ============================================================================================================================ 504 | // EXTERNAL - switchPeer() - switch the default peer to hit 505 | // ============================================================================================================================ 506 | ibc.prototype.switchPeer = function(index) { 507 | if(ibc.chaincode.details.peers[index]) { 508 | rest.init({ //load default values for rest call to peer 509 | host: ibc.chaincode.details.peers[index].api_host, 510 | port: pick_port(index), 511 | headers: { 512 | 'Content-Type': 'application/json', 513 | 'Accept': 'application/json', 514 | }, 515 | ssl: ibc.chaincode.details.peers[index].tls, 516 | timeout: ibc.chaincode.details.options.timeout, 517 | quiet: ibc.chaincode.details.options.quiet 518 | }); 519 | ibc.selectedPeer = index; 520 | return true; 521 | } else { 522 | return false; 523 | } 524 | }; 525 | 526 | // ============================================================================================================================ 527 | // EXTERNAL - save() - write chaincode details to a json file 528 | // ============================================================================================================================ 529 | ibc.prototype.save = function(dir, cb){ 530 | var errors = []; 531 | if(!dir) errors.push('the option "dir" is required'); 532 | if(errors.length > 0){ //check for input errors 533 | logger.error('[ibc-js] Input Error - ibc.save()', errors); 534 | if(cb) cb(helper.eFmt('save() input error', 400, errors)); 535 | } 536 | else{ 537 | var fn = 'chaincode.json'; //default name 538 | if(ibc.chaincode.details.deployed_name) fn = ibc.chaincode.details.deployed_name + '.json'; 539 | var dest = path.join(dir, fn); 540 | fs.writeFile(dest, JSON.stringify({details: ibc.chaincode.details}), function(e){ 541 | if(e != null){ 542 | logger.warn('[ibc-js] ibc.save() warning', e); 543 | if(cb) cb(helper.eFmt('save() fs write error', 500, e), null); 544 | } 545 | else { 546 | if(cb) cb(null, null); 547 | } 548 | }); 549 | } 550 | }; 551 | 552 | // ============================================================================================================================ 553 | // EXTERNAL - clear() - clear the temp directory 554 | // ============================================================================================================================ 555 | ibc.prototype.clear = function(cb){ 556 | logger.log('[ibc-js] removing temp dir'); 557 | helper.removeThing(tempDirectory, cb); //remove everything in this directory 558 | }; 559 | 560 | //============================================================================================================================ 561 | // EXTERNAL chain_stats() - get blockchain stats 562 | //============================================================================================================================ 563 | ibc.prototype.chain_stats = function(cb){ 564 | var options = {path: '/chain'}; //very simple API, get chainstats! 565 | 566 | rest.get(options, null, function(statusCode, data){ 567 | if(statusCode != null){ 568 | logger.error('[ibc-js] Chain Stats - failure:', statusCode, data); 569 | if(cb) cb(helper.eFmt('chain_stats() error', statusCode, data), null); 570 | } 571 | else{ 572 | logger.log('[ibc-js] Chain Stats - success'); 573 | if(cb) cb(null, data); 574 | } 575 | }); 576 | }; 577 | 578 | //============================================================================================================================ 579 | // EXTERNAL block_stats() - get block meta data 580 | //============================================================================================================================ 581 | ibc.prototype.block_stats = function(id, cb){ 582 | var options = {path: '/chain/blocks/' + id}; //i think block IDs start at 0, height starts at 1, fyi 583 | 584 | rest.get(options, null, function(statusCode, data){ 585 | if(statusCode != null){ 586 | logger.error('[ibc-js] Block Stats ', id , '- failure:', statusCode); 587 | if(cb) cb(helper.eFmt('block_stats() error', statusCode, data), null); 588 | } 589 | else{ 590 | logger.log('[ibc-js] Block Stats ', id , '- success'); 591 | if(cb) cb(null, data); 592 | } 593 | }); 594 | }; 595 | 596 | //============================================================================================================================ 597 | //read() - read generic variable from chaincode state - ! [legacy. do not use it anymore 4/1/2016] 598 | //============================================================================================================================ 599 | function read(args, enrollId, cb){ 600 | if(typeof enrollId === 'function'){ //if cb is in 2nd param use known enrollId 601 | cb = enrollId; 602 | enrollId = ibc.chaincode.details.peers[ibc.selectedPeer].enrollId; 603 | } 604 | if(enrollId == null) { //if enrollId not provided, use known valid one 605 | enrollId = ibc.chaincode.details.peers[ibc.selectedPeer].enrollId; 606 | } 607 | 608 | var options = {path: '/chaincode'}; 609 | var body = { 610 | jsonrpc: '2.0', 611 | method: 'query', 612 | params: { 613 | type: 1, 614 | chaincodeID:{ 615 | name: ibc.chaincode.details.deployed_name 616 | }, 617 | ctorMsg: { 618 | function: 'query', 619 | args: args 620 | }, 621 | secureContext: enrollId 622 | }, 623 | id: Date.now() 624 | }; 625 | 626 | rest.post(options, null, body, function(statusCode, data){ 627 | if(statusCode != null){ 628 | logger.error('[ibc-js] (Read) - failure:', statusCode); 629 | if(cb) cb(helper.eFmt('read() error', statusCode, data), null); 630 | } 631 | else{ 632 | logger.log('[ibc-js] (Read) - success:', data); 633 | if(cb){ 634 | if(data.error) cb(helper.eFmt('query() resp error', 400, data.error), null); 635 | else if(data.result) cb(null, data.result.message); 636 | else cb(null, data.OK); 637 | } 638 | } 639 | }); 640 | } 641 | 642 | //============================================================================================================================ 643 | // EXTERNAL - register() - register a enrollId with a peer (only for a blockchain network with membership) 644 | //============================================================================================================================ 645 | ibc.prototype.register = function(index, enrollId, enrollSecret, maxRetry, cb) { 646 | register(index, enrollId, enrollSecret, maxRetry, 1, cb); 647 | }; 648 | 649 | function register(index, enrollId, enrollSecret, maxRetry, attempt, cb){ 650 | logger.log('[ibc-js] Registering ', ibc.chaincode.details.peers[index].name, ' w/enrollId - ' + enrollId); 651 | var options = { 652 | path: '/registrar', 653 | host: ibc.chaincode.details.peers[index].api_host, 654 | port: pick_port(index), 655 | ssl: ibc.chaincode.details.peers[index].tls 656 | }; 657 | 658 | var body = { 659 | enrollId: enrollId, 660 | enrollSecret: enrollSecret 661 | }; 662 | rest.post(options, null, body, function(statusCode, data){ 663 | if(statusCode != null){ 664 | logger.error('[ibc-js] Register - failure x' + attempt + ' :', enrollId, statusCode); 665 | if(attempt <= maxRetry){ //lets try again after a short delay, maybe the peer is still starting 666 | logger.log('[ibc-js] \tgoing to try to register again in 30 secs'); 667 | setTimeout(function(){register(index, enrollId, enrollSecret, maxRetry, ++attempt, cb);}, 30000); 668 | } 669 | else{ 670 | if(cb) cb(helper.eFmt('register() error', statusCode, data), null); //give up 671 | } 672 | } 673 | else { 674 | logger.log('[ibc-js] Registration success x' + attempt + ' :', enrollId); 675 | ibc.chaincode.details.peers[index].enrollId = enrollId; //remember a valid enrollId for this peer 676 | if(cb) cb(null, data); 677 | } 678 | }); 679 | } 680 | 681 | //============================================================================================================================ 682 | // EXTERNAL - unregister() - unregister a enrollId from a peer (only for a blockchain network with membership), enrollId can no longer make transactions 683 | //============================================================================================================================ 684 | ibc.prototype.unregister = function(index, enrollId, cb) { 685 | logger.log('[ibc-js] Unregistering ', ibc.chaincode.details.peers[index].name, ' w/enrollId - ' + enrollId); 686 | var options = { 687 | path: '/registrar/' + enrollId, 688 | host: ibc.chaincode.details.peers[index].api_host, 689 | port: pick_port(index), 690 | ssl: ibc.chaincode.details.peers[index].tls 691 | }; 692 | 693 | rest.delete(options, null, null, function(statusCode, data){ 694 | if(statusCode != null){ 695 | logger.log('[ibc-js] Unregistering - failure:', enrollId, statusCode); 696 | if(cb) cb(helper.eFmt('unregister() error', statusCode, data), null); 697 | } 698 | else { 699 | logger.log('[ibc-js] Unregistering success:', enrollId); 700 | ibc.chaincode.details.peers[index].enrollId = null; //unremember a valid enrollId for this peer 701 | if(cb) cb(null, data); 702 | } 703 | }); 704 | }; 705 | 706 | //============================================================================================================================ 707 | // EXTERNAL - check_register() - check if a enrollId is registered or not with a peer 708 | //============================================================================================================================ 709 | ibc.prototype.check_register = function(index, enrollId, cb) { 710 | logger.log('[ibc-js] Checking ', ibc.chaincode.details.peers[index].name, ' w/enrollId - ' + enrollId); 711 | var options = { 712 | path: '/registrar/' + enrollId, 713 | host: ibc.chaincode.details.peers[index].api_host, 714 | port: pick_port(index), 715 | ssl: ibc.chaincode.details.peers[index].tls 716 | }; 717 | 718 | rest.get(options, null, function(statusCode, data){ 719 | if(statusCode != null){ 720 | logger.error('[ibc-js] Check Register - failure:', enrollId, statusCode); 721 | if(cb) cb(helper.eFmt('check_register() error', statusCode, data), null); 722 | } 723 | else{ 724 | logger.log('[ibc-js] Check Register success:', enrollId); 725 | if(cb) cb(null, data); 726 | } 727 | }); 728 | }; 729 | 730 | //============================================================================================================================ 731 | //deploy() - deploy chaincode and call a cc function 732 | //============================================================================================================================ 733 | function deploy(func, args, deploy_options, enrollId, cb){ 734 | if(typeof enrollId === 'function'){ //if cb is in 2nd param use known enrollId 735 | cb = enrollId; 736 | enrollId = ibc.chaincode.details.peers[ibc.selectedPeer].enrollId; 737 | } 738 | if(enrollId == null) { //if enrollId not provided, use known valid one 739 | enrollId = ibc.chaincode.details.peers[ibc.selectedPeer].enrollId; 740 | } 741 | 742 | logger.log('[ibc-js] Deploy Chaincode - Starting'); 743 | logger.log('[ibc-js] \tfunction:', func, ', arg:', args); 744 | logger.log('\n\n\t Waiting...'); //this can take awhile 745 | 746 | var options = {}, body = {}; 747 | options = {path: '/chaincode'}; 748 | body = { 749 | jsonrpc: '2.0', 750 | method: 'deploy', 751 | params: { 752 | type: 1, 753 | chaincodeID:{ 754 | path: ibc.chaincode.details.git_url 755 | }, 756 | ctorMsg: { 757 | function: func, 758 | args: args 759 | }, 760 | secureContext: enrollId 761 | }, 762 | id: Date.now() 763 | }; 764 | 765 | rest.post(options, null, body, function(statusCode, data){ 766 | // ---- Failure ---- /// 767 | if(statusCode != null){ 768 | logger.error('[ibc-js] deploy - failure:', statusCode); 769 | if(cb) cb(helper.eFmt('deploy() error', statusCode, data), null); 770 | } 771 | 772 | // ---- Success ---- // 773 | else{ 774 | if(data.result && ibc.chaincode.details.version.indexOf('hyperledger/fabric/core/chaincode/shim') >= 0){//hyperledger response 775 | ibc.chaincode.details.deployed_name = data.result.message; 776 | } 777 | else ibc.chaincode.details.deployed_name = data.message; //obc-peer response 778 | 779 | if(!ibc.chaincode.details.deployed_name || ibc.chaincode.details.deployed_name.length < 32){ 780 | ibc.chaincode.details.deployed_name = ''; //doesnt look right, let code below catch error 781 | } 782 | 783 | if(ibc.chaincode.details.deployed_name === ''){ 784 | logger.error('\n\n\t deploy resp error - there is no chaincode hash name in response:', data); 785 | if(cb) cb(helper.eFmt('deploy() error no cc name', 502, data), null); 786 | } 787 | else{ 788 | ibc.prototype.save(tempDirectory); //save it to known place so we remember the cc name 789 | if(deploy_options && deploy_options.save_path != null) { //save it to custom route 790 | ibc.prototype.save(deploy_options.save_path); 791 | } 792 | 793 | if(cb){ 794 | var wait_ms = 45000; //default wait after deploy, peer may still be starting 795 | if(deploy_options && deploy_options.delay_ms && Number(deploy_options.delay_ms)) wait_ms = deploy_options.delay_ms; 796 | logger.log('\n\n\t deploy success [waiting another', (wait_ms / 1000) ,'seconds]'); 797 | logger.log('\t', ibc.chaincode.details.deployed_name, '\n'); 798 | 799 | setTimeout(function(){ 800 | logger.log('[ibc-js] Deploy Chaincode - Complete'); 801 | cb(null, data); 802 | }, wait_ms); //wait extra long, not always ready yet 803 | } 804 | } 805 | } 806 | }); 807 | } 808 | 809 | //============================================================================================================================ 810 | //heart_beat() - interval function to poll against blockchain height (has fast and slow mode) 811 | //============================================================================================================================ 812 | var slow_mode = 10000; 813 | var fast_mode = 500; 814 | function heart_beat(){ 815 | if(ibc.lastPoll + slow_mode < Date.now()){ //slow mode poll 816 | //logger.log('[ibc-js] Its been awhile, time to poll'); 817 | ibc.lastPoll = Date.now(); 818 | ibc.prototype.chain_stats(cb_got_stats); 819 | } 820 | else{ 821 | for(var i in ibc.q){ 822 | var elasped = Date.now() - ibc.q[i]; 823 | if(elasped <= 3000){ //fresh unresolved action, fast mode! 824 | logger.log('[ibc-js] Unresolved action, must poll'); 825 | ibc.lastPoll = Date.now(); 826 | ibc.prototype.chain_stats(cb_got_stats); 827 | } 828 | else{ 829 | //logger.log('[ibc-js] Expired, removing'); 830 | ibc.q.pop(); //expired action, remove it 831 | } 832 | } 833 | } 834 | } 835 | 836 | function cb_got_stats(e, stats){ 837 | if(e == null){ 838 | if(stats && stats.height){ 839 | if(ibc.lastBlock != stats.height) { //this is a new block! 840 | logger.log('[ibc-js] New block!', stats.height); 841 | ibc.lastBlock = stats.height; 842 | ibc.q.pop(); //action is resolved, remove 843 | if(ibc.monitorFunction) ibc.monitorFunction(stats); //call the user's callback 844 | } 845 | } 846 | } 847 | } 848 | 849 | //============================================================================================================================ 850 | // EXTERNAL- monitor_blockheight() - exposed function that user can use to get callback when any new block is written to the chain 851 | //============================================================================================================================ 852 | ibc.prototype.monitor_blockheight = function(cb) { //hook in your own function, triggers when chain grows 853 | setInterval(function(){heart_beat();}, fast_mode); 854 | ibc.monitorFunction = cb; //store it 855 | }; 856 | 857 | //============================================================================================================================ 858 | // EXTERNAL- get_transaction() - exposed function to find a transaction based on its UDID 859 | //============================================================================================================================ 860 | ibc.prototype.get_transaction = function(udid, cb) { 861 | var options = { 862 | path: '/transactions/' + udid 863 | }; 864 | 865 | rest.get(options, null, function(statusCode, data){ 866 | if(statusCode != null){ 867 | logger.error('[ibc-js] Get Transaction - failure:', statusCode); 868 | if(cb) cb(helper.eFmt('read() error', statusCode, data), null); 869 | } 870 | else{ 871 | logger.log('[ibc-js] Get Transaction - success:', data); 872 | if(cb) cb(null, data); 873 | } 874 | }); 875 | }; 876 | 877 | //============================================================================================================================ 878 | // Helper Functions() 879 | //============================================================================================================================ 880 | //build_invoke_func() - create JS function that calls the custom goLang function in the chaincode 881 | //================================================================== 882 | function build_invoke_func(name){ 883 | if(ibc.chaincode.invoke[name] != null){ //skip if already exists 884 | //logger.log('[ibc-js] \t skip, func', name, 'already exists'); 885 | } 886 | else { 887 | logger.log('[ibc-js] Found cc invoke function: ', name); 888 | ibc.chaincode.details.func.invoke.push(name); 889 | ibc.chaincode.invoke[name] = function(args, enrollId, cb){ //create the function in the chaincode obj 890 | if(typeof enrollId === 'function'){ //if cb is in 2nd param use known enrollId 891 | cb = enrollId; 892 | enrollId = ibc.chaincode.details.peers[ibc.selectedPeer].enrollId; 893 | } 894 | if(enrollId == null) { //if enrollId not provided, use known valid one 895 | enrollId = ibc.chaincode.details.peers[ibc.selectedPeer].enrollId; 896 | } 897 | 898 | var options = {}, body = {}; 899 | options = {path: '/chaincode'}; 900 | body = { 901 | jsonrpc: '2.0', 902 | method: 'invoke', 903 | params: { 904 | type: 1, 905 | chaincodeID:{ 906 | name: ibc.chaincode.details.deployed_name 907 | }, 908 | ctorMsg: { 909 | function: name, 910 | args: args 911 | }, 912 | secureContext: enrollId 913 | }, 914 | id: Date.now() 915 | }; 916 | rest.post(options, null, body, function(statusCode, data){ 917 | if(statusCode != null){ 918 | logger.error('[ibc-js]', name, ' - failure:', statusCode, data); 919 | if(cb) cb(helper.eFmt('invoke() error', statusCode, data), null); 920 | } 921 | else{ 922 | logger.log('[ibc-js]', name, ' - success:', data); 923 | ibc.q.push(Date.now()); //new action, add it to queue 924 | if(cb) cb(null, data); 925 | } 926 | }); 927 | }; 928 | } 929 | } 930 | 931 | //================================================================== 932 | //build_query_func() - create JS function that calls the custom goLang function in the chaincode 933 | //================================================================== 934 | function build_query_func(name){ 935 | if(ibc.chaincode.query[name] != null && name !== 'read'){ //skip if already exists 936 | //logger.log('[ibc-js] \t skip, func', name, 'already exists'); 937 | } 938 | else { 939 | logger.log('[ibc-js] Found cc query function: ', name); 940 | ibc.chaincode.details.func.query.push(name); 941 | ibc.chaincode.query[name] = function(args, enrollId, cb){ //create the function in the chaincode obj 942 | if(typeof enrollId === 'function'){ //if cb is in 2nd param use known enrollId 943 | cb = enrollId; 944 | enrollId = ibc.chaincode.details.peers[ibc.selectedPeer].enrollId; 945 | } 946 | if(enrollId == null) { //if enrollId not provided, use known valid one 947 | enrollId = ibc.chaincode.details.peers[ibc.selectedPeer].enrollId; 948 | } 949 | 950 | var options = {}, body = {}; 951 | 952 | options = {path: '/chaincode'}; 953 | body = { 954 | jsonrpc: '2.0', 955 | method: 'query', 956 | params: { 957 | type: 1, 958 | chaincodeID:{ 959 | name: ibc.chaincode.details.deployed_name 960 | }, 961 | ctorMsg: { 962 | function: name, 963 | args: args 964 | }, 965 | secureContext: enrollId 966 | }, 967 | id: Date.now() 968 | }; 969 | 970 | rest.post(options, null, body, function(statusCode, data){ 971 | if(statusCode != null){ 972 | logger.error('[ibc-js]', name, ' - failure:', statusCode, data); 973 | if(cb) cb(helper.eFmt('query() error', statusCode, data), null); 974 | } 975 | else{ 976 | logger.log('[ibc-js]', name, ' - success:', data); 977 | if(cb){ 978 | if(data){ 979 | if(data.error) cb(helper.eFmt('query() resp error', 400, data.error), null); 980 | else if(data.result) cb(null, data.result.message); 981 | else cb(null, data.OK); 982 | } 983 | else cb(helper.eFmt('query() resp error', 502, data), null); //something is wrong, response is not what we expect 984 | } 985 | } 986 | }); 987 | }; 988 | } 989 | } 990 | 991 | module.exports = ibc; 992 | -------------------------------------------------------------------------------- /lib/helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /******************************************************************************* 3 | * Copyright (c) 2016 IBM Corp. 4 | * 5 | * All rights reserved. 6 | * 7 | *******************************************************************************/ 8 | var fs = require('fs'); 9 | var async = require('async'); 10 | var path = require('path'); 11 | 12 | //================================================================== 13 | //eFmt() - format errors 14 | //================================================================== 15 | module.exports.eFmt = function eFmt(name, code, details){ //my error format 16 | return { 17 | name: String(name), //error short name 18 | code: Number(code), //http code when applicable 19 | details: details //error description 20 | }; 21 | }; 22 | 23 | 24 | //================================================================== 25 | //filter_users() - return only client level enrollId - [1=client, 2=nvp, 4=vp, 8=auditor accurate as of 2/18] 26 | //================================================================== 27 | module.exports.filter_users = function(users){ //this is only needed in a permissioned network 28 | var valid_users = []; 29 | for(var i = 0; i < users.length; i++) { 30 | if(users[i].enrollId.indexOf('user_type1') === 0){ //type should be 1 for client 31 | valid_users.push(users[i]); 32 | } 33 | } 34 | return valid_users; 35 | }; 36 | 37 | // ============================================================================================================================ 38 | //removeThing() - clear the temp directory 39 | // ============================================================================================================================ 40 | module.exports.removeThing = function(dir, cb){ 41 | //console.log('!', dir); 42 | fs.readdir(dir, function (err, files) { 43 | if(err != null || !files || files.length === 0){ 44 | cb(); 45 | } 46 | else{ 47 | async.each(files, function (file, cb) { //over each thing 48 | file = path.join(dir, file); 49 | fs.stat(file, function(err, stat) { 50 | if (err) { 51 | if(cb) cb(err); 52 | return; 53 | } 54 | if (stat.isDirectory()) { 55 | module.exports.removeThing(file, cb); //keep going 56 | } 57 | else { 58 | //console.log('!', dir); 59 | fs.unlink(file, function(err) { 60 | if (err) { 61 | if(cb) cb(err); 62 | return; 63 | } 64 | //console.log('good', dir); 65 | if(cb) cb(); 66 | return; 67 | }); 68 | } 69 | }); 70 | }, function (err) { 71 | if(err){ 72 | if(cb) cb(err); 73 | return; 74 | } 75 | fs.rmdir(dir, function (err) { 76 | if(cb) cb(err); 77 | return; 78 | }); 79 | }); 80 | } 81 | }); 82 | }; -------------------------------------------------------------------------------- /lib/rest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* global Buffer */ 3 | /******************************************************************************* 4 | * Copyright (c) 2015 IBM Corp. 5 | * 6 | * All rights reserved. 7 | * 8 | * Contributors: 9 | * David Huffman - Initial implementation 10 | *******************************************************************************/ 11 | /* 12 | Version: 0.7.3 13 | Updated: 11/03/2016 14 | ----------------------------------------------------------------- 15 | Use: var rest = require('./rest'); 16 | rest.init({quiet: false}); //set default values here for all calls of 'rest' 17 | var options = { //set options here for this call (overrides init) 18 | host: HOST_HERE, 19 | path: PATH HERE, 20 | headers: {"Accept": "application/json"} 21 | }; 22 | rest.get(options, null, function(errCode, response){ 23 | if(errCode != null) logger.log('Get - failure', errCode, response); 24 | else logger.log('Get - success', response); 25 | }); 26 | 27 | ----------------------------------------------------------------- 28 | 29 | Valid "options" values: (these are default ones that come from requests module) 30 | ----------------------------------------------------------------- 31 | host: A domain name or IP address of the server to issue the request to. Defaults to 'localhost'. 32 | hostname: To support url.parse() hostname is preferred over host 33 | port: Port of remote server. Defaults to 80. 34 | localAddress: Local interface to bind for network connections. 35 | socketPath: Unix Domain Socket (use one of host:port or socketPath) 36 | method: A string specifying the HTTP request method. Defaults to 'GET'. 37 | path: Request path. Defaults to '/'. Should include query string if any. E.G. '/index.html?page=12'. 38 | An exception is thrown when the request path contains illegal characters. Currently, only spaces are rejected but that may change in the future. 39 | headers: An object containing request headers. 40 | auth: Basic authentication i.e. 'user:password' to compute an Authorization header. 41 | agent: Controls Agent behavior. When an Agent is used request will default to Connection: keep-alive. Possible values: 42 | undefined (default): use global Agent for this host and port. 43 | Agent object: explicitly use the passed in Agent. 44 | false: opts out of connection pooling with an Agent, defaults request to Connection: close. 45 | keepAlive: {Boolean} Keep sockets around in a pool to be used by other requests in the future. Default = false 46 | keepAliveMsecs: {Integer} When using HTTP KeepAlive, how often to send TCP KeepAlive packets over sockets being kept alive. 47 | Default = 1000. Only relevant if keepAlive is set to true. 48 | 49 | Plus my "options" values: 50 | ----------------------------------------------------------------- 51 | quiet: If true will not print to logger. Defaults false. 52 | tls: Iff false will use http instead of https. Defaults true. 53 | timeout: Integer in milliseconds to time out requests. Defaults 20,000 54 | include_headers: If true the response argument will be {"response":, "headers":} 55 | */ 56 | 57 | var https_mod = require('https'); 58 | var http_mod = require('http'); 59 | var querystring = require('querystring'); 60 | var default_options = { 61 | quiet: false, 62 | tls: true, 63 | timeout: 20000, 64 | include_headers: false 65 | }; 66 | var logger = {log: console.log, error: console.error, debug: console.log, warn: console.log}; 67 | 68 | //is the obj empty or not 69 | function isEmpty(obj) { 70 | for(var prop in obj) { 71 | if(obj.hasOwnProperty(prop)) 72 | return false; 73 | } 74 | return true; 75 | } 76 | 77 | //merge fields in obj B to obj A only if they don't exist in obj A 78 | function mergeBtoA(b, a){ 79 | for(var i in b){ 80 | if(a[i] === undefined) { 81 | a[i] = JSON.parse(JSON.stringify(b[i])); 82 | } 83 | } 84 | return a; 85 | } 86 | 87 | //main http request builder/handler/thingy 88 | function http_req(options, query_params, body, attempt, cb){ 89 | var acceptJson = false, http, http_txt = '', request = null, formatted_body = null; 90 | var ids = 'abcdefghijkmnopqrstuvwxyz'; 91 | var id = ids[Math.floor(Math.random() * ids.length)]; //random letter to help id calls when there are multiple rest calls 92 | var cb_fired = false; 93 | 94 | if(!attempt || isNaN(attempt)) attempt = 1; //defaults to attempt # 1 95 | options = mergeBtoA(default_options, options); 96 | 97 | // ----- Handle Call Back ----- // 98 | function call_cb(ret){ 99 | if(cb_fired === false){ //only call cb once! 100 | cb_fired = true; 101 | if(options.include_headers) ret.msg = {response:ret.msg, headers: ret.headers}; 102 | if(ret.code <= 399 && ret.code !== 302) ret.code = null; 103 | if(cb) cb(ret.code, ret.msg); //1st arg is error status code, null if no error code 104 | return; 105 | } 106 | } 107 | 108 | // ---- Pick HTTP vs HTTPS ---- // 109 | if(options.ssl === false || options.tls === false) { 110 | http = http_mod; //if options.tls === false use http 111 | http_txt = '[http ' + options.method + ' - ' + id + ']'; 112 | } 113 | else{ //else use https 114 | http = https_mod; 115 | http_txt = '[https ' + options.method + ' - ' + id + ']'; 116 | } 117 | 118 | if(!options.quiet) logger.debug(http_txt + ' ' + options.host + ':' + options.port); 119 | if(!options.quiet) logger.debug(http_txt + ' ' + options.path); 120 | 121 | // ---- Sanitize Inputs ---- // 122 | if(!options.headers) options.headers = {}; 123 | for(var i in options.headers) { //convert all header keys to lower-case for easier parsing 124 | var temp = options.headers[i]; 125 | delete options.headers[i]; 126 | if(temp != null){ 127 | options.headers[i.toLowerCase()] = temp; 128 | } 129 | } 130 | 131 | if(typeof body === 'object' && body != null){ 132 | options.headers['content-type'] = 'application/json'; 133 | formatted_body = JSON.stringify(body); //stringify body 134 | } 135 | else formatted_body = body; 136 | 137 | if(options.headers.accept && options.headers.accept.indexOf('json') >= 0) acceptJson = true; 138 | if(query_params && typeof query_params === 'object') options.path += '?' + querystring.stringify(query_params); 139 | 140 | if(formatted_body) options.headers['content-length'] = Buffer.byteLength(formatted_body); 141 | else if(options.headers['content-length']) delete options.headers['content-length']; //we don't need you 142 | 143 | if(!options.quiet && options.method.toLowerCase() !== 'get') logger.debug(' body:', formatted_body); 144 | 145 | // --------- Handle Request --------- // 146 | request = http.request(options, function(resp) { 147 | var str = '', chunks = 0; 148 | if(!options.quiet) logger.debug(http_txt + ' Status code: ' + resp.statusCode); 149 | 150 | resp.setEncoding('utf8'); 151 | resp.on('data', function(chunk) { //merge chunks of request 152 | str += chunk; 153 | chunks++; 154 | }); 155 | resp.on('end', function() { //wait for end before decision 156 | var ret = { 157 | code: resp.statusCode, 158 | headers: resp.headers, 159 | msg: str 160 | }; 161 | 162 | // --------- Process Response - Debug Msgs --------- // 163 | if(resp.statusCode == 204){ //empty response, don't parse body 164 | if(!options.quiet) logger.debug(http_txt + ' Data: No Content'); 165 | } 166 | else if(resp.statusCode === 302){ //redirect 167 | if(!options.quiet) logger.error(http_txt + ' Error - got a redirect, not what we want'); 168 | } 169 | else if(resp.statusCode >= 200 && resp.statusCode <= 399){ //valid status codes 170 | if(acceptJson){ 171 | try{ 172 | ret.msg = JSON.parse(str); //all good [json resp] 173 | } 174 | catch(e){ 175 | if(!options.quiet) logger.error(http_txt + ' Error - response is not JSON: ', str); 176 | ret.code = 500; 177 | ret.msg = 'Invalid JSON response: ' + str; 178 | } 179 | } 180 | else { //all good [not json resp] 181 | if(!options.quiet) logger.debug(http_txt + ' Data:', str); 182 | } 183 | } 184 | else { //invalid status codes 185 | if(!options.quiet) logger.error(http_txt + ' Error - status code: ' + resp.statusCode, str); 186 | if(acceptJson){ 187 | try{ 188 | ret.msg = JSON.parse(str); //attempt to parse error for JSON 189 | } 190 | catch(e){} 191 | } 192 | } 193 | 194 | // --------- Call CallBack --------- // 195 | return call_cb(ret); 196 | }); 197 | }); 198 | 199 | // --------- Handle Request Errors --------- // 200 | request.on('error', function(e) { //handle error event 201 | if(e.code === 'ECONNRESET' && attempt <= 3) { //try ECONNRESETs again 202 | if(cb_fired === false){ 203 | logger.warn(http_txt + ' Warning - detected ECONNRESET, will try HTTP req again. attempt:' + attempt); 204 | attempt++; 205 | cb_fired = true; //set this just in case 206 | setTimeout(function(){ http_req(options, query_params, body, attempt, cb); }, 250 * Math.pow(2, attempt+1)); 207 | } 208 | return; 209 | } 210 | else { 211 | if(!options.quiet) logger.error(http_txt + ' Error - unknown issue with request: ', e);//catch failed request (failed DNS lookup and such) 212 | return call_cb({code: 500, headers: null, msg: e}); 213 | } 214 | }); 215 | 216 | // --------- Handle Request Timeouts --------- // 217 | request.setTimeout(Number(options.timeout) || default_options.timeout); 218 | request.on('timeout', function(){ //handle time out events 219 | if(!options.quiet) logger.error(http_txt + ' Error - request timed out'); 220 | return call_cb({code: 408, headers: null, msg: 'Request timed out'}); 221 | }); 222 | 223 | // ----- Body ----- // 224 | if(formatted_body && formatted_body !== '' && !isEmpty(formatted_body)){ 225 | request.write(formatted_body); 226 | } 227 | request.end(); //send the request 228 | } 229 | 230 | //load new default option values 231 | module.exports.init = function(opt, log_outputs){ 232 | for(var i in opt){ 233 | default_options[i] = JSON.parse(JSON.stringify(opt[i])); 234 | } 235 | 236 | if(log_outputs && log_outputs.info) logger.log = log_outputs.info; //send normal logs here 237 | if(log_outputs && log_outputs.error) logger.error = log_outputs.error; //send error logs here 238 | if(log_outputs && log_outputs.warn) logger.warn = log_outputs.warn; //send warn logs here 239 | if(log_outputs && log_outputs.debug) logger.debug = log_outputs.debug; //send debug logs here 240 | }; 241 | 242 | //http post 243 | module.exports.post = function (l_options, query_params, body, cb){ 244 | l_options.method = 'POST'; 245 | http_req(l_options, query_params, body, 1 , cb); 246 | }; 247 | 248 | //http put 249 | module.exports.put = function (l_options, query_params, body, cb){ 250 | l_options.method = 'PUT'; 251 | http_req(l_options, query_params, body, 1 , cb); 252 | }; 253 | 254 | //http delete 255 | module.exports.delete = function (l_options, query_params, body, cb){ 256 | l_options.method = 'DELETE'; 257 | http_req(l_options, query_params, body, 1 , cb); 258 | }; 259 | 260 | //http get 261 | module.exports.get = function (l_options, query_params, cb){ 262 | l_options.method = 'GET'; 263 | http_req(l_options, query_params, null, 1 , cb); 264 | }; 265 | 266 | //http head 267 | module.exports.head = function (l_options, query_params, cb){ 268 | l_options.method = 'HEAD'; 269 | http_req(l_options, query_params, null, 1 , cb); 270 | }; 271 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ibm-blockchain-js", 3 | "version": "1.3.2", 4 | "description": "A library for easily interacting with IBM Blockchain.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/IBM-Blockchain/ibm-blockchain-js.git" 12 | }, 13 | "keywords": [ 14 | "ibm", 15 | "blockchain", 16 | "obc", 17 | "peer", 18 | "node", 19 | "hyperledger" 20 | ], 21 | "dependencies": { 22 | "adm-zip": "0.4.4", 23 | "async": "1.5.0" 24 | }, 25 | "devDependencies": { 26 | "tape": "latest" 27 | }, 28 | "author": { 29 | "name": "David Huffman and Ben Smith" 30 | }, 31 | "license": "Apache-2.0" 32 | } 33 | -------------------------------------------------------------------------------- /test/ValidLoadTest.js: -------------------------------------------------------------------------------- 1 | // Function testing the SDK 2 | // Logging statement to terminal to indicate which test is running. 3 | console.log("Now starting LoadTest.js"); 4 | 5 | // Starting out by requiring all dependancies 6 | var test = require('tape'); 7 | var Ibc1 = require('..'); 8 | 9 | // Then define new instances. 10 | var ibc = new Ibc1(); 11 | var chaincode = {}; 12 | 13 | // Define a flag which determines whether to use Valid or Invalid Options for the test. 14 | // V = Valid, I = Invalid 15 | var Flag = "I"; 16 | 17 | // Define options for ibc.load_chancode, where to get the blockchain code 18 | // Made this an if/then flagged option to decide which option set to use. 19 | if (Flag == "V") { 20 | var options = { 21 | zip_url: 'https://github.com/ibm-blockchain/marbles-chaincode/archive/master.zip', 22 | unzip_dir: 'marbles-chaincode-master/part2', 23 | git_url: 'https://github.com/ibm-blockchain/marbles-chaincode/part2', 24 | deployed_name: null 25 | }; 26 | } 27 | else { 28 | // Define some options with a bad zip_url to see how the sdk catches it. 29 | var options = { 30 | zip_url: 'https://github.com/ibm-lockchain/marbles-chaincode/archive/master.zip', 31 | unzip_dir: 'marbles-chaincode-master/part2', 32 | git_url: 'https://github.com/ibm-blockchain/marbles-chaincode/part2', 33 | deployed_name: null 34 | }; 35 | } 36 | 37 | // I need to create just one test, and run ibc.load inside of it. 38 | // I put the tests into an if/then with the Flag for options. 39 | test('Was the load_chaincode sucessful', function (t) { 40 | if (Flag == "V") { 41 | ibc.load_chaincode(options, function cb_ready(err, cc) { 42 | t.error(err, 'There were no errors'); 43 | }); // End of the Valid Load Test 44 | } 45 | else { 46 | ibc.load_chaincode(options, function cb_ready(err, cc) { 47 | t.equal(err, 'Invalid or unsupported zip format. No END header found', 'The error message was unexpected.') 48 | }) 49 | } 50 | t.end(); //End Testing 51 | }); -------------------------------------------------------------------------------- /test/cb_readyTest.js: -------------------------------------------------------------------------------- 1 | // Function testing the SDK 2 | // This file contains the most recent tests still being developed. 3 | console.log("Now starting cb_readyTeats.js"); 4 | 5 | // Starting out by requiring all dependancies 6 | var test = require('tape'); 7 | var Ibc1 = require('..'); 8 | 9 | // Then define new instances that will be needed 10 | var ibc = new Ibc1(); 11 | var chaincode = {}; 12 | 13 | // configure ibc-js sdk by defining options 14 | var options = { 15 | 16 | // Set existant network credentials 17 | // Create a network on Bluemix Experimental BlockChain offering 18 | // Service Credentials are found under the Blockchain instance tab. 19 | 20 | network:{ peers: [{ 21 | "api_host": "3f3fa6c3-a8b4-48b2-95bc-63b5058fa333_vp1-api.blockchain.ibm.com", 22 | "api_port": "80", 23 | "id": "3f3fa6c3-a8b4-48b2-95bc-63b5058fa333_vp1", 24 | "api_url": "http://3f3fa6c3-a8b4-48b2-95bc-63b5058fa333_vp1-api.blockchain.ibm.com:80" 25 | }], 26 | 27 | // For simplicity, I chose the first user on the list provided. 28 | users: [{ 29 | "username": "user_type0_52737ec3c6", 30 | "secret": "4841d68d27", 31 | "enrollId":"user_type0_52737ec3c6", 32 | "enrollSecret":"4841d68d27" 33 | }] }, 34 | 35 | // The chaincode version being tested here is the one deployed in Marbles2. 36 | chaincode:{ 37 | zip_url: 'https://github.com/ibm-blockchain/marbles-chaincode/archive/master.zip', 38 | unzip_dir: 'marbles-chaincode-master/part2', 39 | git_url: 'https://github.com/ibm-blockchain/marbles-chaincode/part2' 40 | } 41 | }; 42 | 43 | test('Was the load_chaincode sucessful', function (t) { 44 | // Load the Marbles2 chaincode, with defined options, and return call-back-when-ready function. 45 | ibc.load(options, cb_ready); 46 | 47 | // Define the call-back-when-ready function returned above 48 | // call-back-when-ready function has err 49 | function cb_ready(err, cc){ 50 | //response has chaincode functions 51 | 52 | t.error(err, 'There were no errors'); 53 | 54 | // if the deployed name is blank, then chaincode has not been deployed 55 | if(cc.details.deployed_name === ""){ 56 | cc.deploy('init', ['99'], './cc_summaries', cb_deployed); 57 | function cb_deployed(err){ 58 | t.error(err, 'There were no errors'); 59 | console.log('sdk has deployed code and waited'); 60 | }; 61 | } 62 | else{ 63 | console.log('chaincode summary file indicates chaincode has been previously deployed'); 64 | }; 65 | } 66 | t.end(); 67 | }); 68 | -------------------------------------------------------------------------------- /test/sdkfunctiontest.js: -------------------------------------------------------------------------------- 1 | // Function testing the SDK 2 | // This file contains the most recent tests still being developed. 3 | 4 | console.log("Now starting SDKFunctiontest.js"); 5 | 6 | // Starting out by requiring all dependancies 7 | var test = require('tape'); 8 | var Ibc1 = require('..'); 9 | 10 | // Then define new instances that will be needed 11 | var ibc = new Ibc1(); 12 | var chaincode = {}; 13 | 14 | // configure ibc-js sdk by defining options 15 | var options = { 16 | 17 | // Set existant network credentials 18 | // Create a network on Bluemix Experimental BlockChain offering 19 | // Service Credentials are found under the Blockchain instance tab. 20 | 21 | network:{ 22 | peers: [{ 23 | "api_host": "3f3fa6c3-a8b4-48b2-95bc-63b5058fa333_vp1-api.blockchain.ibm.com", 24 | "api_port": "80", 25 | "id": "3f3fa6c3-a8b4-48b2-95bc-63b5058fa333_vp1", 26 | "api_url": "http://3f3fa6c3-a8b4-48b2-95bc-63b5058fa333_vp1-api.blockchain.ibm.com:80" 27 | }], 28 | 29 | // For simplicity, I chose the first user on the list provided. 30 | users: [{ 31 | "username": "user_type0_52737ec3c6", 32 | "secret": "4841d68d27" 33 | }] }, 34 | 35 | // The chaincode version being tested here is the one deployed in Marbles2. 36 | chaincode:{ 37 | zip_url: 'https://github.com/ibm-blockchain/marbles-chaincode/archive/master.zip', 38 | unzip_dir: 'marbles-chaincode-master/part2', 39 | git_url: 'https://github.com/ibm-blockchain/marbles-chaincode/part2' 40 | } 41 | }; 42 | 43 | 44 | // Load the Marbles2 chaincode, with defined options, and return call-back-when-ready function. 45 | ibc.load(options, cb_ready); 46 | 47 | // Define the call-back-when-ready function returned above 48 | // call-back-when-ready function has err 49 | function cb_ready(err, cc){ 50 | //response has chaincode functions 51 | 52 | // if the deployed name is blank, then chaincode has not been deployed 53 | if(cc.details.deployed_name === ""){ 54 | cc.deploy('init', ['99'], './cc_summaries', cb_deployed); 55 | function cb_deployed(err){ 56 | console.log('sdk has deployed code and waited'); 57 | } 58 | } 59 | else{ 60 | console.log('chaincode summary file indicates chaincode has been previously deployed'); 61 | } 62 | }; 63 | 64 | ibc.chain_stats([stats_callback]); 65 | function stats_callback(e, stats){ 66 | console.log('got some stats', stats); 67 | } 68 | 69 | --------------------------------------------------------------------------------