From e29bcd9e6480d4953039a86bd54da625620bc07b Mon Sep 17 00:00:00 2001 From: wenjian3 Date: Wed, 20 Feb 2019 12:24:42 -0500 Subject: [PATCH] [FABN-1148] Allow offline singer for token - Add the following APIs to TokenClient generateUnsignedTokenCommand generateUnsignedTokenTransaction sendSignedTokenCommand sendSignedTokenTransaction Change-Id: I0efef872b05f46c6c7673b3dde1678b08d0ca1c5 Signed-off-by: Wenjian Qiao --- fabric-client/lib/Channel.js | 79 ++++++++++ fabric-client/lib/TokenClient.js | 186 ++++++++++++++++++++++- fabric-client/lib/token-utils.js | 36 ++++- fabric-client/test/TokenClient.js | 235 +++++++++++++++++++++++++++++- fabric-client/test/token-utils.js | 65 ++++++++- 5 files changed, 582 insertions(+), 19 deletions(-) diff --git a/fabric-client/lib/Channel.js b/fabric-client/lib/Channel.js index 6655f3e275..fbe95a0313 100644 --- a/fabric-client/lib/Channel.js +++ b/fabric-client/lib/Channel.js @@ -3263,6 +3263,50 @@ const Channel = class { return client_utils.toEnvelope(client_utils.signProposal(signer, payload)); } + /** + * Send signed token command to peer + * + * @param {SignedCommandRequest} request - Required. Signed token command + * that will be sent to peer. Must contain a signedCommand object. + * @param {Number} timeout - Optional. A number indicating milliseconds to wait on the + * response before rejecting the promise with a timeout error. This + * overrides the default timeout of the Orderer instance and the global + * timeout in the config settings. + * @returns {Promise} A Promise for the {@link CommandResponse} + */ + static async sendSignedTokenCommand(request, timeout) { + const method = 'sendSignedTokenCommand'; + logger.debug('%s - start', method); + + if (!request) { + throw Error(util.format('Missing required "request" parameter on the %s call', method)); + } + if (!request.command_bytes) { + throw new Error(util.format('Missing required "command_bytes" in request on the %s call', method)); + } + if (!request.signature) { + throw new Error(util.format('Missing required "signature" in request on the %s call', method)); + } + + const signedCommand = token_utils.toSignedCommand(request.signature, request.command_bytes); + + if (this._prover_handler) { + const params = { + request: request, + signed_command: signedCommand, + timeout: timeout, + }; + + const response = await this._prover_handler.processCommand(params); + return response; + } else { + // convert any names into peer objects or if empty find all + // prover peers added to this channel + const targetPeers = this._getTargets(request.targets, Constants.NetworkConfig.PROVER_PEER_ROLE); + return await token_utils.sendTokenCommandToPeer(targetPeers, signedCommand, timeout); + } + } + /** * Sends a token command to a prover peer and returns a [CommandResponse] {@link CommandResponse} * that contains either a token transaction or unspent tokens depending on the command. @@ -3354,6 +3398,41 @@ const Channel = class { return signed_command; } + /** + * send the signed token transaction + * + * @param {SignedTokenTransactionRequest} request - Required. The signed token transaction. + * Must contain 'signature' and 'tokentx_bytes' properties. + * @param {Number} timeout - A number indicating milliseconds to wait on the + * response before rejecting the promise with a timeout error. This + * overrides the default timeout of the Orderer instance and the global + * timeout in the config settings. + * @returns {BroadcastResponse} A BroadcastResponse message returned by + * the orderer that contains a single "status" field for a + * standard [HTTP response code]{@link https://github.com/hyperledger/fabric/blob/v1.0.0/protos/common/common.proto#L27}. + * This will be an acknowledgement from the orderer of a successfully + * submitted transaction. + */ + async sendSignedTokenTransaction(request, timeout) { + const method = 'sendSignedTokenTransaction'; + logger.debug('%s - start', method); + + const signed_envelope = token_utils.toEnvelope(request.signature, request.tokentx_bytes); + if (this._commit_handler) { + const params = { + signed_envelope, + request: request, + timeout: timeout, + }; + + return this._commit_handler.commit(params); + } else { + // verify that we have an orderer configured + const orderer = this._clientContext.getTargetOrderer(request.orderer, this.getOrderers(), this._name); + return orderer.sendBroadcast(signed_envelope, timeout); + } + } + /** * @typedef {Object} TokenTransactionRequest * This object contains properties that will be used for broadcasting token transaction. diff --git a/fabric-client/lib/TokenClient.js b/fabric-client/lib/TokenClient.js index 7708055d80..70509cbe0f 100644 --- a/fabric-client/lib/TokenClient.js +++ b/fabric-client/lib/TokenClient.js @@ -14,10 +14,16 @@ 'use strict'; +const Channel = require('./Channel'); +const TransactionID = require('./TransactionID'); const util = require('util'); +const clientUtils = require('./client-utils.js'); const tokenUtils = require('./token-utils.js'); const utils = require('./utils.js'); const logger = utils.getLogger('TokenClient.js'); +const fabprotos = require('fabric-protos'); +const {Identity} = require('fabric-common'); +const {HashPrimitives} = require('fabric-common'); /** * @classdesc @@ -179,7 +185,7 @@ const TokenClient = class { An array of token ids that will be spent by the token command. * @property {TokenParam[]} params - Required. One or multiple TokenParam * for the token command. - * @property {TransactionID} txId - Required. Transaction ID to use for this request. + * @property {TransactionID} txId - Optional. Required for issue, transfer and redeem. */ /** @@ -200,7 +206,7 @@ const TokenClient = class { */ async issue(request, timeout) { logger.debug('issue - Start'); - tokenUtils.checkTokenRequest(request, 'issue'); + tokenUtils.checkTokenRequest(request, 'issue', true); // copy request so that we can make update const sendRequest = Object.assign({}, request); @@ -228,7 +234,7 @@ const TokenClient = class { */ async transfer(request, timeout) { logger.debug('transfer - Start'); - tokenUtils.checkTokenRequest(request, 'transfer'); + tokenUtils.checkTokenRequest(request, 'transfer', true); // copy request so that we can make update const sendRequest = Object.assign({}, request); @@ -255,7 +261,7 @@ const TokenClient = class { */ async redeem(request, timeout) { logger.debug('redeem - Start'); - tokenUtils.checkTokenRequest(request, 'redeem'); + tokenUtils.checkTokenRequest(request, 'redeem', true); // copy request so that we can make update const sendRequest = Object.assign({}, request); @@ -328,6 +334,178 @@ const TokenClient = class { const result = await this._channel.sendTokenTransaction(request, timeout); return result; } + + /** + * @typedef {Object} SignedCommandRequest + * The object contains properties that will be used for sending a token command. + * @property {Buffer} command_bytes - Required. The token command bytes. + * @property {Buffer} signature - Required. The signer's signature for the command_bytes. + * @property {Peer[]|string[]} targets - Optional. The peers that will receive this + * request, when not provided the list of peers added to this channel + * object will be used. + */ + + /** + * Sends signed token command to peer + * + * @param {SignedCommandRequest} request containing signedCommand and targets. + * The signed command would be sent to peer directly. + * @param {number} timeout the timeout setting passed on sendSignedCommand + * @returns {Promise} A Promise for a "CommandResponse" message returned by + * the prover peer. + */ + async sendSignedTokenCommand(request, timeout) { + // copy request to protect user input + const sendRequest = Object.assign({}, request); + if (!sendRequest.targets) { + sendRequest.targets = this._targets; + } + return Channel.sendSignedTokenCommand(sendRequest, timeout); + } + + /** + * @typedef {Object} SignedTokenTransactionRequest + * This object contains properties that will be used for broadcasting a token transaction. + * @property {Buffer} signature - Required. The signature. + * @property {Buffer} payload_bytes - Required. The token transaction payload bytes. + * @property {TransactionID} txId - Required. Transaction ID to use for this request. + * @property {Orderer | string} orderer - Optional. The orderer that will receive this request, + * when not provided, the transaction will be sent to the orderers assigned to this channel instance. + */ + + /** + * send the signed token transaction + * + * @param {SignedTokenTransactionRequest} request - Required. The signed token transaction. + * Must contain 'signature' and 'tokentx_bytes' properties. + * @param {Number} timeout - A number indicating milliseconds to wait on the + * response before rejecting the promise with a timeout error. This + * overrides the default timeout of the Orderer instance and the global + * timeout in the config settings. + * @returns {BroadcastResponse} A BroadcastResponse message returned by + * the orderer that contains a single "status" field for a + * standard [HTTP response code]{@link https://github.com/hyperledger/fabric/blob/v1.0.0/protos/common/common.proto#L27}. + * This will be an acknowledgement from the orderer of a successfully + * submitted transaction. + */ + async sendSignedTokenTransaction(request, timeout) { + return this._channel.sendSignedTokenTransaction(request, timeout); + } + + /** + * Generates the unsigned token command + * + * Currently the [sendTokenCommand]{@link Channel#sendTokenCommand} + * signs a token command using the user identity from SDK's context (which + * contains the user's private key). + * + * This method is designed to build the token command at SDK side, + * and user can sign this token command with their private key, and send + * the signed token command to peer by [sendSignedTokenCommand] + * so the user's private key would not be required at SDK side. + * + * @param {TokenRequest} request token request + * @param {string} mspId the mspId for this identity + * @param {string} certificate PEM encoded certificate + * @param {boolean} admin if this transaction is invoked by admin + * @returns {Command} protobuf message for token command + */ + generateUnsignedTokenCommand(request, mspId, certificate, admin) { + const method = 'generateUnsignedTokenCommand'; + logger.debug('%s - start', method); + + logger.debug('%s - token command name is %s', method, request.commandName); + tokenUtils.checkTokenRequest(request, method, false); + + let tokenCommand; + if (request.commandName === 'issue') { + tokenCommand = tokenUtils.buildIssueCommand(request); + } else if (request.commandName === 'transfer') { + tokenCommand = tokenUtils.buildTransferCommand(request); + } else if (request.commandName === 'redeem') { + tokenCommand = tokenUtils.buildRedeemCommand(request); + } else if (request.commandName === 'list') { + tokenCommand = tokenUtils.buildListCommand(request); + } else if (!request.commandName) { + throw new Error(utils.format('Missing commandName on the %s call', method)); + } else { + throw new Error(utils.format('Invalid commandName (%s) on the %s call', request.commandName, method)); + } + + // create identity using certificate, publicKey (null), and mspId + const identity = new Identity(certificate, null, mspId); + const txId = new TransactionID(identity, admin); + const header = tokenUtils.buildTokenCommandHeader( + identity, + this._channel._name, + txId.getNonce(), + this._client.getClientCertHash() + ); + + tokenCommand.setHeader(header); + return tokenCommand; + } + + /** + * @typedef {Object} TokenTransactionRequest + * This object contains properties that will be used build token transaction payload. + * @property {Buffer} tokentx_bytes - Required. The token transaction bytes. + * @property {Command} tokenCommand - Required. The token command that is used to receive the token transaction. + */ + + /** + * Generates unsigned payload for the token transaction. + * The tokenCommand used to generate the token transaction must be passed in the request. + * + * @param {TokenTransactionRequest} request - required. + * @returns {Payload} protobuf message for token transaction payload + */ + generateUnsignedTokenTransaction(request) { + const method = 'generateUnsignedTokenTransaction'; + logger.debug('%s - start', method); + + if (!request) { + throw new Error(util.format('Missing input request parameter on the %s call', method)); + } + if (!request.tokenTransaction) { + throw new Error(util.format('Missing required "tokenTransaction" in request on the %s call', method)); + } + if (!request.tokenCommand) { + throw new Error(util.format('Missing required "tokenCommand" in request on the %s call', method)); + } + if (!request.tokenCommand.header) { + throw new Error(util.format('Missing required "header" in tokenCommand on the %s call', method)); + } + + const commandHeader = request.tokenCommand.header; + const trans_bytes = Buffer.concat([commandHeader.nonce.toBuffer(), commandHeader.creator.toBuffer()]); + const trans_hash = HashPrimitives.SHA2_256(trans_bytes); + const txId = Buffer.from(trans_hash).toString(); + + const channel_header = clientUtils.buildChannelHeader( + fabprotos.common.HeaderType.TOKEN_TRANSACTION, + this._channel._name, + txId, + null, // no epoch + '', + clientUtils.buildCurrentTimestamp(), + this._client.getClientCertHash() + ); + + const signature_header = new fabprotos.common.SignatureHeader(); + signature_header.setCreator(commandHeader.creator); + signature_header.setNonce(commandHeader.nonce); + + const header = new fabprotos.common.Header(); + header.setChannelHeader(channel_header.toBuffer()); + header.setSignatureHeader(signature_header.toBuffer()); + + const payload = new fabprotos.common.Payload(); + payload.setHeader(header); + payload.setData(request.tokenTransaction.toBuffer()); + + return payload; + } }; module.exports = TokenClient; diff --git a/fabric-client/lib/token-utils.js b/fabric-client/lib/token-utils.js index f930919124..e844b8752d 100644 --- a/fabric-client/lib/token-utils.js +++ b/fabric-client/lib/token-utils.js @@ -20,6 +20,8 @@ const utils = require('./utils.js'); const logger = utils.getLogger('token-utils.js'); const fabprotos = require('fabric-protos'); +const valid_command_names = ['issue', 'transfer', 'redeem', 'list']; + /* * This function will build a token command for issue request */ @@ -181,21 +183,31 @@ module.exports.buildTokenCommandHeader = (creator, channelId, nonce, tlsCertHash }; /* - * Checks token request and throw an error if any required parameter is missing. + * Checks token request and throw an error if any required parameter is missing or invalid. */ -module.exports.checkTokenRequest = (request, command_name) => { +module.exports.checkTokenRequest = (request, command_name, txIdRequired) => { logger.debug('checkTokenRequest - start'); if (!request) { logger.error('Missing required "request" parameter on %s call', command_name); throw new Error(util.format('Missing required "request" parameter on %s call', command_name)); } - if (!request.txId) { + if (txIdRequired && !request.txId) { logger.error('Missing required "txId" in request on %s call', command_name); throw new Error(util.format('Missing required "txId" in request on %s call', command_name)); } - if (request.commandName !== undefined && request.commandName !== command_name) { - throw new Error(util.format('Wrong "commandName" in request on %s call: %s', command_name, request.commandName)); + if (request.commandName !== undefined && !valid_command_names.includes(request.commandName)) { + throw new Error(util.format('Invalid "commandName" in request on %s call: %s', command_name, request.commandName)); + } + if (!request.commandName && !valid_command_names.includes(command_name)) { + throw new Error(util.format('Missing "commandName" in request on %s call', command_name)); + } + if (valid_command_names.includes(command_name) && request.commandName !== undefined && request.commandName !== command_name) { + throw new Error(util.format('Invalid "commandName" in request on %s call: %s', command_name, request.commandName)); + } + + if (request.commandName === 'list' || command_name === 'list') { + return; } // check parameters for non-list commands @@ -230,3 +242,17 @@ module.exports.checkTokenRequest = (request, command_name) => { } }); }; + +/** + * convert to protos.token.SignedCommand + * @param signature + * @param command_bytes + */ +exports.toSignedCommand = (signature, command_bytes) => ({signature: signature, command: command_bytes}); + +/** + * convert to protos.common.Envelope + * @param signature + * @param tokentx_bytes + */ +exports.toEnvelope = (signature, tokentx_bytes) => ({signature: signature, payload: tokentx_bytes}); diff --git a/fabric-client/test/TokenClient.js b/fabric-client/test/TokenClient.js index 67b84d3c1f..eb2e53369e 100644 --- a/fabric-client/test/TokenClient.js +++ b/fabric-client/test/TokenClient.js @@ -14,8 +14,11 @@ 'use strict'; +const console = require('console'); +const util = require('util'); const rewire = require('rewire'); const Client = require('../lib/Client'); +const {Identity} = require('fabric-common'); const TokenClient = rewire('../lib/TokenClient'); const TransactionID = require('../lib/TransactionID.js'); const fabprotos = require('fabric-protos'); @@ -46,6 +49,7 @@ describe('TokenClient', () => { const testowner = {type: fabprotos.token.TokenOwner_MSP_IDENTIFIER, raw: Buffer.from('testowner')}; const txId = sinon.createStubInstance(TransactionID); + const channelId = 'mychannel'; beforeEach(() => { revert = []; @@ -74,6 +78,7 @@ describe('TokenClient', () => { sendTokenCommandStub.returns(mockResponse); sendTokenTransactionStub.returns(mockResult); + // use following common stubs for all tests revert.push(TokenClient.__set__('tokenUtils.checkTokenRequest', checkTokenRequestStub)); revert.push(TokenClient.__set__('tokenUtils.buildIssueCommand', buildIssueCommandStub)); revert.push(TokenClient.__set__('tokenUtils.buildTransferCommand', buildTransferCommandStub)); @@ -81,6 +86,7 @@ describe('TokenClient', () => { revert.push(TokenClient.__set__('tokenUtils.buildListCommand', buildListCommandStub)); mockChannel = { + _name: channelId, sendTokenCommand: sendTokenCommandStub, sendTokenTransaction: sendTokenTransactionStub, }; @@ -144,7 +150,7 @@ describe('TokenClient', () => { sinon.assert.calledOnce(sendTokenTransactionStub); sinon.assert.calledWith(buildIssueCommandStub, request); - sinon.assert.calledWith(checkTokenRequestStub, request); + sinon.assert.calledWith(checkTokenRequestStub, request, 'issue', true); let arg = sendTokenCommandStub.getCall(0).args[0]; arg.tokenCommand.should.deep.equal(mockCommand); arg = sendTokenTransactionStub.getCall(0).args[0]; @@ -247,7 +253,7 @@ describe('TokenClient', () => { sinon.assert.calledOnce(sendTokenCommandStub); sinon.assert.calledOnce(sendTokenTransactionStub); - sinon.assert.calledWith(checkTokenRequestStub, request); + sinon.assert.calledWith(checkTokenRequestStub, request, 'transfer', true); sinon.assert.calledWith(buildTransferCommandStub, request); let arg = sendTokenCommandStub.getCall(0).args[0]; arg.tokenCommand.should.deep.equal(mockCommand); @@ -351,7 +357,7 @@ describe('TokenClient', () => { sinon.assert.calledOnce(sendTokenCommandStub); sinon.assert.calledOnce(sendTokenTransactionStub); - sinon.assert.calledWith(checkTokenRequestStub, request); + sinon.assert.calledWith(checkTokenRequestStub, request, 'redeem', true); sinon.assert.calledWith(buildRedeemCommandStub, request); let arg = sendTokenCommandStub.getCall(0).args[0]; arg.tokenCommand.should.deep.equal(mockCommand); @@ -566,4 +572,227 @@ describe('TokenClient', () => { } }); }); + + describe('#generateUnsignedTokenCommand', () => { + let request; + let param; + let tokenIds; + let newIdentityStub; + let identityStub; + let newTransactionIDStub; + let transactionIDStub; + let clientStub; + let buildTokenCommandHeaderStub; + + const admin = true; + const mspId = 'fake-mspid'; + const certificate = 'fake-certificate'; + const commandHeader = new fabprotos.token.Header(); + const tlsCertHash = Buffer.from('test-client-cert-hash'); + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + // update mockCommand with mocked header + mockCommand.setHeader(commandHeader); + + // prepare request parameters + tokenIds = [{tx_id: 'mock_tx_id', index: 0}]; + param = {recipient: testowner, type: 'abc123', quantity: 210}; + + // create stubs + buildTokenCommandHeaderStub = sinon.stub(); + buildTokenCommandHeaderStub.returns(commandHeader); + + identityStub = sinon.createStubInstance(Identity); + identityStub.serialize.returns('fake-serialized-identity'); + newIdentityStub = sinon.stub(); + newIdentityStub.returns(identityStub); + + transactionIDStub = sinon.createStubInstance(TransactionID); + transactionIDStub.getNonce.returns('fake-nonce'); + newTransactionIDStub = sinon.stub(); + newTransactionIDStub.returns(transactionIDStub); + + revert.push(TokenClient.__set__('Identity', newIdentityStub)); + revert.push(TokenClient.__set__('TransactionID', newTransactionIDStub)); + revert.push(TokenClient.__set__('tokenUtils.buildTokenCommandHeader', buildTokenCommandHeaderStub)); + + clientStub = sinon.createStubInstance(Client); + clientStub.getClientCertHash.returns(tlsCertHash); + tokenClient = new TokenClient(clientStub, mockChannel); + }); + + it('should return a unsigned token command for issue', () => { + request = {commandName: 'issue', params: [param]}; + const command = tokenClient.generateUnsignedTokenCommand(request, mspId, certificate, admin); + command.should.deep.equal(mockCommand); + + sinon.assert.calledWith(buildIssueCommandStub, request); + sinon.assert.calledWith(newIdentityStub, certificate, null, mspId); + sinon.assert.calledWith(newTransactionIDStub, identityStub, admin); + sinon.assert.calledWith(buildTokenCommandHeaderStub, identityStub, channelId, 'fake-nonce', tlsCertHash); + }); + + it('should return a unsigned token command for transfer', () => { + request = {commandName: 'transfer', tokenIds: tokenIds, params: [param]}; + const command = tokenClient.generateUnsignedTokenCommand(request, mspId, certificate, admin); + command.should.deep.equal(mockCommand); + + sinon.assert.calledWith(buildTransferCommandStub, request); + sinon.assert.calledWith(newIdentityStub, certificate, null, mspId); + sinon.assert.calledWith(newTransactionIDStub, identityStub, admin); + sinon.assert.calledWith(buildTokenCommandHeaderStub, identityStub, channelId, 'fake-nonce', tlsCertHash); + }); + + it('should return a unsigned token command for redeem', () => { + request = {commandName: 'redeem', tokenIds: tokenIds, params: [param]}; + const command = tokenClient.generateUnsignedTokenCommand(request, mspId, certificate, admin); + command.should.deep.equal(mockCommand); + + sinon.assert.calledWith(buildRedeemCommandStub, request); + sinon.assert.calledWith(newIdentityStub, certificate, null, mspId); + sinon.assert.calledWith(newTransactionIDStub, identityStub, admin); + sinon.assert.calledWith(buildTokenCommandHeaderStub, identityStub, channelId, 'fake-nonce', tlsCertHash); + }); + + it('should return a unsigned token command for list', () => { + request = {commandName: 'list'}; + const command = tokenClient.generateUnsignedTokenCommand(request, mspId, certificate, admin); + command.should.deep.equal(mockCommand); + + sinon.assert.calledWith(checkTokenRequestStub, request, 'generateUnsignedTokenCommand', false); + sinon.assert.calledWith(buildListCommandStub, request); + sinon.assert.calledWith(newIdentityStub, certificate, null, mspId); + sinon.assert.calledWith(newTransactionIDStub, identityStub, admin); + sinon.assert.calledWith(buildTokenCommandHeaderStub, identityStub, channelId, 'fake-nonce', tlsCertHash); + }); + + it('should get error when new Identity throws error', () => { + (() => { + const fakeError = new Error('forced new identity error'); + newIdentityStub.throws(fakeError); + + request = {commandName: 'issue', params: [param], txId: txId}; + tokenClient.generateUnsignedTokenCommand(request, mspId, certificate, admin); + }).should.throw('forced new identity error'); + }); + + it('should get error when new TransactionID throws error', () => { + (() => { + const fakeError = new Error('forced new identity error'); + newTransactionIDStub.throws(fakeError); + + request = {commandName: 'issue', params: [param], txId: txId}; + tokenClient.generateUnsignedTokenCommand(request, mspId, certificate, admin); + }).should.throw('forced new identity error'); + }); + }); + + describe('#generateUnsignedTokenTransaction', () => { + let request; + let tokenTx; + let command; + let commandHeader; + let sha2_256Stub; + let buildChannelHeaderStub; + let buildCurrentTimestampStub; + let clientStub; + + const trans_hash = 'fake-sha2_256-tran-hash'; + const tlsCertHash = Buffer.from('test-client-cert-hash'); + const channelHeader = new fabprotos.common.ChannelHeader(); + const mockTimestamp = sinon.createStubInstance(fabprotos.google.protobuf.Timestamp); + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + // prepare command header, command and token transaction for input request + commandHeader = new fabprotos.token.Header(); + commandHeader.setChannelId(channelId); + commandHeader.setCreator(Buffer.from('fake-creator')); + commandHeader.setNonce(Buffer.from('fake-nonce')); + commandHeader.setTlsCertHash(tlsCertHash); + + command = new fabprotos.token.Command(); + command.setHeader(commandHeader); + + tokenTx = new fabprotos.token.TokenTransaction(); + tokenTx.set('plain_action', new fabprotos.token.PlainTokenAction()); + + // create stubs + sha2_256Stub = sinon.stub(); + sha2_256Stub.returns(trans_hash); + + buildChannelHeaderStub = sinon.stub(); + buildChannelHeaderStub.returns(channelHeader); + + buildCurrentTimestampStub = sinon.stub(); + buildCurrentTimestampStub.returns(mockTimestamp); + + revert.push(TokenClient.__set__('clientUtils.buildChannelHeader', buildChannelHeaderStub)); + revert.push(TokenClient.__set__('clientUtils.buildCurrentTimestamp', buildCurrentTimestampStub)); + revert.push(TokenClient.__set__('HashPrimitives.SHA2_256', sha2_256Stub)); + + // create an intance of TokenClient + clientStub = sinon.createStubInstance(Client); + clientStub.getClientCertHash.returns(tlsCertHash); + tokenClient = new TokenClient(clientStub, mockChannel); + }); + + it('should return a unsigned transaction payload', () => { + console.log('comparing 1'); + request = {tokenTransaction: tokenTx, tokenCommand: command}; + const payload = tokenClient.generateUnsignedTokenTransaction(request); + + console.log('comparing 2 payload: %s', util.inspect(payload, {depth: null})); + payload.data.toBuffer().should.deep.equal(tokenTx.toBuffer()); + + console.log('comparing 4 payload: %s', util.inspect(payload, {depth: null})); + const signatureHeader = new fabprotos.common.SignatureHeader(); + signatureHeader.setCreator(commandHeader.creator); + signatureHeader.setNonce(commandHeader.nonce); + payload.header.signature_header.toBuffer().should.deep.equal(signatureHeader.toBuffer()); + + console.log('comparing 5 payload: %s', util.inspect(payload, {depth: null})); + payload.header.channel_header.toBuffer().should.deep.equal(channelHeader.toBuffer()); + + console.log('comparing 6 payload: %s', util.inspect(payload, {depth: null})); + sinon.assert.calledWith(buildChannelHeaderStub, + fabprotos.common.HeaderType.TOKEN_TRANSACTION, channelId, trans_hash, null, '', mockTimestamp, tlsCertHash); + sinon.assert.calledOnce(buildCurrentTimestampStub); + }); + + it('should get error when request has no tokenTransaction', () => { + (() => { + request = {tokenCommand: command}; + tokenClient.generateUnsignedTokenTransaction(request); + }).should.throw('Missing required "tokenTransaction" in request on the generateUnsignedTokenTransaction call'); + }); + + it('should get error when request has no tokenCommand', () => { + (() => { + request = {tokenTransaction: tokenTx}; + tokenClient.generateUnsignedTokenTransaction(request); + }).should.throw('Missing required "tokenCommand" in request on the generateUnsignedTokenTransaction call'); + }); + + it('should get error when request has no tokenCommand', () => { + (() => { + command.header = undefined; + request = {tokenTransaction: tokenTx, tokenCommand: command}; + tokenClient.generateUnsignedTokenTransaction(request); + }).should.throw('Missing required "header" in tokenCommand on the generateUnsignedTokenTransaction call'); + }); + + it('should get error when buildChannelHeader throws error', () => { + (() => { + const fakeError = new Error('forced build header error'); + buildChannelHeaderStub.throws(fakeError); + + request = {tokenTransaction: tokenTx, tokenCommand: command}; + tokenClient.generateUnsignedTokenTransaction(request); + }).should.throw('forced build header error'); + }); + }); }); diff --git a/fabric-client/test/token-utils.js b/fabric-client/test/token-utils.js index ac124668be..96517e066e 100644 --- a/fabric-client/test/token-utils.js +++ b/fabric-client/test/token-utils.js @@ -62,7 +62,7 @@ describe('token-utils', () => { it('should get error when no txId is passed', () => { (() => { request.txId = undefined; - TokenUtils.checkTokenRequest(request, 'issue'); + TokenUtils.checkTokenRequest(request, 'issue', true); }).should.throw('Missing required "txId" in request on issue call'); }); @@ -70,7 +70,7 @@ describe('token-utils', () => { describe('#checkTokenRequest for issue', () => { it('should return without any error', () => { - TokenUtils.checkTokenRequest(request, 'issue'); + TokenUtils.checkTokenRequest(request, 'issue', true); }); it('should get error when no params in request', () => { @@ -105,13 +105,13 @@ describe('token-utils', () => { (() => { request.commandName = 'badname'; TokenUtils.checkTokenRequest(request, 'issue'); - }).should.throw('Wrong "commandName" in request on issue call: badname'); + }).should.throw('Invalid "commandName" in request on issue call: badname'); }); }); describe('#checkTokenRequest for transfer', () => { it('should return without any error', () => { - TokenUtils.checkTokenRequest(request, 'transfer'); + TokenUtils.checkTokenRequest(request, 'transfer', true); }); it('should get error when no tokenIds in request', () => { @@ -146,13 +146,13 @@ describe('token-utils', () => { (() => { request.commandName = 'badname'; TokenUtils.checkTokenRequest(request, 'transfer'); - }).should.throw('Wrong "commandName" in request on transfer call: badname'); + }).should.throw('Invalid "commandName" in request on transfer call: badname'); }); }); describe('#checkTokenRequest for redeem', () => { it('should return without any error', () => { - TokenUtils.checkTokenRequest(request, 'redeem'); + TokenUtils.checkTokenRequest(request, 'redeem', true); }); it('should get error when no tokenIds in request', () => { @@ -180,7 +180,58 @@ describe('token-utils', () => { (() => { request.commandName = 'badname'; TokenUtils.checkTokenRequest(request, 'redeem'); - }).should.throw('Wrong "commandName" in request on redeem call: badname'); + }).should.throw('Invalid "commandName" in request on redeem call: badname'); + }); + }); + + describe('#checkTokenRequest for generateUnsignedTokenCommand', () => { + it('should return without any error for issue request', () => { + request = {commandName: 'issue', params: [param]}; + TokenUtils.checkTokenRequest(request, 'generateUnsignedTokenCommand', false); + }); + + it('should return without any error for transfer request', () => { + request = {commandName: 'transfer', tokenIds: tokenIds, params: [param]}; + TokenUtils.checkTokenRequest(request, 'generateUnsignedTokenCommand', false); + }); + + it('should return without any error for redeem request', () => { + request = {commandName: 'redeem', tokenIds: tokenIds, params: [param]}; + TokenUtils.checkTokenRequest(request, 'generateUnsignedTokenCommand', false); + }); + + it('should return without any error for list request', () => { + request = {commandName: 'list'}; + TokenUtils.checkTokenRequest(request, 'generateUnsignedTokenCommand', false); + }); + + it('should get error when wrong commandName in request', () => { + (() => { + request = {commandName: 'badName', params: [param]}; + TokenUtils.checkTokenRequest(request, 'generateUnsignedTokenCommand', false); + }).should.throw('Invalid "commandName" in request on generateUnsignedTokenCommand call: badName'); + }); + + it('should get error when no commandName in request', () => { + (() => { + request = {params: [param]}; + TokenUtils.checkTokenRequest(request, 'generateUnsignedTokenCommand', false); + }).should.throw('Missing "commandName" in request on generateUnsignedTokenCommand call'); + }); + + it('should get error when no params in request', () => { + (() => { + request = {commandName: 'issue'}; + TokenUtils.checkTokenRequest(request, 'generateUnsignedTokenCommand', false); + }).should.throw('Missing required "params" in request on generateUnsignedTokenCommand call'); + }); + + it('should get error when parameter has no quantity', () => { + (() => { + request = {commandName: 'issue', params: [param]}; + param.quantity = undefined; + TokenUtils.checkTokenRequest(request, 'generateUnsignedTokenCommand'); + }).should.throw('Missing required "quantity" in request on generateUnsignedTokenCommand call'); }); }); });