diff --git a/test/fixtures/channel/v2/configtx.yaml b/test/fixtures/channel/v2/configtx.yaml new file mode 100644 index 0000000000..a95fcb95fc --- /dev/null +++ b/test/fixtures/channel/v2/configtx.yaml @@ -0,0 +1,296 @@ +# Copyright IBM Corp. All Rights Reserved. +# +# SPDX-License-Identifier: Apache-2.0 +# + +--- +################################################################################ +# +# SECTION: Application +# +# - This section defines the values to encode into a config transaction or +# genesis block for application related parameters +# +################################################################################ +Application: &ApplicationDefaults + + # Organizations is the list of orgs which are defined as participants on + # the application side of the network + Organizations: + + # Policies defines the set of policies at this level of the config tree + # For Application policies, their canonical path is + # /Channel/Application/ + Policies: &ApplicationDefaultPolicies + Readers: + Type: ImplicitMeta + Rule: "ANY Readers" + Writers: + Type: ImplicitMeta + Rule: "ANY Writers" + Admins: + Type: ImplicitMeta + Rule: "MAJORITY Admins" +################################################################################ +# +# SECTION: Capabilities +################################################################################ +Capabilities: + Channel: &ChannelCapabilities + V2_0: true + + Orderer: &OrdererCapabilities + V2_0: true + + Application: &ApplicationCapabilities + V2_0: true + +################################################################################ +# +# CHANNEL +# +# This section defines the values to encode into a config transaction or +# genesis block for channel related parameters. +# +################################################################################ +Channel: &ChannelDefaults + # Policies defines the set of policies at this level of the config tree + # For Channel policies, their canonical path is + # /Channel/ + Policies: + # Who may invoke the 'Deliver' API + Readers: + Type: ImplicitMeta + Rule: "ANY Readers" + # Who may invoke the 'Broadcast' API + Writers: + Type: ImplicitMeta + Rule: "ANY Writers" + # By default, who may modify elements at this config level + Admins: + Type: ImplicitMeta + Rule: "MAJORITY Admins" + + + # Capabilities describes the channel level capabilities, see the + # dedicated Capabilities section elsewhere in this file for a full + # description + Capabilities: + <<: *ChannelCapabilities + + +################################################################################ +# +# SECTION: Orderer +# +# - This section defines the values to encode into a config transaction or +# genesis block for orderer related parameters +# +################################################################################ +Orderer: &OrdererDefaults + + # Orderer Type: The orderer implementation to start + # Available types are "solo" and "kafka" + OrdererType: solo + + Addresses: + - orderer.example.com:7050 + + # Batch Timeout: The amount of time to wait before creating a batch + BatchTimeout: 0.5s + + # Batch Size: Controls the number of messages batched into a block + BatchSize: + + # Max Message Count: The maximum number of messages to permit in a batch + MaxMessageCount: 10 + + # Absolute Max Bytes: The absolute maximum number of bytes allowed for + # the serialized messages in a batch. + AbsoluteMaxBytes: 98 MB + + # Preferred Max Bytes: The preferred maximum number of bytes allowed for + # the serialized messages in a batch. A message larger than the preferred + # max bytes will result in a batch larger than preferred max bytes. + PreferredMaxBytes: 512 KB + + Kafka: + # Brokers: A list of Kafka brokers to which the orderer connects + # NOTE: Use IP:port notation + Brokers: + - 127.0.0.1:9092 + + # Organizations is the list of orgs which are defined as participants on + # the orderer side of the network + Organizations: + + # Policies defines the set of policies at this level of the config tree + # For Orderer policies, their canonical path is + # /Channel/Orderer/ + Policies: + Readers: + Type: ImplicitMeta + Rule: "ANY Readers" + Writers: + Type: ImplicitMeta + Rule: "ANY Writers" + Admins: + Type: ImplicitMeta + Rule: "MAJORITY Admins" + # BlockValidation specifies what signatures must be included in the block + # from the orderer for the peer to validate it. + BlockValidation: + Type: ImplicitMeta + Rule: "ANY Writers" + +################################################################################ +# +# Section: Organizations +# +# - This section defines the different organizational identities which will +# be referenced later in the configuration. +# +################################################################################ +Organizations: + + # OrdererOrg defines an MSP using the sampleconfig. It should never be used + # in production but may be used as a template for other definitions + - &OrdererOrg + # DefaultOrg defines the organization which is used in the sampleconfig + # of the fabric.git development environment + Name: OrdererMSP + + # ID to load the MSP definition as + ID: OrdererMSP + + # MSPDir is the filesystem path which contains the MSP configuration + MSPDir: ../crypto-config/ordererOrganizations/example.com/msp + + # Policies defines the set of policies at this level of the config tree + # For organization policies, their canonical path is usually + # /Channel/// + Policies: &OrdererOrgPolicies + Readers: + Type: Signature + Rule: "OR('OrdererMSP.member')" + # If your MSP is configured with the new NodeOUs, you might + # want to use a more specific rule like the following: + # Rule: "OR('OrdererMSP.admin', 'OrdererMSP.peer', 'OrdererMSP.client')" + Writers: + Type: Signature + Rule: "OR('OrdererMSP.member')" + # If your MSP is configured with the new NodeOUs, you might + # want to use a more specific rule like the following: + # Rule: "OR('OrdererMSP.admin', 'OrdererMSP.client')" + Admins: + Type: Signature + Rule: "OR('OrdererMSP.admin')" + + - &Org1 + # DefaultOrg defines the organization which is used in the sampleconfig + # of the fabric.git development environment + Name: Org1MSP + + # ID to load the MSP definition as + ID: Org1MSP + + MSPDir: ../crypto-config/peerOrganizations/org1.example.com/msp + + AnchorPeers: + # AnchorPeers defines the location of peers which can be used + # for cross org gossip communication. Note, this value is only + # encoded in the genesis block in the Application section context + - Host: peer0.org1.example.com + Port: 7051 + + # Policies defines the set of policies at this level of the config tree + # For organization policies, their canonical path is usually + # /Channel/// + Policies: &Org1Policies + Readers: + Type: Signature + Rule: "OR('Org1MSP.member')" + # If your MSP is configured with the new NodeOUs, you might + # want to use a more specific rule like the following: + # Rule: "OR('Org1MSP.admin', 'Org1MSP.peer', 'Org1MSP.client')" + Writers: + Type: Signature + Rule: "OR('Org1MSP.member')" + # If your MSP is configured with the new NodeOUs, you might + # want to use a more specific rule like the following: + # Rule: "OR('Org1MSP.admin', 'Org1MSP.client')" + Admins: + Type: Signature + Rule: "OR('Org1MSP.admin')" + + - &Org2 + # DefaultOrg defines the organization which is used in the sampleconfig + # of the fabric.git development environment + Name: Org2MSP + + # ID to load the MSP definition as + ID: Org2MSP + + MSPDir: ../crypto-config/peerOrganizations/org2.example.com/msp + + AnchorPeers: + # AnchorPeers defines the location of peers which can be used + # for cross org gossip communication. Note, this value is only + # encoded in the genesis block in the Application section context + - Host: peer0.org2.example.com + Port: 8051 + + # Policies defines the set of policies at this level of the config tree + # For organization policies, their canonical path is usually + # /Channel/// + Policies: &Org2Policies + Readers: + Type: Signature + Rule: "OR('Org2MSP.member')" + # If your MSP is configured with the new NodeOUs, you might + # want to use a more specific rule like the following: + # Rule: "OR('Org2MSP.admin', 'Org2MSP.peer', 'Org2MSP.client')" + Writers: + Type: Signature + Rule: "OR('Org2MSP.member')" + # If your MSP is configured with the new NodeOUs, you might + # want to use a more specific rule like the following: + # Rule: "OR('Org2MSP.admin', 'Org2MSP.client')" + Admins: + Type: Signature + Rule: "OR('Org2MSP.admin')" + +################################################################################ +# +# Profile +# +# - Different configuration profiles may be encoded here to be specified +# as parameters to the configtxgen tool +# +################################################################################ +Profiles: + + TwoOrgsOrdererGenesis: + <<: *ChannelDefaults + Orderer: + <<: *OrdererDefaults + Organizations: + - *OrdererOrg + Capabilities: + <<: *OrdererCapabilities + Consortiums: + SampleConsortium: + Organizations: + - *Org1 + - *Org2 + TwoOrgsChannel: + <<: *ChannelDefaults + Consortium: SampleConsortium + Application: + <<: *ApplicationDefaults + Organizations: + - *Org1 + - *Org2 + Capabilities: + <<: *ApplicationCapabilities + diff --git a/test/fixtures/channel/v2/tokenchannel.tx b/test/fixtures/channel/v2/tokenchannel.tx new file mode 100644 index 0000000000..7babcac6c2 Binary files /dev/null and b/test/fixtures/channel/v2/tokenchannel.tx differ diff --git a/test/integration/e2e/create-channel.js b/test/integration/e2e/create-channel.js index 91d3dd789d..9fe287593c 100644 --- a/test/integration/e2e/create-channel.js +++ b/test/integration/e2e/create-channel.js @@ -55,7 +55,8 @@ test('\n\n***** SDK Built config update create flow *****\n\n', async (t) => { client.setCryptoSuite(cryptoSuite); // use the config update created by the configtx tool - const envelope_bytes = fs.readFileSync(path.join(__dirname, '../../fixtures/channel/mychannel.tx')); + const channeltx_basename = process.env.channeltx_subdir ? path.join(process.env.channeltx_subdir, channel_name) : channel_name; + const envelope_bytes = fs.readFileSync(path.join(__dirname, '../../fixtures/channel/' + channeltx_basename + '.tx')); const config = client.extractChannelConfig(envelope_bytes); t.pass('Successfully extracted the config update from the configtx envelope'); @@ -145,6 +146,7 @@ test('\n\n***** SDK Built config update create flow *****\n\n', async (t) => { }; // send create request to orderer + logger.info('creating channel %s', channel_name); const result = await client.createChannel(request); logger.debug('\n***\n completed the create \n***\n', result); diff --git a/test/integration/e2e/e2eUtils.js b/test/integration/e2e/e2eUtils.js index 3f0890109d..a1fc19056b 100644 --- a/test/integration/e2e/e2eUtils.js +++ b/test/integration/e2e/e2eUtils.js @@ -846,6 +846,83 @@ function queryChaincode(org, version, targets, fcn, args, value, chaincodeId, t, module.exports.queryChaincode = queryChaincode; +// It sets up everything needed for a TokenClient: +// creates a client, enrolls a user, adds channel to client, and add orderers to the channel. +// The channel must be created and joined before this call. +// Returns an object including client, tokenClient, and user. +function createTokenClient(org, channel_name, targets, t) { + init(); + + Client.setConfigSetting('request-timeout', 60000); + + // this is a transaction, will just use org's admin identity to + // submit the request. either org should work properly + const client = new Client(); + const channel = client.newChannel(channel_name); + const tokenClient = client.newTokenClient(channel, targets); + + const orgName = ORGS[org].name; + const cryptoSuite = Client.newCryptoSuite(); + cryptoSuite.setCryptoKeyStore(Client.newCryptoKeyStore({path: testUtil.storePathForOrg(orgName)})); + client.setCryptoSuite(cryptoSuite); + + const caRootsPath = ORGS.orderer.tls_cacerts; + const cadata = fs.readFileSync(path.join(__dirname, caRootsPath)); + const caroots = Buffer.from(cadata).toString(); + let tlsInfo = null; + + return e2eUtils.tlsEnroll(org) + .then((enrollment) => { + t.pass('Successfully retrieved TLS certificate'); + tlsInfo = enrollment; + client.setTlsClientCertAndKey(tlsInfo.certificate, tlsInfo.key); + + return Client.newDefaultKeyValueStore({path: testUtil.storePathForOrg(orgName)}); + }).then((store) => { + + client.setStateStore(store); + return testUtil.getSubmitter(client, t, org); + + }).then((admin) => { + the_user = admin; + + t.pass('Successfully enrolled user \'admin\' (e2eUtil 4)'); + + channel.addOrderer( + client.newOrderer( + ORGS.orderer.url, + { + 'pem': caroots, + 'ssl-target-name-override': ORGS.orderer['server-hostname'] + } + ) + ); + + // set up the channel to use each org's 'peer1' for both requests and events + for (const key in ORGS) { + if (ORGS.hasOwnProperty(key) && typeof ORGS[key].peer1 !== 'undefined') { + const data = fs.readFileSync(path.join(__dirname, ORGS[key].peer1.tls_cacerts)); + const peer = client.newPeer( + ORGS[key].peer1.requests, + { + pem: Buffer.from(data).toString(), + 'ssl-target-name-override': ORGS[key].peer1['server-hostname'] + }); + channel.addPeer(peer); + } + } + + return {client: client, tokenClient: tokenClient, user: the_user}; + }, + (err) => { + logger.error('createTokenClient got error: %s', err); + t.fail('Failed to get submitter \'admin\'. Error: ' + err.stack ? err.stack : err); + throw new Error('Failed to get submitter'); + }); +} + +module.exports.createTokenClient = createTokenClient; + module.exports.sleep = testUtil.sleep; function loadMSPConfig(name, mspdir) { diff --git a/test/integration/e2e/join-channel.js b/test/integration/e2e/join-channel.js index 0cebab359f..52933ea994 100644 --- a/test/integration/e2e/join-channel.js +++ b/test/integration/e2e/join-channel.js @@ -24,6 +24,7 @@ const e2eUtils = require('./e2eUtils.js'); let tx_id = null; let ORGS; +const channelName = process.env.channel ? process.env.channel : testUtil.END2END.channel; // // Attempt to send a request to the orderer with the createChannel method @@ -32,10 +33,10 @@ test('\n\n***** End-to-end flow: join channel *****\n\n', (t) => { Client.addConfigFile(path.join(__dirname, './config.json')); ORGS = Client.getConfigSetting('test-network'); - joinChannel('org1', t) + joinChannel('org1', channelName, t) .then(() => { t.pass(util.format('Successfully joined peers in organization "%s" to the channel', ORGS.org1.name)); - return joinChannel('org2', t); + return joinChannel('org2', channelName, t); }, (err) => { t.fail(util.format('Failed to join peers in organization "%s" to the channel. %s', ORGS.org1.name, err.stack ? err.stack : err)); t.end(); @@ -53,13 +54,14 @@ test('\n\n***** End-to-end flow: join channel *****\n\n', (t) => { }); }); -function joinChannel(org, t) { - const channel_name = Client.getConfigSetting('E2E_CONFIGTX_CHANNEL_NAME', testUtil.END2END.channel); +function joinChannel(org, defaultChannelName, t) { + const channel_name = Client.getConfigSetting('E2E_CONFIGTX_CHANNEL_NAME', defaultChannelName); // // Create and configure the test channel // const client = new Client(); const channel = client.newChannel(channel_name); + logger.info('joining channel %s', channel_name); const orgName = ORGS[org].name; diff --git a/test/integration/e2e/token.js b/test/integration/e2e/token.js new file mode 100755 index 0000000000..69b1e21abe --- /dev/null +++ b/test/integration/e2e/token.js @@ -0,0 +1,490 @@ +/** + * Copyright 2019 IBM All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +// This includes end-to-end tests for the fabric token feature, both positive and negative. +'use strict'; + +const util = require('util'); +const utils = require('fabric-client/lib/utils.js'); +const logger = utils.getLogger('E2E token'); + +const tape = require('tape'); +const _test = require('tape-promise').default; +const test = _test(tape); + +const e2eUtils = require('./e2eUtils.js'); +const fabprotos = require('fabric-protos'); + +const channel_name = process.env.channel ? process.env.channel : 'tokenchannel'; + +// Positive tests that call issue/transfer/redeem APIs to invoke token transactions +// and call list API to verify the results. +test('\n\n***** Token end-to-end flow (green path): issue, transfer, redeem and list *****\n\n', async (t) => { + try { + // create TokenClient for user1 (admin user in org1) + const user1ClientObj = await e2eUtils.createTokenClient('org1', channel_name, 'localhost:7051', t); + const user1TokenClient = user1ClientObj.tokenClient; + const user1Identity = user1ClientObj.user.getIdentity(); + + // create TokenClient for user2 (admin user in org2) + const user2ClientObj = await e2eUtils.createTokenClient('org2', channel_name, 'localhost:8051', t); + const user2TokenClient = user2ClientObj.tokenClient; + const user2Identity = user2ClientObj.user.getIdentity(); + + // clean up tokens from previous tests so that we can rerun token test + await resetTokens(user1TokenClient, 'user1', t); + await resetTokens(user2TokenClient, 'user2', t); + + const eventhub = user1TokenClient._channel.getChannelEventHub('localhost:7051'); + + // build the request for user2 to issue tokens to user1 + let txId = user2TokenClient.getClient().newTransactionID(); + let param = { + recipient: {type: fabprotos.token.TokenOwner_MSP_IDENTIFIER, raw: user1Identity.serialize()}, + type: 'abc123', + quantity: 210 + }; + const param2 = { + recipient: {type: fabprotos.token.TokenOwner_MSP_IDENTIFIER, raw: user1Identity.serialize()}, + type: 'horizon', + quantity: 300, + }; + let request = { + params: [param, param2], + txId: txId, + }; + + // user2 issues tokens to user1 + let result = await user2TokenClient.issue(request); + logger.debug('issue returns: %s', util.inspect(result, {depth: null})); + t.equals(result.status, 'SUCCESS', 'Successfully sent issue token transaction to orderer. Waiting for transaction to be committed ...'); + await waitForTxEvent(eventhub, txId.getTransactionID(), 'VALID', t); + + // user1 call list to view his tokens + result = await user1TokenClient.list(); + logger.debug('\nuser1(org1) listed %d tokens after issue: \n%s', result.length, util.inspect(result, false, null)); + validateTokens(result, [param, param2], 'for user1 (recipient) after issue', t); + + const transferToken = result[0]; + const redeemToken = result[1]; + + // build request for user1 to transfer transfer token to user2 + txId = user1TokenClient.getClient().newTransactionID(); + param = { + recipient: {type: fabprotos.token.TokenOwner_MSP_IDENTIFIER, raw: user2Identity.serialize()}, + quantity: transferToken.quantity + }; + request = { + tokenIds: [transferToken.id], + params: [param], + txId: txId, + }; + + // user1 transfers tokens to user2 + result = await user1TokenClient.transfer(request); + logger.debug('transfer returns: %s', util.inspect(result, {depth: null})); + t.equals(result.status, 'SUCCESS', 'Successfully sent transfer token transaction to orderer. Waiting for transaction to be committed ...'); + await waitForTxEvent(eventhub, txId.getTransactionID(), 'VALID', t); + + // verify user1's unspent tokens after transfer, it should return 1 token + result = await user1TokenClient.list(); + logger.debug('(org1)list tokens after transfer token %s', util.inspect(result, false, null)); + t.equals(result.length, 1, 'Checking number of tokens for user1 after transfer'); + t.equals(result[0].type, redeemToken.type, 'Checking token type for user1 after transfer'); + t.equals(result[0].quantity.low, redeemToken.quantity.low, 'Checking token quantity for user1 after transfer'); + + // verify recipient's (user2) unspent tokens after transfer, it should return 1 token + result = await user2TokenClient.list(); + t.equals(result.length, 1, 'Checking number of tokens for user2 after transfer'); + t.equals(result[0].type, transferToken.type, 'Checking token type for user2 after transfer'); + t.equals(result[0].quantity.low, transferToken.quantity.low, 'Checking token quantity for user2 after transfer'); + + // build requst for user1 to redeem token + txId = user1TokenClient.getClient().newTransactionID(); + param = { + quantity: 50, + }; + request = { + tokenIds: [redeemToken.id], + params: param, + txId: txId, + }; + + // user1 redeems his token + result = await user1TokenClient.redeem(request); + logger.debug('redeem returns: %s', util.inspect(result, {depth: null})); + t.equals(result.status, 'SUCCESS', 'Successfully sent redeem token transaction to orderer. Waiting for transaction to be committed ...'); + await waitForTxEvent(eventhub, txId.getTransactionID(), 'VALID', t); + + // verify owner's (user1) unspent tokens after redeem - pass optional request + request = {txId: user1TokenClient.getClient().newTransactionID()}; + result = await user1TokenClient.list(request); + const remainingQuantity = redeemToken.quantity - param.quantity; + logger.debug('(org1)list tokens after transfer token %s', util.inspect(result, false, null)); + t.equals(result.length, 1, 'Checking number of tokens for user1 after redeem'); + t.equals(result[0].type, redeemToken.type, 'Checking token type for user1 after redeem'); + t.equals(result[0].quantity.low, remainingQuantity, 'Checking token quantity for user1 after redeem'); + + t.end(); + } catch (err) { + logger.error(err); + t.fail('Failed to test token commands due to error: ' + err.stack ? err.stack : err); + t.end(); + } +}); + +test('\n\n***** Token end-to-end flow: double spending fails *****\n\n', async (t) => { + try { + // create TokenClient for user1 (admin user in org1) + const user1ClientObj = await e2eUtils.createTokenClient('org1', channel_name, 'localhost:7051', t); + const user1TokenClient = user1ClientObj.tokenClient; + const user1Identity = user1ClientObj.user.getIdentity(); + + // create TokenClient for user2 (admin user in org2) + const user2ClientObj = await e2eUtils.createTokenClient('org2', channel_name, 'localhost:8051', t); + const user2TokenClient = user2ClientObj.tokenClient; + const user2Identity = user2ClientObj.user.getIdentity(); + + await resetTokens(user1TokenClient, 'user1', t); + await resetTokens(user2TokenClient, 'user2', t); + + const eventhub = user1TokenClient._channel.getChannelEventHub('localhost:7051'); + + // build request for user2 to issue a token to user1 + let txId = user2TokenClient.getClient().newTransactionID(); + let param = { + recipient: {type: fabprotos.token.TokenOwner_MSP_IDENTIFIER, raw: user1Identity.serialize()}, + type: 'abc123', + quantity: 210 + }; + let request = { + params: [param], + txId: txId, + }; + + // user2 issues token to user1 + let result = await user2TokenClient.issue(request); + logger.debug('issue returns: %s', util.inspect(result, {depth: null})); + t.equals(result.status, 'SUCCESS', 'Successfully sent issue token transaction to orderer. Waiting for transaction to be committed ...'); + await waitForTxEvent(eventhub, txId.getTransactionID(), 'VALID', t); + + // user1 lists tokens after issue + result = await user1TokenClient.list(); + logger.debug('\nuser1(org1) listed %d tokens after issue: \n%s', result.length, util.inspect(result, false, null)); + validateTokens(result, [param], 'for user1 (recipient) after issue', t); + + const transferToken = result[0]; + + // build request for user1 to transfer transfer token to user2 + txId = user1TokenClient.getClient().newTransactionID(); + param = { + recipient: {type: fabprotos.token.TokenOwner_MSP_IDENTIFIER, raw: user2Identity.serialize()}, + quantity: transferToken.quantity + }; + request = { + tokenIds: [transferToken.id], + params: [param], + txId: txId, + }; + + // user1 transfers token to user2 + result = await user1TokenClient.transfer(request); + logger.debug('transfer returns: %s', util.inspect(result, {depth: null})); + t.equals(result.status, 'SUCCESS', 'Successfully sent transfer token transaction to orderer. Waiting for transaction to be committed ...'); + await waitForTxEvent(eventhub, txId.getTransactionID(), 'VALID', t); + + // Transfer the same tokenId again - transaction should be invalidated + txId = user1TokenClient.getClient().newTransactionID(); + request = { + tokenIds: [transferToken.id], + params: [param], + txId: txId, + }; + + // user1 transfers token again using the same tokenIds + result = await user1TokenClient.transfer(request); + await waitForTxEvent(eventhub, txId.getTransactionID(), 'INVALID_OTHER_REASON', t); + + t.end(); + } catch (err) { + logger.error(err); + t.fail('Failed to test token commands due to error: ' + err.stack ? err.stack : err); + t.end(); + } +}); + +test('\n\n***** Token end-to-end flow: non owner transfer fails *****\n\n', async (t) => { + try { + // create TokenClient for user1 (admin user in org1) + // create TokenClient for user1 (admin user in org1) + const user1ClientObj = await e2eUtils.createTokenClient('org1', channel_name, 'localhost:7051', t); + const user1TokenClient = user1ClientObj.tokenClient; + const user1Identity = user1ClientObj.user.getIdentity(); + + // create TokenClient for user2 (admin user in org2) + const user2ClientObj = await e2eUtils.createTokenClient('org2', channel_name, 'localhost:8051', t); + const user2TokenClient = user2ClientObj.tokenClient; + const user2Identity = user2ClientObj.user.getIdentity(); + + await resetTokens(user1TokenClient, 'user1', t); + await resetTokens(user2TokenClient, 'user2', t); + + const eventhub = user1TokenClient._channel.getChannelEventHub('localhost:7051'); + + // build request for user2 to issue a token to user1 + let txId = user2TokenClient.getClient().newTransactionID(); + let param = { + recipient: {type: fabprotos.token.TokenOwner_MSP_IDENTIFIER, raw: user1Identity.serialize()}, + type: 'abc123', + quantity: 210 + }; + let request = { + params: [param], + txId: txId, + }; + + // user2 issues token to user1 + let result = await user2TokenClient.issue(request); + logger.debug('issue returns: %s', util.inspect(result, {depth: null})); + t.equals(result.status, 'SUCCESS', 'Successfully sent issue token transaction to orderer. Waiting for transaction to be committed ...'); + await waitForTxEvent(eventhub, txId.getTransactionID(), 'VALID', t); + + // user1 lists tokens after issue + result = await user1TokenClient.list(); + logger.debug('\nuser1(org1) listed %d tokens after issue: \n%s', result.length, util.inspect(result, false, null)); + validateTokens(result, [param], 'for user1 (recipient) after issue', t); + + const transferToken = result[0]; + + // token is owned by user1, but user2 attempts to transfer the token, should fail + txId = user2TokenClient.getClient().newTransactionID(); + param = { + recipient: {type: fabprotos.token.TokenOwner_MSP_IDENTIFIER, raw: user2Identity.serialize()}, + quantity: 10 + }; + request = { + tokenIds: [transferToken.id], + params: [param], + txId: txId, + }; + + // user2 attempts to transfer token, should fail + result = await user2TokenClient.transfer(request); + t.fail(''); + t.end(); + } catch (err) { + t.equals( + err.message, + 'command response has error: the requestor does not own inputs', + 'Transfer failed as expected because the requestor does not own the token'); + t.end(); + } +}); + +test('\n\n***** Token end-to-end flow: invalid transfer amount fails *****\n\n', async (t) => { + try { + // create TokenClient for user1 (admin user in org1) + const user1ClientObj = await e2eUtils.createTokenClient('org1', channel_name, 'localhost:7051', t); + const user1TokenClient = user1ClientObj.tokenClient; + const user1Identity = user1ClientObj.user.getIdentity(); + + // create TokenClient for user2 (admin user in org2) + const user2ClientObj = await e2eUtils.createTokenClient('org2', channel_name, 'localhost:8051', t); + const user2TokenClient = user2ClientObj.tokenClient; + const user2Identity = user2ClientObj.user.getIdentity(); + + await resetTokens(user1TokenClient, 'user1', t); + await resetTokens(user2TokenClient, 'user2', t); + + const eventhub = user1TokenClient._channel.getChannelEventHub('localhost:7051'); + + // build request for user2 to issue a token to user1 + let txId = user2TokenClient.getClient().newTransactionID(); + let param = { + recipient: {type: fabprotos.token.TokenOwner_MSP_IDENTIFIER, raw: user1Identity.serialize()}, + type: 'abc123', + quantity: 210 + }; + let request = { + params: [param], + txId: txId, + }; + + // user2 issues token to user1 + let result = await user2TokenClient.issue(request); + logger.debug('issue returns: %s', util.inspect(result, {depth: null})); + t.equals(result.status, 'SUCCESS', 'Successfully sent issue token transaction to orderer. Waiting for transaction to be committed ...'); + await waitForTxEvent(eventhub, txId.getTransactionID(), 'VALID', t); + + // user1 lists tokens after issue + result = await user1TokenClient.list(); + logger.debug('\nuser1(org1) listed %d tokens after issue: \n%s', result.length, util.inspect(result, false, null)); + validateTokens(result, [param], 'for user1 (recipient) after issue', t); + + const transferToken = result[0]; + + // build request for user1 to transfer transfer token to user2 + txId = user1TokenClient.getClient().newTransactionID(); + param = { + recipient: {type: fabprotos.token.TokenOwner_MSP_IDENTIFIER, raw: user2Identity.serialize()}, + quantity: 10 + }; + request = { + tokenIds: [transferToken.id], + params: [param], + txId: txId, + }; + + // user1 transfers token to user2 + result = await user1TokenClient.transfer(request); + logger.debug('transfer returns: %s', util.inspect(result, {depth: null})); + t.equals(result.status, 'SUCCESS', 'Successfully sent transfer token transaction to orderer. Waiting for transaction to be committed ...'); + await waitForTxEvent(eventhub, txId.getTransactionID(), 'INVALID_OTHER_REASON', t); + + t.end(); + } catch (err) { + logger.error(err); + t.fail('Failed to test token commands due to error: ' + err.stack ? err.stack : err); + t.end(); + } +}); + +test('\n\n***** Token end-to-end flow: invalid redeem amount fails *****\n\n', async (t) => { + try { + // create TokenClient for user1 (admin user in org1) + const user1ClientObj = await e2eUtils.createTokenClient('org1', channel_name, 'localhost:7051', t); + const user1TokenClient = user1ClientObj.tokenClient; + const user1Identity = user1ClientObj.user.getIdentity(); + + // create TokenClient for user2 (admin user in org2) + const user2ClientObj = await e2eUtils.createTokenClient('org2', channel_name, 'localhost:8051', t); + const user2TokenClient = user2ClientObj.tokenClient; + + await resetTokens(user1TokenClient, 'user1', t); + await resetTokens(user2TokenClient, 'user2', t); + + const eventhub = user1TokenClient._channel.getChannelEventHub('localhost:7051'); + + // build request for user2 to issue a token to user1 + let txId = user2TokenClient.getClient().newTransactionID(); + let param = { + recipient: {type: fabprotos.token.TokenOwner_MSP_IDENTIFIER, raw: user1Identity.serialize()}, + type: 'abc123', + quantity: 100 + }; + let request = { + params: [param], + txId: txId, + }; + + // user2 issues token to user1 + let result = await user2TokenClient.issue(request); + logger.debug('issue returns: %s', util.inspect(result, {depth: null})); + t.equals(result.status, 'SUCCESS', 'Successfully sent issue token transaction to orderer. Waiting for transaction to be committed ...'); + await waitForTxEvent(eventhub, txId.getTransactionID(), 'VALID', t); + + // user1 lists tokens after issue + result = await user1TokenClient.list(); + logger.debug('\nuser1(org1) listed %d tokens after issue: \n%s', result.length, util.inspect(result, false, null)); + validateTokens(result, [param], 'for user1 (recipient) after issue', t); + + const redeemToken = result[0]; + + // build request for user1 to transfer transfer token to user2 + txId = user1TokenClient.getClient().newTransactionID(); + param = { + quantity: 110 + }; + request = { + tokenIds: [redeemToken.id], + params: [param], + txId: txId, + }; + + // user1 transfers token to user2 + result = await user1TokenClient.redeem(request); + t.fail('Redeem failed because redeem quantity exceeded the token quantity'); + + t.end(); + } catch (err) { + t.equals( + err.message, + 'command response has error: total quantity [100] from TokenIds is less than quantity [110] to be redeemed', + 'Redeem failed as expected because redeemed quantity exceeded token quantity' + ); + t.end(); + } +}); + +// list to tx event and verify that validation code matches expected code +async function waitForTxEvent(eventhub, transactionID, expectedCode, t) { + logger.debug('waitForTxEvent start, transactionID: %s', transactionID); + const txPromise = new Promise((resolve, reject) => { + const handle = setTimeout(() => { + eventhub.disconnect(); + t.fail('REQUEST_TIMEOUT -- eventhub did not respond'); + reject(new Error('REQUEST_TIMEOUT:' + eventhub.getPeerAddr())); + }, 30000); + + eventhub.registerTxEvent(transactionID, (tx, code) => { + clearTimeout(handle); + + const action = expectedCode === 'VALID' ? 'committed' : 'invalidated'; + t.equals(code, expectedCode, 'Transaction has been successfully ' + action + ' on peer ' + eventhub.getPeerAddr()); + if (code === expectedCode) { + // t.pass('transaction has been committed or invalidated on peer ' + eventhub.getPeerAddr()); + resolve(); + } else { + // t.fail('transaction has wrong validation code, expected ' + expectedCode + ', but got code ' + code); + reject(new Error('INVALID:' + code)); + } + }, (error) => { + clearTimeout(handle); + + t.fail('Event registration for this transaction was invalid ::' + error); + reject(error); + }, + {disconnect: true} + ); + + eventhub.connect(); + }); + return txPromise; +} + +function validateTokens(actual, expected, message, t) { + t.equals(actual.length, expected.length, 'Validating number of tokens ' + message); + for (const actualToken of actual) { + let found = false; + for (const expectedToken of expected) { + if (actualToken.type === expectedToken.type) { + found = true; + t.equals(actualToken.type, expectedToken.type, 'Validating token type ' + message); + // compare quantity based on if it is a Long or simple integer + if (expectedToken.quantity.low) { + t.equals(actualToken.quantity.low, expectedToken.quantity.low, 'Validating token quantity ' + message); + } else { + t.equals(actualToken.quantity.low, expectedToken.quantity, 'Validating token quantity ' + message); + } + break; + } + } + if (!found) { + t.fail('failed to validate token type (%s) %s', actualToken.type, message); + } + } +} + +async function resetTokens(tokenClient, userName, t) { + let request; + let result; + const tokens = await tokenClient.list(); + for (const token of tokens) { + request = {tokenIds: [token.id], params: {quantity: token.quantity}, txId: tokenClient._client.newTransactionID()}; + result = await tokenClient.redeem(request); + t.equals(result.status, 'SUCCESS', 'Successfully resetting tokens for user %s', userName); + } +} diff --git a/test/integration/tokene2e.js b/test/integration/tokene2e.js new file mode 100644 index 0000000000..e693d6940b --- /dev/null +++ b/test/integration/tokene2e.js @@ -0,0 +1,15 @@ +/* + Copyright IBM Corp. All Rights Reserved. + + SPDX-License-Identifier: Apache-2.0 +*/ + +// Set env vars for the channel name and subdir for token e2e tests + +process.env.channel = 'tokenchannel'; +process.env.channeltx_subdir = 'v2'; + +// Create and join the channel and then run token e2e tests +require('./e2e/create-channel.js'); +require('./e2e/join-channel.js'); +require('./e2e/token.js');