├── .nvmrc ├── .travis.yml ├── .gitignore ├── src ├── utils │ ├── timestamp.js │ ├── normalizePayload.js │ ├── parseErrorMessage.js │ ├── logger.js │ ├── identity.js │ ├── db.js │ └── migrations.js ├── constants │ └── errors.js ├── lib │ ├── ChaincodeError.js │ ├── ChaincodeBase.js │ └── TransactionHelper.js ├── index.js └── mocks │ └── ChaincodeStub.js ├── .editorconfig ├── LICENSE ├── package.json ├── .eslintrc ├── test └── migrations.test.js └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | 4 | *.orig 5 | -------------------------------------------------------------------------------- /src/utils/timestamp.js: -------------------------------------------------------------------------------- 1 | function toDate(timestamp) { 2 | const milliseconds = (timestamp.seconds.low + ((timestamp.nanos / 1000000) / 1000)) * 1000; 3 | 4 | return new Date(milliseconds); 5 | } 6 | 7 | module.exports = {toDate}; 8 | -------------------------------------------------------------------------------- /src/constants/errors.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | 'TYPE_ERROR': 'type_error', 4 | 'MIGRATION_PATH_NOT_DEFINED': 'migration_path_not_defined', 5 | 6 | 'UNKNOWN_FUNCTION': 'unknown_function', 7 | 'UNKNOWN_ERROR': 'unknown_error', 8 | 9 | 'PARSING_PARAMETERS_ERROR': 'parsing_parameters_error', 10 | 'CHAINCODE_INVOKE_ERROR': 'chaincode_invoke_error' 11 | }; 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # change these settings to your own preference 11 | indent_style = space 12 | indent_size = 4 13 | 14 | # we recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | 23 | [*.json] 24 | indent_style = space 25 | indent_size = 2 -------------------------------------------------------------------------------- /src/lib/ChaincodeError.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | class ChaincodeError extends Error { 4 | 5 | constructor(key, data, stack) { 6 | super(key); 7 | 8 | if (!_.isUndefined(stack)) { 9 | this.stack = stack; 10 | } 11 | this.data = data || {}; 12 | } 13 | 14 | get serialized() { 15 | 16 | return JSON.stringify({ 17 | 'key': this.message, 18 | 'data': this.data, 19 | 'stack': this.stack 20 | }); 21 | } 22 | 23 | } 24 | 25 | module.exports = ChaincodeError; 26 | -------------------------------------------------------------------------------- /src/utils/normalizePayload.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | module.exports = function normalizePayload(value) { 4 | if (_.isDate(value)) { 5 | 6 | return value.getTime(); 7 | } else if (_.isString(value)) { 8 | 9 | return value; 10 | } else if (_.isArray(value)) { 11 | 12 | return _.map(value, (v) => { 13 | 14 | return normalizePayload(v); 15 | }); 16 | } else if (_.isObject(value)) { 17 | 18 | return _.mapValues(value, (v) => { 19 | 20 | return normalizePayload(v); 21 | }); 22 | } 23 | 24 | return value; 25 | }; 26 | -------------------------------------------------------------------------------- /src/utils/parseErrorMessage.js: -------------------------------------------------------------------------------- 1 | const logger = require('./logger').getLogger('utils/fabric/parseErrorMessage'); 2 | 3 | const INVOKE_REGEX = /^.*?Calling\s+chaincode\s+Invoke\(\)\s+returned\s+error\s+response\s+(.*)\..*?$/i; 4 | 5 | module.exports = function parseErrorMessage(message) { 6 | try { 7 | if (INVOKE_REGEX.test(message)) { 8 | const match = message.match(INVOKE_REGEX)[1]; 9 | const errorResponse = JSON.parse(match); 10 | return Array.isArray(errorResponse) ? errorResponse[0] : errorResponse; 11 | } 12 | } catch (e) { 13 | logger.error(`Unable to parse error details from error: ${message}.`); 14 | } 15 | 16 | return message; 17 | }; 18 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const ChaincodeBase = require('./lib/ChaincodeBase'); 2 | const ChaincodeError = require('./lib/ChaincodeError'); 3 | const TransactionHelper = require('./lib/TransactionHelper'); 4 | 5 | const logger = require('./utils/logger'); 6 | const normalizePayload = require('./utils/normalizePayload'); 7 | const identity = require('./utils/identity'); 8 | const migrations = require('./utils/migrations'); 9 | const {iteratorToList} = require('./utils/db'); 10 | 11 | module.exports = { 12 | ChaincodeBase, 13 | ChaincodeError, 14 | TransactionHelper, 15 | utils: { 16 | logger, 17 | normalizePayload, 18 | identity, 19 | migrations, 20 | db: {iteratorToList} 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | const log4js = require('log4js'); 2 | 3 | /** 4 | * @param {String} name 5 | * 6 | * @return a log4j logger object prefixed with the given name. 7 | */ 8 | module.exports.getLogger = function(name) { 9 | const logger = log4js.getLogger(`chaincode-utils/${name}`); 10 | 11 | // set the logging level based on the environment variable 12 | // configured by the peer 13 | const level = process.env.CHAINCODE_LOGGING_LEVEL; 14 | let loglevel = 'debug'; 15 | if (typeof level === 'string') { 16 | switch (level.toUpperCase()) { 17 | case 'CRITICAL': 18 | loglevel = 'fatal'; 19 | break; 20 | case 'ERROR': 21 | loglevel = 'error'; 22 | break; 23 | case 'WARNING': 24 | loglevel = 'warn'; 25 | break; 26 | case 'DEBUG': 27 | loglevel = 'debug'; 28 | } 29 | } 30 | 31 | logger.level = loglevel; 32 | 33 | return logger; 34 | }; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kunstmaan | Part of Accenture Interactive 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kunstmaan/hyperledger-fabric-node-chaincode-utils", 3 | "version": "0.4.0", 4 | "description": "Utilities for writing Hyperledger Fabric chaincode in node.js", 5 | "main": "./src/index.js", 6 | "scripts": { 7 | "test": "npm run lint", 8 | "lint": "eslint ." 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Kunstmaan/hyperledger-fabric-node-chaincode-utils.git" 13 | }, 14 | "keywords": [ 15 | "chaincode", 16 | "hyperledger", 17 | "fabric", 18 | "node.js" 19 | ], 20 | "author": "Kunstmaan", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/Kunstmaan/hyperledger-fabric-node-chaincode-utils/issues" 24 | }, 25 | "homepage": "https://github.com/Kunstmaan/hyperledger-fabric-node-chaincode-utils#readme", 26 | "dependencies": { 27 | "grpc": "^1.8.4", 28 | "js-sha3": "^0.7.0", 29 | "jsrsasign": "^8.0.5", 30 | "lodash": "^4.17.4", 31 | "log4js": "^2.5.2" 32 | }, 33 | "devDependencies": { 34 | "eslint": "^4.17.0", 35 | "eslint-config-airbnb-base": "^12.1.0", 36 | "eslint-plugin-import": "^2.8.0", 37 | "eslint-plugin-jest": "^21.7.0" 38 | }, 39 | "peerDependencies": { 40 | "fabric-shim": "^1.1.2" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "es6": true, 6 | "jest/globals": true 7 | }, 8 | "parserOptions": { 9 | "sourceType": "module", 10 | "modules": true, 11 | "ecmaFeatures": { 12 | "jsx": true, 13 | "modules": true 14 | } 15 | }, 16 | "plugins": [ 17 | "jest" 18 | ], 19 | "extends": "airbnb-base", 20 | "rules": { 21 | "arrow-body-style": "off", 22 | "arrow-parens": ["error", "always"], 23 | "comma-dangle": ["error", "never"], 24 | "default-case": "off", 25 | "indent": ["error", 4, { "SwitchCase": 1 }], 26 | "max-len": ["error", 250, {"ignoreComments": true, "ignoreTemplateLiterals": true}], 27 | "no-console": 0, 28 | "no-new": "off", 29 | "no-use-before-define": ["error", {"functions": false, "classes": true}], 30 | "no-trailing-spaces": ["error", {"skipBlankLines": true}], 31 | "linebreak-style": ["error", "unix"], 32 | "object-curly-spacing": ["error", "never"], 33 | "quotes": ["error", "single"], 34 | "semi": ["error", "always"], 35 | "space-before-blocks": ["error", "always"], 36 | "space-before-function-paren": ["error", { 37 | "anonymous": "never", 38 | "named": "never", 39 | "asyncArrow": "always" 40 | }], 41 | "no-param-reassign": ["error", { "props": true }], 42 | "import/prefer-default-export": "off", 43 | "import/no-extraneous-dependencies": ["error", {"devDependencies": ["gulpfile.babel.js"]}], 44 | "import/no-dynamic-require": "off", 45 | "global-require": "off", 46 | "func-names": "off", 47 | "no-restricted-syntax": "off", 48 | "jest/no-disabled-tests": "warn", 49 | "jest/no-focused-tests": "error", 50 | "jest/no-identical-title": "error", 51 | "jest/prefer-to-have-length": "warn", 52 | "jest/valid-expect": "error", 53 | "quote-props": "off", 54 | "no-await-in-loop": "off", 55 | "class-methods-use-this": "off", 56 | "padded-blocks": "off" 57 | }, 58 | "globals": {"module": true, "jest": true} 59 | } 60 | -------------------------------------------------------------------------------- /test/migrations.test.js: -------------------------------------------------------------------------------- 1 | /* 2 | const path = require('path'); 3 | const {MIGRATION_STATE_KEY, runMigrations} = require('./../src/utils/migrations'); 4 | const {serialize, toObject, toDate} = require('./../src/utils/db'); 5 | const ChaincodeStub = require('./../src/mocks/ChaincodeStub'); 6 | const TransactionHelper = require('./../src/lib/TransactionHelper'); 7 | 8 | 9 | test('runs no migrations for the ground contours when last update time was today', async () => { 10 | let lastUpdateTimeChanged = false; 11 | const chainCodeStubMiddleware = (functionName, args) => { 12 | if (functionName === 'getState') { 13 | return serialize(new Date()); 14 | } 15 | if (args.key === MIGRATION_STATE_KEY) { 16 | lastUpdateTimeChanged = true; 17 | } 18 | 19 | return undefined; 20 | }; 21 | 22 | const migrationsDir = path.join(__dirname, '/../ground_contours/migrations'); 23 | const txHelper = new TransactionHelper(new ChaincodeStub(chainCodeStubMiddleware)); 24 | const result = await runMigrations( 25 | migrationsDir, 26 | null, 27 | new ChaincodeStub(chainCodeStubMiddleware), 28 | txHelper, 29 | [] 30 | ); 31 | expect(result).toBe('No migrations to execute'); 32 | // Migration state updates 33 | expect(lastUpdateTimeChanged).not.toBeTruthy(); 34 | }); 35 | 36 | test('runs all migrations for the ground contours when last update time was in the past', async () => { 37 | let putStateTimesCalled = 0; 38 | let lastUpdateTimeChanged = false; 39 | const chainCodeStubMiddleware = (functionName, args) => { 40 | if (functionName === 'getState') { 41 | return serialize(new Date(1970, 1, 1)); 42 | } 43 | if (functionName === 'putState') { 44 | expect(args.key).toBeDefined(); 45 | if (args.key.indexOf(CONSTANTS.CONTOUR_TYPE_PREFIX) === 0) { 46 | putStateTimesCalled += 1; 47 | expect(toObject(args.value).policy).toBeDefined(); 48 | expect(toObject(args.value).roles).toBeDefined(); 49 | } 50 | if (args.key === MIGRATION_STATE_KEY) { 51 | expect(toDate(args.value)).toBeDefined(); 52 | lastUpdateTimeChanged = true; 53 | } 54 | } 55 | 56 | return undefined; 57 | }; 58 | 59 | const migrationsDir = path.join(__dirname, '/../src/chaincodes/contours/migrations'); 60 | const txHelper = new TransactionHelper(new ChaincodeStub(chainCodeStubMiddleware)); 61 | const result = await runMigrations(migrationsDir, null, new ChaincodeStub(chainCodeStubMiddleware), txHelper, []); 62 | expect(result).toEqual(['Version-20171122161550.js']); 63 | 64 | expect(putStateTimesCalled).toBe(Object.values(CONSTANTS.CONTOUR_TYPES).length); 65 | // Migration state updates 66 | expect(lastUpdateTimeChanged).toBeTruthy(); 67 | }); 68 | */ 69 | -------------------------------------------------------------------------------- /src/utils/identity.js: -------------------------------------------------------------------------------- 1 | 2 | const _ = require('lodash'); // eslint-disable-line 3 | const {X509} = require('jsrsasign'); 4 | const {sha3_256} = require('js-sha3'); // eslint-disable-line 5 | 6 | const logger = require('./logger').getLogger('utils/identity'); 7 | 8 | const ChaincodeError = require('./../lib/ChaincodeError'); 9 | const ERRORS = require('./../constants/errors'); 10 | 11 | const normalizeX509PEM = function(raw) { 12 | logger.debug(`[normalizeX509]raw cert: ${raw}`); 13 | 14 | const regex = /(-----\s*BEGIN ?[^-]+?-----)([\s\S]*)(-----\s*END ?[^-]+?-----)/; 15 | let matches = raw.match(regex); 16 | 17 | if (!matches || matches.length !== 4) { 18 | 19 | throw new ChaincodeError(ERRORS.INVALID_CERTIFICATE, { 20 | 'cert': raw 21 | }); 22 | } 23 | 24 | // remove the first element that is the whole match 25 | matches.shift(); 26 | // remove LF or CR 27 | matches = matches.map((element) => { 28 | return element.trim(); 29 | }); 30 | 31 | // make sure '-----BEGIN CERTIFICATE-----' and '-----END CERTIFICATE-----' are in their own lines 32 | // and that it ends in a new line 33 | return `${matches.join('\n')}\n`; 34 | }; 35 | 36 | const getCertificateFromPEM = function(pem) { 37 | const normalizedPEM = normalizeX509PEM(pem); 38 | 39 | const cert = new X509(); 40 | cert.readCertPEM(normalizedPEM); 41 | 42 | return cert; 43 | }; 44 | 45 | const getCertificateFromStub = function(stub) { 46 | 47 | return getCertificateFromPEM(getPEMFromStub(stub)); 48 | }; 49 | 50 | const getPublicKeyHashFromPEM = function(pem) { 51 | const cert = getCertificateFromPEM(pem); 52 | const publicKey = cert.getPublicKeyHex(); 53 | const publicKeyHash = sha3_256(publicKey); 54 | 55 | logger.info(`Public key: ${publicKey}`); 56 | logger.info(`Public key Hash: ${publicKeyHash}`); 57 | 58 | return publicKeyHash; 59 | }; 60 | 61 | const getPublicKeyHashFromStub = function(stub) { 62 | 63 | return getPublicKeyHashFromPEM(getPEMFromStub(stub)); 64 | }; 65 | 66 | const validatePublicKeyHash = function(hash) { 67 | if (!_.isString(hash)) { 68 | 69 | return false; 70 | } 71 | 72 | // sha3_256 = 32 bytes long 73 | if (hash.length !== 64) { 74 | 75 | return false; 76 | } 77 | 78 | // check for hexadecimal signs 79 | if (!/^(\d|[A-F]|[a-f])+$/.test(hash)) { 80 | 81 | return false; 82 | } 83 | 84 | return true; 85 | }; 86 | 87 | module.exports = { 88 | normalizeX509PEM, 89 | 90 | validatePublicKeyHash, 91 | 92 | getCertificateFromStub, 93 | getCertificateFromPEM, 94 | 95 | getPublicKeyHashFromStub, 96 | getPublicKeyHashFromPEM 97 | }; 98 | 99 | function getPEMFromStub(stub) { 100 | const signingId = stub.getCreator(); 101 | const idBytes = signingId.getIdBytes().toBuffer(); 102 | 103 | return idBytes.toString(); 104 | } 105 | -------------------------------------------------------------------------------- /src/utils/db.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); // eslint-disable-line 2 | const logger = require('./logger').getLogger('utils/db'); 3 | const normalizePayload = require('./normalizePayload'); 4 | const {toDate: timestampToDate} = require('./timestamp'); 5 | 6 | const serialize = (value) => { 7 | if (_.isDate(value) || _.isString(value)) { 8 | 9 | return Buffer.from(normalizePayload(value).toString()); 10 | } 11 | 12 | return Buffer.from(JSON.stringify(normalizePayload(value))); 13 | }; 14 | 15 | const toObject = (buffer) => { 16 | if (buffer == null) { 17 | 18 | return undefined; 19 | } 20 | 21 | const bufferString = buffer.toString(); 22 | if (bufferString.length <= 0) { 23 | 24 | return undefined; 25 | } 26 | 27 | return JSON.parse(bufferString); 28 | }; 29 | 30 | const toDate = (buffer) => { 31 | if (buffer == null) { 32 | 33 | return undefined; 34 | } 35 | 36 | const bufferString = buffer.toString(); 37 | if (bufferString.length <= 0) { 38 | 39 | return undefined; 40 | } 41 | 42 | if (/\d+/g.test(bufferString)) { 43 | 44 | return new Date(parseInt(bufferString, 10)); 45 | } 46 | 47 | return undefined; 48 | }; 49 | 50 | const toString = (buffer) => { 51 | if (buffer == null) { 52 | 53 | return null; 54 | } 55 | 56 | return buffer.toString(); 57 | }; 58 | 59 | /** 60 | * Creates an array of objects from the query iterator. 61 | * Each object has two keys: 62 | * - key: the key of the object in the database 63 | * - record: the value associated to that key in the database, 64 | * according to the query 65 | * @param {StateQueryIterator} iterator the query iterator 66 | * @return {Array[Object]} an array with the result of the query 67 | */ 68 | const iteratorToList = async function iteratorToList(iterator) { 69 | const allResults = []; 70 | let res; 71 | while (res == null || !res.done) { 72 | res = await iterator.next(); 73 | if (res.value && res.value.value.toString()) { 74 | const jsonRes = {}; 75 | logger.debug(res.value.value.toString('utf8')); 76 | 77 | jsonRes.key = res.value.key; 78 | try { 79 | jsonRes.record = JSON.parse(res.value.value.toString('utf8')); 80 | } catch (err) { 81 | logger.debug(err); 82 | jsonRes.record = res.value.value.toString('utf8'); 83 | } 84 | 85 | if (res.value.timestamp) { 86 | jsonRes.lastModifiedOn = timestampToDate(res.value.timestamp); 87 | } 88 | 89 | allResults.push(jsonRes); 90 | } 91 | } 92 | 93 | logger.debug('end of data'); 94 | await iterator.close(); 95 | logger.info(JSON.stringify(allResults)); 96 | 97 | return allResults; 98 | }; 99 | 100 | module.exports = { 101 | iteratorToList, 102 | toObject, 103 | toDate, 104 | toString, 105 | serialize 106 | }; 107 | -------------------------------------------------------------------------------- /src/utils/migrations.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const logger = require('../utils/logger').getLogger('migrations/runMigrations'); 4 | 5 | const MIGRATION_FILE_REGEX = /^Version-([0-9]+)\.js/i; 6 | const MIGRATION_STATE_KEY = 'last-update-time'; 7 | 8 | async function runMigrations(migrationsDir, contract, stub, txHelper, args) { 9 | const lastUpdateTime = await txHelper.getStateAsDate(MIGRATION_STATE_KEY); 10 | 11 | const files = await loadFiles(migrationsDir); 12 | const migrationFiles = getMigrationFiles(files, lastUpdateTime); 13 | 14 | if (migrationFiles.length === 0) { 15 | 16 | return 'No migrations to execute'; 17 | } 18 | 19 | for (const file of migrationFiles) { 20 | const migrate = require(path.join(migrationsDir, file)); 21 | logger.info(`Running migration for file ${file}`); 22 | await migrate(contract, stub, txHelper, args); 23 | } 24 | 25 | txHelper.putState(MIGRATION_STATE_KEY, txHelper.getTxDate()); 26 | 27 | return migrationFiles; 28 | } 29 | 30 | module.exports = { 31 | MIGRATION_STATE_KEY, 32 | MIGRATION_FILE_REGEX, 33 | runMigrations 34 | }; 35 | 36 | function getMigrationFiles(files, lastUpdateTime) { 37 | const versionFiles = sortMigrationFiles(files.filter((file) => MIGRATION_FILE_REGEX.test(file))); 38 | const migrationsToRun = []; 39 | for (const versionFile of versionFiles) { 40 | const dateMigrationFile = getDateTime(versionFile); 41 | 42 | if (!lastUpdateTime || dateMigrationFile > lastUpdateTime) { 43 | migrationsToRun.push(versionFile); 44 | } 45 | } 46 | 47 | return migrationsToRun; 48 | } 49 | 50 | function loadFiles(migrationsDir) { 51 | return new Promise((resolve, reject) => { 52 | fs.stat(migrationsDir, (statErr, stats) => { 53 | if (statErr || !stats.isDirectory()) { 54 | resolve([]); 55 | 56 | return; 57 | } 58 | fs.readdir(migrationsDir, (readErr, files) => { 59 | if (readErr) { 60 | reject(new Error('Failed to read migrations directory')); 61 | 62 | return; 63 | } 64 | resolve(files); 65 | }); 66 | }); 67 | }); 68 | } 69 | 70 | function getDateTime(fileName) { 71 | const timeStamp = fileName.match(MIGRATION_FILE_REGEX)[1]; 72 | const year = timeStamp.substr(0, 4); 73 | // The argument month is 0-based. This means that January = 0 and December = 11 74 | const month = timeStamp.substr(4, 2) - 1; 75 | const day = timeStamp.substr(6, 2); 76 | const hour = timeStamp.substr(8, 2); 77 | const min = timeStamp.substr(10, 2); 78 | const sec = timeStamp.substr(12, 2); 79 | return new Date(year, month, day, hour, min, sec); 80 | } 81 | 82 | function compare(a, b) { 83 | const dateA = getDateTime(a); 84 | const dateB = getDateTime(b); 85 | 86 | if (dateA < dateB) { 87 | return -1; 88 | } 89 | if (dateA > dateB) { 90 | return 1; 91 | } 92 | return 0; 93 | } 94 | 95 | function sortMigrationFiles(files) { 96 | return files.sort(compare); 97 | } 98 | -------------------------------------------------------------------------------- /src/lib/ChaincodeBase.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | 3 | const ChaincodeError = require('./ChaincodeError'); 4 | const TransactionHelper = require('./TransactionHelper'); 5 | 6 | const migrations = require('./../utils/migrations'); 7 | const loggerUtils = require('./../utils/logger'); 8 | const normalizePayload = require('./../utils/normalizePayload'); 9 | 10 | const ERRORS = require('./../constants/errors'); 11 | 12 | class ChaincodeBase { 13 | 14 | constructor(shim) { 15 | this.shim = shim; 16 | this.migrating = false; 17 | this.logger = loggerUtils.getLogger(`chaincode/${this.name}`); 18 | } 19 | 20 | /** 21 | * @return the name of the current chaincode. 22 | */ 23 | get name() { 24 | 25 | return this.constructor.name; 26 | } 27 | 28 | /** 29 | * @return the path where the migrations can be found for the current chaincode. 30 | */ 31 | get migrationsPath() { 32 | 33 | throw new ChaincodeError(ERRORS.MIGRATION_PATH_NOT_DEFINED); 34 | } 35 | 36 | /** 37 | * @return the transaction helper for the given stub. This can be used to extend 38 | * the Default TransactionHelper with extra functionality and return your own instance. 39 | */ 40 | getTransactionHelperFor(stub) { 41 | 42 | return new TransactionHelper(stub); 43 | } 44 | 45 | /** 46 | * @param {Array} params 47 | * @returns the parsed parameters 48 | */ 49 | parseParameters(params) { 50 | const parsedParams = []; 51 | 52 | params.forEach((param) => { 53 | try { 54 | // try to parse ... 55 | parsedParams.push(JSON.parse(param)); 56 | } catch (err) { 57 | // if it fails fall back to original param 58 | this.logger.error(`failed to parse param ${param}`); 59 | parsedParams.push(param); 60 | } 61 | }); 62 | 63 | return parsedParams; 64 | } 65 | 66 | /** 67 | * Called when Instantiating chaincode 68 | */ 69 | async Init() { 70 | this.logger.info(`=========== Instantiated Chaincode ${this.name} ===========`); 71 | 72 | return this.shim.success(); 73 | } 74 | 75 | /** 76 | * Basic implementation that redirects Invocations to the right functions on this instance 77 | */ 78 | async Invoke(stub) { 79 | try { 80 | this.logger.info(`=========== Invoked Chaincode ${this.name} ===========`); 81 | this.logger.info(`Transaction ID: ${stub.getTxID()}`); 82 | this.logger.info(util.format('Args: %j', stub.getArgs())); 83 | 84 | const ret = stub.getFunctionAndParameters(); 85 | this.logger.info(ret); 86 | 87 | const method = this[ret.fcn]; 88 | if (!method) { 89 | this.logger.error(`Unknown function ${ret.fcn}.`); 90 | 91 | return this.shim.error(new ChaincodeError(ERRORS.UNKNOWN_FUNCTION, { 92 | 'fn': ret.fcn 93 | }).serialized); 94 | } 95 | 96 | let parsedParameters; 97 | try { 98 | parsedParameters = this.parseParameters(ret.params); 99 | } catch (err) { 100 | throw new ChaincodeError(ERRORS.PARSING_PARAMETERS_ERROR, { 101 | 'message': err.message 102 | }); 103 | } 104 | 105 | let payload = await method.call(this, stub, this.getTransactionHelperFor(stub), ...parsedParameters); 106 | 107 | if (!Buffer.isBuffer(payload)) { 108 | payload = Buffer.from(payload ? JSON.stringify(normalizePayload(payload)) : ''); 109 | } 110 | 111 | return this.shim.success(payload); 112 | } catch (err) { 113 | let error = err; 114 | 115 | const stacktrace = err.stack; 116 | 117 | if (!(err instanceof ChaincodeError)) { 118 | error = new ChaincodeError(ERRORS.UNKNOWN_ERROR, { 119 | 'message': err.message 120 | }); 121 | } 122 | this.logger.error(stacktrace); 123 | this.logger.error(`Data of error ${err.message}: ${JSON.stringify(err.data)}`); 124 | 125 | return this.shim.error(error.serialized); 126 | } 127 | } 128 | 129 | /** 130 | * Run Migrations for the current chaincode. 131 | * 132 | * @param {Stub} stub 133 | * @param {TransactionHelper} txHelper 134 | * @param {Array} args 135 | */ 136 | async runMigrations(stub, txHelper, ...args) { 137 | this.migrating = true; 138 | const result = await migrations.runMigrations(this.migrationsPath, this, stub, txHelper, args); 139 | this.migrating = false; 140 | 141 | return result; 142 | } 143 | 144 | /** 145 | * Returns 'pong' when everything is correct. 146 | */ 147 | async ping() { 148 | 149 | return 'pong'; 150 | } 151 | 152 | } 153 | 154 | module.exports = ChaincodeBase; 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hyperledger Fabric Node.js Chaincode utils [![npm version](https://badge.fury.io/js/%40kunstmaan%2Fhyperledger-fabric-node-chaincode-utils.svg)](https://badge.fury.io/js/%40kunstmaan%2Fhyperledger-fabric-node-chaincode-utils) [![Build Status](https://travis-ci.org/Kunstmaan/hyperledger-fabric-node-chaincode-utils.svg?branch=master)](https://travis-ci.org/Kunstmaan/hyperledger-fabric-node-chaincode-utils) 2 | 3 | This repository consists out of a set of utilities functions which can be used to create Node.js chaincode on a Fabric blockchain network. Node.js chaincode is only supported since Hyperledger Fabric 1.1.0. 4 | 5 | ## API 6 | 7 | This Library exposes 2 main classes and some useful utilities. 8 | 9 | ### ChaincodeBase 10 | 11 | ChaincodeBase is a super class that can be used for all your Node.js chaincode. It has a default implementation for `Invoke` that catches the transaction and redirect it to the right function with parameter on the Chaincode class. It also reads the respond from the function and wraps it into a `shim.error()` when an error was thrown or a `shim.success()` when a regular object was returned from the function. By extending this class you don't need to worry anymore about the Chaincode details. 12 | 13 | An instance of `fabric-shim` needs to be passed as an argument on the constructor. This is required to ensure that both the chaincode as the `ChaincodeBase` use the same version. 14 | To ensure compatibility `fabric-shim` is set as a peer dependency of this package. 15 | 16 | ```javascript 17 | const shim = require('fabric-shim'); 18 | 19 | const FooChaincode = class extends ChaincodeBase { 20 | 21 | constructor() { 22 | super(shim); 23 | } 24 | 25 | async yourFunction(stub, txHelper, param1, param2) { 26 | 27 | this.logger.info('execution of yourFunction!'); 28 | 29 | return { 30 | 'foo': 'bar' 31 | }; 32 | } 33 | 34 | } 35 | 36 | shim.start(new FooChaincode()); 37 | ``` 38 | 39 | when a function is called the stub is given together with the params. But also a txHelper is provided, a wrapper around the stub with a lot of helpful functions (see TransactionHelper). 40 | 41 | ChaincodeBase has a default logger that you can use which is prefixed with the ChaincodeName (the classname in the case of the example 'chaincode/FooChaincode'). 42 | 43 | Furthermore the ChaincodBase class exposes some default behaviour for your Chaincode: 44 | 45 | #### Ping 46 | 47 | A simple function that you can use to ping your chaincode, by using this you can see that the chaincode is working properly and is accessible. When everything is ok this chaincode function should return 'pong'. 48 | 49 | #### Migrations 50 | 51 | There is a migration system build in that will read the migration files `/migrations/Version-yyyyMMddhhmmss.js` where `yyyyMMddhhmmss` stands for the date the migration was made. [hyperledger-fabric-chaincode-dev-setup](https://github.com/Kunstmaan/hyperledger-fabric-chaincode-dev-setup) has a good helper command build in for creating this migration files. 52 | 53 | When you trigger the migrations all migration files since the last execution of the command will be executed. The date of the last execution is stored in the state db. 54 | 55 | A migration file needs expose a function like this: 56 | 57 | ```javascript 58 | module.exports = async function migrate(contract, stub, txHelper, args) { 59 | // migrate your data here ... 60 | }; 61 | ``` 62 | 63 | ### TransactionHelper 64 | 65 | The TransactionHelper is a wrapper class around the stub class of Hyperledger Fabric. That has some helper functions build in and some wrappers around functions from the stub class. 66 | 67 | ```javascript 68 | const txHelper = new TransactionHelper(stub) 69 | ``` 70 | 71 | #### UUID 72 | 73 | ```javascript 74 | txHelper.uuid('FOO'); 75 | -> FOO_5b8460e25e1892ceaf658b3e41d06a9933831806cbbd5fc49ccfbccda4d8bbaa_0 76 | txHelper.uuid('FOO'); 77 | -> FOO_5b8460e25e1892ceaf658b3e41d06a9933831806cbbd5fc49ccfbccda4d8bbaa_1 78 | ``` 79 | 80 | This function will return a unique key that can be used to store something to the state database. The key will exists out of the prefix, the transaction id and a sequence number. All the DBKeys generated within the same Transaction will have the same txid. 81 | 82 | #### Invoke chaincode 83 | 84 | ```javascript 85 | invokeChaincode(chaincodeName, functionName, args = undefined, channel = undefined) 86 | ``` 87 | 88 | A helper function around the invokeChaincode of the stub that will throw a ChaincodeError if the invocation failed or return the parsed result when it succeeded. 89 | 90 | ### Invoked by chaincode 91 | 92 | ```javascript 93 | invokedByChaincode(chaincodeName, functionName = undefined) 94 | ``` 95 | 96 | Check if the current invocation is invoked from another chaincode. It's also possible to check if it was invoked from a particular function within that chaincode. 97 | 98 | ### Execute query and return list 99 | 100 | ```javascript 101 | getQueryResultAsList(query) 102 | ``` 103 | 104 | Execute the given query on the state db and return the results as an array containing of objects `[{key: '', record: {}}]`. 105 | 106 | #### Delete results returned by query 107 | 108 | ```javascript 109 | deleteAllReturnedByQuery(query) 110 | ``` 111 | 112 | Delete all records returned by that query. 113 | 114 | #### GET/PUT State 115 | 116 | ```javascript 117 | putState(key, value) 118 | getStateAsObject(key) 119 | getStateAsString(key) 120 | getStateAsDate(key) 121 | ``` 122 | 123 | Helper functions to put and get state from the database, that will handle the serialisation/deserialisation of the values. 124 | 125 | #### Transaction Date 126 | 127 | ```javascript 128 | getTxDate() 129 | ``` 130 | 131 | Return the Date of the transaction as a [Javascript Date Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date). 132 | 133 | #### Creator Public Key 134 | 135 | ```javascript 136 | getCreatorPublicKey(); 137 | ``` 138 | 139 | Returns the Public Key from the Transaction creator as a SHA3 256 Hash. 140 | 141 | #### Publish Event 142 | 143 | ```javascript 144 | setEvent(name, payload) 145 | ``` 146 | 147 | Wraps the payload into a Buffer and then calls setEvent on the `stub`. 148 | 149 | ### Utils 150 | 151 | Exposes some utilities that are used by the ChaincodeBase and can be useful for other parts of the chaincode as well. 152 | 153 | #### Logger 154 | 155 | ```javascript 156 | const {logger} = require('@kunstmaan/hyperledger-fabric-node-chaincode-utils').utils; 157 | const myLogger = logger.getLogger('myLogger'); 158 | myLogger.info('foo'); 159 | ``` 160 | 161 | Can be used to create a log4j logger object that prefixes the logs with a certain name. It reads the `CHAINCODE_LOGGING_LEVEL` environment variable to set the right log level (critical, error, warning, debug). The default value is debug. 162 | 163 | #### Normalize Payload 164 | 165 | ```javascript 166 | const {normalizePayload} = require('@kunstmaan/hyperledger-fabric-node-chaincode-utils').utils; 167 | normalizePayload({'foo': 'bar'}); 168 | ``` 169 | 170 | The function used by invoke to normalize the payload before returning the payload back to the client. This will normalize Javascript Date Objects to a UNIX timestamp. 171 | 172 | #### Identity 173 | 174 | ```javascript 175 | const {identity} = require('@kunstmaan/hyperledger-fabric-node-chaincode-utils').utils; 176 | identity.getPublicKeyHashFromStub(stub); 177 | ``` 178 | 179 | Exposes helper functions to get the public key from the Transaction. 180 | 181 | #### Migrations 182 | 183 | ```javascript 184 | const {migrations} = require('@kunstmaan/hyperledger-fabric-node-chaincode-utils').utils; 185 | console.log(migrations.MIGRATION_STATE_KEY); 186 | migrations.runMigrations(migrationsDir, contract, stub, txHelper, args); 187 | ``` 188 | 189 | Exposes the function used to run the migrations. It also exposes the key `MIGRATION_STATE_KEY` on which the date is stored when the last migrations where run. 190 | 191 | #### Db 192 | 193 | ```javascript 194 | const {db} = require('@kunstmaan/hyperledger-fabric-node-chaincode-utils').utils; 195 | // Converts a db query result into an array of objects 196 | db.iteratorToList(queryIterator); 197 | ``` 198 | -------------------------------------------------------------------------------- /src/lib/TransactionHelper.js: -------------------------------------------------------------------------------- 1 | const grpc = require('grpc'); 2 | const path = require('path'); 3 | const _ = require('lodash'); 4 | 5 | const ChaincodeError = require('./ChaincodeError'); 6 | 7 | const ERRORS = require('./../constants/errors'); 8 | 9 | const loggerUtils = require('./../utils/logger'); 10 | const dbUtils = require('./../utils/db'); 11 | const identityUtils = require('./../utils/identity'); 12 | const parseErrorMessage = require('./../utils/parseErrorMessage'); 13 | const {toDate} = require('./../utils/timestamp'); 14 | 15 | const chaincodeProto = grpc.load({ 16 | root: path.join(process.cwd(), 'node_modules/fabric-shim/lib/protos'), 17 | file: 'peer/chaincode.proto' 18 | }).protos; 19 | 20 | // Keep track of sequence number 21 | // This needs to be a global variable as every time a 22 | // new chaincode is invoked this should not be resetted 23 | // for example if this chaincode is invoked multiple times from 24 | // within another chaincode. 25 | let cachedIdSequences = {}; 26 | const ID_SEQUENCE_TTL = (30 * 60 * 1000); 27 | 28 | const TransactionHelper = class { 29 | 30 | constructor(stub) { 31 | this.stub = stub; 32 | this.logger = loggerUtils.getLogger('lib/TransactionHelper'); 33 | } 34 | 35 | /** 36 | * Generate a UUID for the given prefix. 37 | * 38 | * @param {String} prefix 39 | */ 40 | uuid(prefix) { 41 | validateRequiredString({prefix}); 42 | 43 | const txId = this.stub.getTxID(); 44 | const txTimestamp = this.getTxDate().getTime(); 45 | 46 | cachedIdSequences[prefix] = cachedIdSequences[prefix] || {}; 47 | const latestId = cachedIdSequences[prefix][txId] != null ? cachedIdSequences[prefix][txId] : { 48 | 'value': -1 49 | }; 50 | 51 | latestId.value += 1; 52 | latestId.lastUsed = txTimestamp; 53 | 54 | cachedIdSequences[prefix][txId] = latestId; 55 | 56 | // clean up old transaction ids; 57 | const cleanedIdSequences = {}; 58 | _.each(cachedIdSequences, (txs, p) => { 59 | _.each(txs, (id, t) => { 60 | if (id.lastUsed > txTimestamp - ID_SEQUENCE_TTL) { 61 | cleanedIdSequences[p] = cleanedIdSequences[p] || {}; 62 | cleanedIdSequences[p][t] = id; 63 | } 64 | }); 65 | }); 66 | 67 | cachedIdSequences = cleanedIdSequences; 68 | 69 | return `${prefix}_${txId}_${latestId.value}`; 70 | } 71 | 72 | /** 73 | * A helper function around the invokeChaincode of the stub that will throw a ChaincodeError 74 | * if the invocation failed or return the parsed result when it succeeded. 75 | * 76 | * @param {String} chaincodeName 77 | * @param {String} functionName 78 | * @param {Array} args 79 | * @param {String} channel 80 | */ 81 | async invokeChaincode(chaincodeName, functionName, args = undefined, channel = undefined) { 82 | validateRequiredString({chaincodeName}); 83 | validateRequiredString({functionName}); 84 | 85 | let invokeArgs = [functionName]; 86 | if (_.isArray(args)) { 87 | invokeArgs = invokeArgs.concat(args.map((a) => { 88 | if (!_.isString(a)) { 89 | 90 | return JSON.stringify(a); 91 | } 92 | 93 | return a; 94 | })); 95 | } 96 | 97 | return new Promise((fulfill, reject) => { 98 | // do this in a timeout to make sure the txId 99 | // is released when another chaincode is invoked before. 100 | // @ref https://jira.hyperledger.org/browse/FAB-7437 101 | setTimeout(async () => { 102 | const invokeChannel = channel || this.stub.getChannelID(); 103 | 104 | try { 105 | const invokeResult = await this.stub.invokeChaincode(chaincodeName, invokeArgs, invokeChannel); 106 | 107 | if (invokeResult == null || invokeResult.status !== 200) { 108 | 109 | throw new ChaincodeError(ERRORS.CHAINCODE_INVOKE_ERROR, { 110 | 'chaincodeName': chaincodeName, 111 | 'args': invokeArgs, 112 | 'channel': invokeChannel, 113 | 'status': invokeResult ? invokeResult.status : undefined, 114 | 'payload': invokeResult ? invokeResult.payload : undefined 115 | }); 116 | } 117 | 118 | fulfill(JSON.parse(invokeResult.payload.toString('utf8'))); 119 | } catch (error) { 120 | let ccError; 121 | 122 | if (error instanceof ChaincodeError) { 123 | ccError = error; 124 | } else { 125 | this.logger.error(`Error while calling ${chaincodeName} with args ${args} on channel ${invokeChannel}`); 126 | 127 | const errorData = parseErrorMessage(error.message); 128 | if (_.isUndefined(errorData.key)) { 129 | ccError = new ChaincodeError(ERRORS.CHAINCODE_INVOKE_ERROR, {'message': error.message}, error.stack); 130 | } else { 131 | ccError = new ChaincodeError(errorData.key, errorData.data, errorData.stack); 132 | } 133 | } 134 | reject(ccError); 135 | } 136 | }, 100); 137 | }); 138 | } 139 | 140 | /** 141 | * This function checks if the current chaincode is invoked by another chaincode. 142 | * 143 | * @param {String} chaincodeName the name of the chaincode 144 | * @param {String} functionName the name of the function. If undefined, will be ignored 145 | * 146 | * @return true if this function is called from the chaincode and function given. 147 | * if func is undefined, will ignore the function. 148 | */ 149 | invokedByChaincode(chaincodeName, functionName = undefined) { 150 | validateRequiredString({chaincodeName}); 151 | 152 | validate({functionName}, (value) => { 153 | return _.isUndefined(value) || _.isString(value); 154 | }, 'string'); 155 | 156 | const {proposal} = this.stub.getSignedProposal(); 157 | const input = chaincodeProto.ChaincodeInput.decode(_.clone(proposal.payload.input)); 158 | const args = input.args.map((entry) => { 159 | 160 | return entry.toBuffer().toString('utf8'); 161 | }); 162 | 163 | this.logger.debug(`Chaincode parent args: ${args}`); 164 | const idxOfCC = args[0].indexOf(chaincodeName); 165 | const idxOfFunc = args[0].indexOf(functionName); 166 | 167 | if (_.isUndefined(functionName)) { 168 | 169 | return idxOfCC > -1; 170 | } 171 | 172 | return idxOfCC < idxOfFunc && idxOfCC > -1; 173 | } 174 | 175 | /** 176 | * Query the state and return a list of results. 177 | * 178 | * @param {Object} query 179 | * 180 | * @return a list of objects in the following format 181 | * { 182 | * key: [String] 183 | * record: [Object] 184 | * } 185 | */ 186 | async getQueryResultAsList(query) { 187 | validateQuery(query); 188 | 189 | const queryString = JSON.stringify(query); 190 | this.logger.debug(`Query: ${queryString}`); 191 | const iterator = await this.stub.getQueryResult(queryString); 192 | 193 | return dbUtils.iteratorToList(iterator); 194 | } 195 | 196 | /** 197 | * Deletes all objects returned by the query 198 | * @param {Object} query the query 199 | */ 200 | async deleteAllReturnedByQuery(query) { 201 | validateQuery(query); 202 | 203 | const allResults = await this.getQueryResultAsList(query); 204 | 205 | return Promise.all(allResults.map((record) => this.stub.deleteState(record.key))); 206 | } 207 | 208 | /** 209 | * Serializes the value and store it on the state db. 210 | * 211 | * @param {String} key 212 | */ 213 | async putState(key, value) { 214 | validateRequiredString({key}); 215 | 216 | return this.stub.putState(key, dbUtils.serialize(value)); 217 | } 218 | 219 | /** 220 | * @param {String} key 221 | * 222 | * @return the state for the given key parsed as an Object 223 | */ 224 | async getStateAsObject(key) { 225 | validateRequiredString({key}); 226 | 227 | const rawValue = await this.stub.getState(key); 228 | 229 | return dbUtils.toObject(rawValue); 230 | } 231 | 232 | /** 233 | * @param {String} key 234 | * 235 | * @return the state for the given key parsed as a String 236 | */ 237 | async getStateAsString(key) { 238 | validateRequiredString({key}); 239 | 240 | const rawValue = await this.stub.getState(key); 241 | 242 | return dbUtils.toString(rawValue); 243 | } 244 | 245 | /** 246 | * @param {String} key 247 | * 248 | * @return the state for the given key parsed as a Date 249 | */ 250 | async getStateAsDate(key) { 251 | validateRequiredString({key}); 252 | 253 | const rawValue = await this.stub.getState(key); 254 | 255 | return dbUtils.toDate(rawValue); 256 | } 257 | 258 | /** 259 | * @return the Transaction date as a Javascript Date Object. 260 | */ 261 | getTxDate() { 262 | const timestamp = this.stub.getTxTimestamp(); 263 | return toDate(timestamp); 264 | } 265 | 266 | /** 267 | * Returns the Certificate from the Transaction creator 268 | */ 269 | getCreatorCertificate() { 270 | 271 | return identityUtils.getCertificateFromStub(this.stub); 272 | } 273 | 274 | /** 275 | * Returns the Public Key from the Transaction creator as a SHA3 256 Hash 276 | */ 277 | getCreatorPublicKey() { 278 | 279 | return identityUtils.getPublicKeyHashFromStub(this.stub); 280 | } 281 | 282 | /** 283 | * Publish an event to the Blockchain 284 | * 285 | * @param {String} name 286 | * @param {Object} payload 287 | */ 288 | setEvent(name, payload) { 289 | let bufferedPayload; 290 | 291 | if (Buffer.isBuffer(payload)) { 292 | bufferedPayload = payload; 293 | } else { 294 | bufferedPayload = Buffer.from(JSON.stringify(payload)); 295 | } 296 | 297 | this.logger.debug(`Setting Event ${name} with payload ${JSON.stringify(payload)}`); 298 | return this.stub.setEvent(name, bufferedPayload); 299 | } 300 | 301 | }; 302 | 303 | module.exports = TransactionHelper; 304 | 305 | function validateRequiredString(params) { 306 | return validate(params, (value) => { 307 | return _.isString(value) && !_.isEmpty(value); 308 | }, 'string'); 309 | } 310 | 311 | function validateQuery(query) { 312 | validate({query}, _.isObject, 'object'); 313 | } 314 | 315 | function validate(params, validator, expected) { 316 | for (const paramName in params) { 317 | if ({}.hasOwnProperty.call(params, paramName)) { 318 | const paramValue = params[paramName]; 319 | 320 | if (!validator(paramValue)) { 321 | 322 | throw new ChaincodeError(ERRORS.TYPE_ERROR, { 323 | 'arg': paramName, 324 | 'value': paramValue, 325 | 'expected': expected 326 | }); 327 | } 328 | } 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/mocks/ChaincodeStub.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The ChaincodeStub is implemented by the fabric-shim 3 | * library and passed to the {@link ChaincodeInterface} calls by the Hyperledger Fabric platform. 4 | * The stub encapsulates the APIs between the chaincode implementation and the Fabric peer 5 | */ 6 | class ChaincodeStub { 7 | 8 | constructor(middleware) { 9 | this.middleware = middleware; 10 | } 11 | 12 | /** 13 | * Returns the arguments as array of strings from the chaincode invocation request. 14 | * Equivalent to [getStringArgs()]{@link ChaincodeStub#getStringArgs} 15 | * @return {string[]} 16 | */ 17 | getArgs() { 18 | return this.middleware('getArgs'); 19 | } 20 | 21 | /** 22 | * Returns the arguments as array of strings from the chaincode invocation request 23 | * @return {string[]} 24 | */ 25 | getStringArgs() { 26 | return this.middleware('getStringArgs'); 27 | } 28 | 29 | /** 30 | * @typedef FunctionAndParameters 31 | * @property {string} fcn The function name, which by chaincode programming convention 32 | * is the first argument in the array of arguments 33 | * @property {string[]} params The rest of the arguments, as array of strings 34 | */ 35 | 36 | /** 37 | * Returns an object containing the chaincode function name to invoke, and the array 38 | * of arguments to pass to the target function 39 | * @return {FunctionAndParameters} 40 | */ 41 | getFunctionAndParameters() { 42 | return this.middleware('getFunctionAndParameters'); 43 | } 44 | 45 | /** 46 | * Returns the transaction ID for the current chaincode invocation request. The transaction 47 | * ID uniquely identifies the transaction within the scope of the channel. 48 | */ 49 | getTxID() { 50 | return this.middleware('getTxID'); 51 | } 52 | 53 | /** 54 | * Returns the channel ID for the proposal for chaincode to process. 55 | * This would be the 'channel_id' of the transaction proposal (see ChannelHeader 56 | * in protos/common/common.proto) except where the chaincode is calling another on 57 | * a different channel. 58 | */ 59 | getChannelID() { 60 | return this.middleware('getChannelID'); 61 | } 62 | 63 | /** 64 | * This object contains the essential identity information of the chaincode invocation's submitter, 65 | * including its organizational affiliation (mspid) and certificate (id_bytes) 66 | * @typedef {Object} ProposalCreator 67 | * @property {string} mspid The unique ID of the Membership Service Provider instance that is associated 68 | * to the identity's organization and is able to perform digital signing and signature verification 69 | */ 70 | 71 | /** 72 | * Returns the identity object of the chaincode invocation's submitter 73 | * @return {ProposalCreator} 74 | */ 75 | getCreator() { 76 | return this.middleware('getCreator'); 77 | } 78 | 79 | /** 80 | * Returns the transient map that can be used by the chaincode but not 81 | * saved in the ledger, such as cryptographic information for encryption and decryption 82 | * @return {Map} 83 | */ 84 | getTransient() { 85 | return this.middleware('getTransient'); 86 | } 87 | 88 | /** 89 | * The SignedProposal object represents the request object sent by the client application 90 | * to the chaincode. 91 | * @typedef {Object} SignedProposal 92 | * @property {Buffer} signature The signature over the proposal. This signature is to be verified against 93 | * the {@link ProposalCreator} returned by getCreator(). The signature will have already been 94 | * verified by the peer before the invocation request reaches the chaincode. 95 | * @property {Proposal} proposal The object containing the chaincode invocation request and metadata about the request 96 | */ 97 | 98 | /** 99 | * The essential content of the chaincode invocation request 100 | * @typedef {Object} Proposal 101 | * @property {Header} header The header object contains metadata describing key aspects of the invocation 102 | * request such as target channel, transaction ID, and submitter identity etc. 103 | * @property {ChaincodeProposalPayload} payload The payload object contains actual content of the invocation request 104 | */ 105 | 106 | /** 107 | * @typedef {Object} Header 108 | * @property {ChannelHeader} channel_header Channel header identifies the destination channel of the invocation 109 | * request and the type of request etc. 110 | * @property {SignatureHeader} signature_header Signature header has replay prevention and message authentication features 111 | */ 112 | 113 | /** 114 | * Channel header identifies the destination channel of the invocation 115 | * request and the type of request etc. 116 | * @typedef {Object} ChannelHeader 117 | * @property {number} type Any of the following: 118 | *
    119 | *
  • MESSAGE = 0; // Used for messages which are signed but opaque 120 | *
  • CONFIG = 1; // Used for messages which express the channel config 121 | *
  • CONFIG_UPDATE = 2; // Used for transactions which update the channel config 122 | *
  • ENDORSER_TRANSACTION = 3; // Used by the SDK to submit endorser based transactions 123 | *
  • ORDERER_TRANSACTION = 4; // Used internally by the orderer for management 124 | *
  • DELIVER_SEEK_INFO = 5; // Used as the type for Envelope messages submitted to instruct the Deliver API to seek 125 | *
  • CHAINCODE_PACKAGE = 6; // Used for packaging chaincode artifacts for install 126 | *
127 | * @property {number} version 128 | * @property {google.protobuf.Timestamp} timestamp The local time when the message was created by the submitter 129 | * @property {string} channel_id Identifier of the channel that this message bound for 130 | * @property {string} tx_id Unique identifier used to track the transaction throughout the proposal endorsement, ordering, 131 | * validation and committing to the ledger 132 | * @property {number} epoch 133 | */ 134 | 135 | /** 136 | * @typedef {Object} SignatureHeader 137 | * @property {ProposalCreator} creator The submitter of the chaincode invocation request 138 | * @property {Buffer} nonce Arbitrary number that may only be used once. Can be used to detect replay attacks. 139 | */ 140 | 141 | /** 142 | * @typedef {Object} ChaincodeProposalPayload 143 | * @property {Buffer} input Input contains the arguments for this invocation. If this invocation 144 | * deploys a new chaincode, ESCC/VSCC are part of this field. This is usually a marshaled ChaincodeInvocationSpec 145 | * @property {Map} transientMap TransientMap contains data (e.g. cryptographic material) that might be used 146 | * to implement some form of application-level confidentiality. The contents of this field are supposed to always 147 | * be omitted from the transaction and excluded from the ledger. 148 | */ 149 | 150 | /** 151 | * Returns a fully decoded object of the signed transaction proposal 152 | * @return {SignedProposal} 153 | */ 154 | getSignedProposal() { 155 | return this.middleware('getSignedProposal'); 156 | } 157 | 158 | /** 159 | * Returns the timestamp when the transaction was created. This 160 | * is taken from the transaction {@link ChannelHeader}, therefore it will indicate the 161 | * client's timestamp, and will have the same value across all endorsers. 162 | */ 163 | getTxTimestamp() { 164 | return this.middleware('getTxTimestamp'); 165 | } 166 | 167 | /** 168 | * Returns a HEX-encoded string of SHA256 hash of the transaction's nonce, creator and epoch concatenated, as a 169 | * unique representation of the specific transaction. This value can be used to prevent replay attacks in chaincodes 170 | * that need to authenticate an identity independent of the transaction's submitter. In a chaincode proposal, the 171 | * submitter will have been authenticated by the peer such that the identity returned by 172 | * [stub.getCreator()]{@link ChaincodeStub#getCreator} can be trusted. But in some scenarios, the chaincode needs 173 | * to authenticate an identity independent of the proposal submitter.

174 | * 175 | * For example, Alice is the administrator who installs and instantiates a chaincode that manages assets. During 176 | * instantiate Alice assigns the initial owner of the asset to Bob. The chaincode has a function called 177 | * transfer() that moves the asset to another identity by changing the asset's "owner" property to the 178 | * identity receiving the asset. Naturally only Bob, the current owner, is supposed to be able to call that function. 179 | * While the chaincode can rely on stub.getCreator() to check the submitter's identity and compare that with the 180 | * current owner, sometimes it's not always possible for the asset owner itself to submit the transaction. Let's suppose 181 | * Bob hires a broker agency to handle his trades. The agency participates in the blockchain network and carry out trades 182 | * on behalf of Bob. The chaincode must have a way to authenticate the transaction to ensure it has Bob's authorization 183 | * to do the asset transfer. This can be achieved by asking Bob to sign the message, so that the chaincode can use 184 | * Bob's certificate, which was obtained during the chaincode instantiate, to verify the signature and thus ensure 185 | * the trade was authorized by Bob.

186 | * 187 | * Now, to prevent Bob's signature from being re-used in a malicious attack, we want to ensure the signature is unique. 188 | * This is where the binding concept comes in. As explained above, the binding string uniquely represents 189 | * the transaction where the trade proposal and Bob's authorization is submitted in. As long as Bob's signature is over 190 | * the proposal payload and the binding string concatenated together, namely sigma=Sign(BobSigningKey, tx.Payload||tx.Binding), 191 | * it's guaranteed to be unique and can not be re-used in a different transaction for exploitation.

192 | * 193 | * @return {string} A HEX-encoded string of SHA256 hash of the transaction's nonce, creator and epoch concatenated 194 | */ 195 | getBinding() { 196 | return this.middleware('getBinding'); 197 | } 198 | 199 | /** 200 | * Retrieves the current value of the state variable key 201 | * @async 202 | * @param {string} key State variable key to retrieve from the state store 203 | * @return {Promise} Promise for the current value of the state variable 204 | */ 205 | async getState(key) { 206 | return this.middleware('getState', {key}); 207 | } 208 | 209 | /** 210 | * Writes the state variable key of value value 211 | * to the state store. If the variable already exists, the value will be 212 | * overwritten. 213 | * @async 214 | * @param {string} key State variable key to set the value for 215 | * @param {byte[]} value State variable value 216 | * @return {Promise} Promise will be resolved when the peer has successfully handled the state update request 217 | * or rejected if any errors 218 | */ 219 | async putState(key, value) { 220 | return this.middleware('putState', {key, value}); 221 | } 222 | 223 | /** 224 | * Deletes the state variable key from the state store. 225 | * @async 226 | * @param {string} key State variable key to delete from the state store 227 | * @return {Promise} Promise will be resolved when the peer has successfully handled the state delete request 228 | * or rejected if any errors 229 | */ 230 | async deleteState(key) { 231 | return this.middleware('deleteState', {key}); 232 | } 233 | 234 | /** 235 | * Returns a range iterator over a set of keys in the 236 | * ledger. The iterator can be used to iterate over all keys 237 | * between the startKey (inclusive) and endKey (exclusive). 238 | * The keys are returned by the iterator in lexical order. Note 239 | * that startKey and endKey can be empty string, which implies unbounded range 240 | * query on start or end.

241 | * Call close() on the returned {@link StateQueryIterator} object when done. 242 | * The query is re-executed during validation phase to ensure result set 243 | * has not changed since transaction endorsement (phantom reads detected). 244 | * @async 245 | * @param {string} startKey State variable key as the start of the key range (inclusive) 246 | * @param {string} endKey State variable key as the end of the key range (exclusive) 247 | * @return {Promise} Promise for a {@link StateQueryIterator} object 248 | */ 249 | async getStateByRange(startKey, endKey) { 250 | return this.middleware('getStateByRange', {startKey, endKey}); 251 | } 252 | 253 | /** 254 | * Performs a "rich" query against a state database. It is 255 | * only supported for state databases that support rich query, 256 | * e.g. CouchDB. The query string is in the native syntax 257 | * of the underlying state database. An {@link StateQueryIterator} is returned 258 | * which can be used to iterate (next) over the query result set.

259 | * The query is NOT re-executed during validation phase, phantom reads are 260 | * not detected. That is, other committed transactions may have added, 261 | * updated, or removed keys that impact the result set, and this would not 262 | * be detected at validation/commit time. Applications susceptible to this 263 | * should therefore not use GetQueryResult as part of transactions that update 264 | * ledger, and should limit use to read-only chaincode operations. 265 | * @async 266 | * @param {string} query Query string native to the underlying state database 267 | * @return {Promise} Promise for a {@link StateQueryIterator} object 268 | */ 269 | async getQueryResult(query) { 270 | return this.middleware('getQueryResult', {query}); 271 | } 272 | 273 | /** 274 | * Returns a history of key values across time. 275 | * For each historic key update, the historic value and associated 276 | * transaction id and timestamp are returned. The timestamp is the 277 | * timestamp provided by the client in the proposal header. 278 | * This method requires peer configuration 279 | * core.ledger.history.enableHistoryDatabase to be true.

280 | * The query is NOT re-executed during validation phase, phantom reads are 281 | * not detected. That is, other committed transactions may have updated 282 | * the key concurrently, impacting the result set, and this would not be 283 | * detected at validation/commit time. Applications susceptible to this 284 | * should therefore not use GetHistoryForKey as part of transactions that 285 | * update ledger, and should limit use to read-only chaincode operations. 286 | * @async 287 | * @param {string} key The state variable key 288 | * @return {Promise} Promise for a {@link HistoryQueryIterator} object 289 | */ 290 | async getHistoryForKey(key) { 291 | return this.middleware('getHistoryForKey', {key}); 292 | } 293 | 294 | /** 295 | * A Response object is returned from a chaincode invocation 296 | * @typedef {Object} Response 297 | * @property {number} status A status code that follows the HTTP status codes 298 | * @property {string} message A message associated with the response code 299 | * @property {byte[]} payload A payload that can be used to include metadata with this response 300 | */ 301 | 302 | /** 303 | * Locally calls the specified chaincode invoke() using the 304 | * same transaction context; that is, chaincode calling chaincode doesn't 305 | * create a new transaction message.

306 | * If the called chaincode is on the same channel, it simply adds the called 307 | * chaincode read set and write set to the calling transaction.

308 | * If the called chaincode is on a different channel, 309 | * only the Response is returned to the calling chaincode; any PutState calls 310 | * from the called chaincode will not have any effect on the ledger; that is, 311 | * the called chaincode on a different channel will not have its read set 312 | * and write set applied to the transaction. Only the calling chaincode's 313 | * read set and write set will be applied to the transaction. Effectively 314 | * the called chaincode on a different channel is a `Query`, which does not 315 | * participate in state validation checks in subsequent commit phase.

316 | * If `channel` is empty, the caller's channel is assumed. 317 | * @async 318 | * @param {string} chaincodeName Name of the chaincode to call 319 | * @param {byte[][]} args List of arguments to pass to the called chaincode 320 | * @param {string} channel Name of the channel where the target chaincode is active 321 | * @return {Promise} Promise for a {@link Response} object returned by the called chaincode 322 | */ 323 | async invokeChaincode(chaincodeName, args, channel) { 324 | return this.middleware('invokeChaincode', {chaincodeName, args, channel}); 325 | } 326 | 327 | /** 328 | * Allows the chaincode to propose an event on the transaction proposal. When the transaction 329 | * is included in a block and the block is successfully committed to the ledger, the block event 330 | * will be delivered to the current event listeners that have been registered with the peer's 331 | * event producer. Note that the block event gets delivered to the listeners regardless of the 332 | * status of the included transactions (can be either valid or invalid), so client applications 333 | * are responsible for checking the validity code on each transaction. Consult each SDK's documentation 334 | * for details. 335 | * @param {string} name Name of the event 336 | * @param {byte[]} payload A payload can be used to include data about the event 337 | */ 338 | setEvent(name, payload) { 339 | return this.middleware('setEvent', {name, payload}); 340 | } 341 | 342 | /** 343 | * Creates a composite key by combining the objectType string and the given `attributes` to form a composite 344 | * key. The objectType and attributes are expected to have only valid utf8 strings and should not contain 345 | * U+0000 (nil byte) and U+10FFFF (biggest and unallocated code point). The resulting composite key can be 346 | * used as the key in [putState()]{@link ChaincodeStub#putState}.

347 | * 348 | * Hyperledger Fabric uses a simple key/value model for saving chaincode states. In some use case scenarios, 349 | * it is necessary to keep track of multiple attributes. Furthermore, it may be necessary to make the various 350 | * attributes searchable. Composite keys can be used to address these requirements. Similar to using composite 351 | * keys in a relational database table, here you would treat the searchable attributes as key columns that 352 | * make up the composite key. Values for the attributes become part of the key, thus they are searchable with 353 | * functions like [getStateByRange()]{@link ChaincodeStub#getStateByRange} and 354 | * [getStateByPartialCompositeKey()]{@link ChaincodeStub#getStateByPartialCompositeKey}.

355 | * 356 | * @param {string} objectType A string used as the prefix of the resulting key 357 | * @param {string[]} attributes List of attribute values to concatenate into the key 358 | * @return {string} A composite key with the objectType and the array of attributes 359 | * joined together with special delimiters that will not be confused with values of the attributes 360 | */ 361 | createCompositeKey(objectType, attributes) { 362 | return this.middleware('createCompositeKey', {objectType, attributes}); 363 | } 364 | 365 | /** 366 | * Splits the specified key into attributes on which the composite key was formed. 367 | * Composite keys found during range queries or partial composite key queries can 368 | * therefore be split into their original composite parts, essentially recovering 369 | * the values of the attributes. 370 | * @param {string} compositeKey The composite key to split 371 | * @return {Object} An object which has properties of 'objectType' (string) and 372 | * 'attributes' (string[]) 373 | */ 374 | splitCompositeKey(compositeKey) { 375 | return this.middleware('splitCompositeKey', {compositeKey}); 376 | } 377 | 378 | /** 379 | * Queries the state in the ledger based on a given partial composite key. This function returns an iterator 380 | * which can be used to iterate over all composite keys whose prefix matches the given partial composite key. 381 | * The `objectType` and attributes are expected to have only valid utf8 strings and should not contain 382 | * U+0000 (nil byte) and U+10FFFF (biggest and unallocated code point).

383 | * 384 | * See related functions [splitCompositeKey]{@link ChaincodeStub#splitCompositeKey} and 385 | * [createCompositeKey]{@link ChaincodeStub#createCompositeKey}.

386 | * 387 | * Call close() on the returned {@link StateQueryIterator} object when done.

388 | * 389 | * The query is re-executed during validation phase to ensure result set has not changed since transaction 390 | * endorsement (phantom reads detected). 391 | * @async 392 | * @param {string} objectType A string used as the prefix of the resulting key 393 | * @param {string[]} attributes List of attribute values to concatenate into the partial composite key 394 | * @return {Promise} A promise that resolves with a {@link StateQueryIterator}, rejects if an error occurs 395 | */ 396 | async getStateByPartialCompositeKey(objectType, attributes) { 397 | return this.middleware('getStateByPartialCompositeKey', {objectType, attributes}); 398 | } 399 | } 400 | 401 | module.exports = ChaincodeStub; 402 | --------------------------------------------------------------------------------