diff --git a/asset-transfer-events/application-javascript/.eslintignore b/asset-transfer-events/application-javascript/.eslintignore new file mode 100644 index 00000000..15958470 --- /dev/null +++ b/asset-transfer-events/application-javascript/.eslintignore @@ -0,0 +1,5 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +coverage diff --git a/asset-transfer-events/application-javascript/.eslintrc.js b/asset-transfer-events/application-javascript/.eslintrc.js new file mode 100644 index 00000000..8422f8a8 --- /dev/null +++ b/asset-transfer-events/application-javascript/.eslintrc.js @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ +'use strict'; + +module.exports = { + env: { + node: true, + mocha: true + }, + parserOptions: { + ecmaVersion: 8, + sourceType: 'script' + }, + extends: 'eslint:recommended', + rules: { + indent: ['error', 'tab'], + 'linebreak-style': ['error', 'unix'], + quotes: ['error', 'single'], + semi: ['error', 'always'], + 'no-unused-vars': ['error', { args: 'none' }], + 'no-console': 'off', + curly: 'error', + eqeqeq: 'error', + 'no-throw-literal': 'error', + 'no-var': 'error', + 'dot-notation': 'error', + 'no-trailing-spaces': 'error', + 'no-use-before-define': 'error', + 'no-useless-call': 'error', + 'no-with': 'error', + 'operator-linebreak': 'error', + yoda: 'error', + 'quote-props': ['error', 'as-needed'] + } +}; diff --git a/asset-transfer-events/application-javascript/.gitignore b/asset-transfer-events/application-javascript/.gitignore new file mode 100644 index 00000000..21b287f7 --- /dev/null +++ b/asset-transfer-events/application-javascript/.gitignore @@ -0,0 +1,14 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Coverage directory used by tools like istanbul +coverage + +# Dependency directories +node_modules/ +jspm_packages/ +package-lock.json + +wallet +!wallet/.gitkeep diff --git a/asset-transfer-events/application-javascript/app.js b/asset-transfer-events/application-javascript/app.js new file mode 100644 index 00000000..6ff641ac --- /dev/null +++ b/asset-transfer-events/application-javascript/app.js @@ -0,0 +1,545 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +/** + * Application that shows events when creating and updating an asset + * -- How to register a contract listener for chaincode events + * -- How to get the chaincode event name and value from the chaincode event + * -- How to retrieve the transaction and block information from the chaincode event + * -- How to register a block listener for full block events + * -- How to retrieve the transaction and block information from the full block event + * -- How to register to recieve private data associated with transactions when + * registering a block listener + * -- How to retreive the private data from the full block event + * -- The listener will be notified of an event at anytime. Notice that events will + * be posted by the listener after the application activity causing the ledger change + * and during other application activity unrelated to the event + * -- How to connect to a Gateway that will not use events when submitting transactions. + * This may be useful when the application does not want to wait for the peer to commit + * blocks and notify the application. + * + * To see the SDK workings, try setting the logging to be displayed on the console + * before executing this application. + * export HFC_LOGGING='{"debug":"console"}' + * See the following on how the SDK is working with the Peer's Event Services + * https://hyperledger-fabric.readthedocs.io/en/latest/peer_event_services.html + * + * See the following for more details on using the Node SDK + * https://hyperledger.github.io/fabric-sdk-node/release-2.2/module-fabric-network.html + */ + +// pre-requisites: +// - fabric-sample two organization test-network setup with two peers, ordering service, +// and 2 certificate authorities +// ===> from directory test-network +// ./network.sh up createChannel -ca +// +// - Use the asset-transfer-events/chaincode-javascript chaincode deployed on +// the channel "mychannel". The following deploy command will package, install, +// approve, and commit the javascript chaincode, all the actions it takes +// to deploy a chaincode to a channel. +// ===> from directory test-network +// ./network.sh deployCC -ccn events -ccl javascript -ccep "OR('Org1MSP.peer','Org2MSP.peer')" +// +// - Be sure that node.js is installed +// ===> from directory asset-transfer-sbe/application-javascript +// node -v +// - npm installed code dependencies +// ===> from directory asset-transfer-sbe/application-javascript +// npm install +// - to run this test application +// ===> from directory asset-transfer-sbe/application-javascript +// node app.js + +// NOTE: If you see an error like these: +/* + + Error in setup: Error: DiscoveryService: mychannel error: access denied + + OR + + Failed to register user : Error: fabric-ca request register failed with errors [[ { code: 20, message: 'Authentication failure' } ]] + + */ +// Delete the /fabric-samples/asset-transfer-sbe/application-javascript/wallet directory +// and retry this application. +// +// The certificate authority must have been restarted and the saved certificates for the +// admin and application user are not valid. Deleting the wallet store will force these to be reset +// with the new certificate authority. +// + +// use this to set logging, must be set before the require('fabric-network'); +process.env.HFC_LOGGING = '{"debug": "./debug.log"}'; + +const { Gateway, Wallets } = require('fabric-network'); +const EventStrategies = require('fabric-network/lib/impl/event/defaulteventhandlerstrategies'); +const FabricCAServices = require('fabric-ca-client'); +const path = require('path'); +const { buildCAClient, registerAndEnrollUser, enrollAdmin } = require('../../test-application/javascript/CAUtil.js'); +const { buildCCPOrg1, buildWallet } = require('../../test-application/javascript/AppUtil.js'); + +const channelName = 'mychannel'; +const chaincodeName = 'events'; + +const org1 = 'Org1MSP'; +const Org1UserId = 'appUser1'; + +const RED = '\x1b[31m\n'; +const GREEN = '\x1b[32m\n'; +const BLUE = '\x1b[34m'; +const RESET = '\x1b[0m'; + +/** + * Perform a sleep -- asynchronous wait + * @param ms the time in milliseconds to sleep for + */ +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +async function initGatewayForOrg1(useCommitEvents) { + console.log(`${GREEN}--> Fabric client user & Gateway init: Using Org1 identity to Org1 Peer${RESET}`); + // build an in memory object with the network configuration (also known as a connection profile) + const ccpOrg1 = buildCCPOrg1(); + + // build an instance of the fabric ca services client based on + // the information in the network configuration + const caOrg1Client = buildCAClient(FabricCAServices, ccpOrg1, 'ca.org1.example.com'); + + // setup the wallet to cache the credentials of the application user, on the app server locally + const walletPathOrg1 = path.join(__dirname, 'wallet', 'org1'); + const walletOrg1 = await buildWallet(Wallets, walletPathOrg1); + + // in a real application this would be done on an administrative flow, and only once + // stores admin identity in local wallet, if needed + await enrollAdmin(caOrg1Client, walletOrg1, org1); + // register & enroll application user with CA, which is used as client identify to make chaincode calls + // and stores app user identity in local wallet + // In a real application this would be done only when a new user was required to be added + // and would be part of an administrative flow + await registerAndEnrollUser(caOrg1Client, walletOrg1, org1, Org1UserId, 'org1.department1'); + + try { + // Create a new gateway for connecting to Org's peer node. + const gatewayOrg1 = new Gateway(); + + if (useCommitEvents) { + await gatewayOrg1.connect(ccpOrg1, { + wallet: walletOrg1, + identity: Org1UserId, + discovery: { enabled: true, asLocalhost: true } + }); + } else { + await gatewayOrg1.connect(ccpOrg1, { + wallet: walletOrg1, + identity: Org1UserId, + discovery: { enabled: true, asLocalhost: true }, + eventHandlerOptions: EventStrategies.NONE + }); + } + + + return gatewayOrg1; + } catch (error) { + console.error(`Error in connecting to gateway for Org1: ${error}`); + process.exit(1); + } +} + +function checkAsset(org, resultBuffer, color, size, owner, appraisedValue, price) { + console.log(`${GREEN}<-- Query results from ${org}${RESET}`); + + let asset; + if (resultBuffer) { + asset = JSON.parse(resultBuffer.toString('utf8')); + } else { + console.log(`${RED}*** Failed to read asset${RESET}`); + } + console.log(`*** verify asset ${asset.ID}`); + + if (asset) { + if (asset.Color === color) { + console.log(`*** asset ${asset.ID} has color ${asset.Color}`); + } else { + console.log(`${RED}*** asset ${asset.ID} has color of ${asset.Color}${RESET}`); + } + if (asset.Size === size) { + console.log(`*** asset ${asset.ID} has size ${asset.Size}`); + } else { + console.log(`${RED}*** Failed size check from ${org} - asset ${asset.ID} has size of ${asset.Size}${RESET}`); + } + if (asset.Owner === owner) { + console.log(`*** asset ${asset.ID} owned by ${asset.Owner}`); + } else { + console.log(`${RED}*** Failed owner check from ${org} - asset ${asset.ID} owned by ${asset.Owner}${RESET}`); + } + if (asset.AppraisedValue === appraisedValue) { + console.log(`*** asset ${asset.ID} has appraised value ${asset.AppraisedValue}`); + } else { + console.log(`${RED}*** Failed appraised value check from ${org} - asset ${asset.ID} has appraised value of ${asset.AppraisedValue}${RESET}`); + } + if (price) { + if (asset.asset_properties && asset.asset_properties.Price === price) { + console.log(`*** asset ${asset.ID} has price ${asset.asset_properties.Price}`); + } else { + console.log(`${RED}*** Failed price check from ${org} - asset ${asset.ID} has price of ${asset.asset_properties.Price}${RESET}`); + } + } + } +} + +function showTransactionData(transactionData) { + const creator = transactionData.actions[0].header.creator; + console.log(` - submitted by: ${creator.mspid}-${creator.id_bytes.toString('hex')}`); + for (const endorsement of transactionData.actions[0].payload.action.endorsements) { + console.log(` - endorsed by: ${endorsement.endorser.mspid}-${endorsement.endorser.id_bytes.toString('hex')}`); + } + const chaincode = transactionData.actions[0].payload.chaincode_proposal_payload.input.chaincode_spec; + console.log(` - chaincode:${chaincode.chaincode_id.name}`); + console.log(` - function:${chaincode.input.args[0].toString()}`); + for (let x = 1; x < chaincode.input.args.length; x++) { + console.log(` - arg:${chaincode.input.args[x].toString()}`); + } +} + +async function main() { + console.log(`${BLUE} **** START ****${RESET}`); + try { + let randomNumber = Math.floor(Math.random() * 1000) + 1; + // use a random key so that we can run multiple times + let assetKey = `item-${randomNumber}`; + + /** ******* Fabric client init: Using Org1 identity to Org1 Peer ******* */ + const gateway1Org1 = await initGatewayForOrg1(true); // transaction handling uses commit events + const gateway2Org1 = await initGatewayForOrg1(); + + try { + // + // - - - - - - C H A I N C O D E E V E N T S + // + console.log(`${BLUE} **** CHAINCODE EVENTS ****${RESET}`); + let transaction; + let listener; + const network1Org1 = await gateway1Org1.getNetwork(channelName); + const contract1Org1 = network1Org1.getContract(chaincodeName); + + try { + // first create a listener to be notified of chaincode code events + // coming from the chaincode ID "events" + listener = async (event) => { + // The payload of the chaincode event is the value place there by the + // chaincode. Notice it is a byte data and the application will have + // to know how to deserialize. + // In this case we know that the chaincode will always place the asset + // being worked with as the payload for all events produced. + const asset = JSON.parse(event.payload.toString()); + console.log(`${GREEN}<-- Contract Event Received: ${event.eventName} - ${JSON.stringify(asset)}${RESET}`); + // show the information available with the event + console.log(`*** Event: ${event.eventName}:${asset.ID}`); + // notice how we have access to the transaction information that produced this chaincode event + const eventTransaction = event.getTransactionEvent(); + console.log(`*** transaction: ${eventTransaction.transactionId} status:${eventTransaction.status}`); + showTransactionData(eventTransaction.transactionData); + // notice how we have access to the full block that contains this transaction + const eventBlock = eventTransaction.getBlockEvent(); + console.log(`*** block: ${eventBlock.blockNumber.toString()}`); + }; + // now start the client side event service and register the listener + console.log(`${GREEN}--> Start contract event stream to peer in Org1${RESET}`); + await contract1Org1.addContractListener(listener); + } catch (eventError) { + console.log(`${RED}<-- Failed: Setup contract events - ${eventError}${RESET}`); + } + + try { + // C R E A T E + console.log(`${GREEN}--> Submit Transaction: CreateAsset, ${assetKey} owned by Sam${RESET}`); + transaction = contract1Org1.createTransaction('CreateAsset'); + await transaction.submit(assetKey, 'blue', '10', 'Sam', '100'); + console.log(`${GREEN}<-- Submit CreateAsset Result: committed, asset ${assetKey}${RESET}`); + } catch (createError) { + console.log(`${RED}<-- Submit Failed: CreateAsset - ${createError}${RESET}`); + } + try { + // R E A D + console.log(`${GREEN}--> Evaluate: ReadAsset, - ${assetKey} should be owned by Sam${RESET}`); + const resultBuffer = await contract1Org1.evaluateTransaction('ReadAsset', assetKey); + checkAsset(org1, resultBuffer, 'blue', '10', 'Sam', '100'); + } catch (readError) { + console.log(`${RED}<-- Failed: ReadAsset - ${readError}${RESET}`); + } + + try { + // U P D A T E + console.log(`${GREEN}--> Submit Transaction: UpdateAsset ${assetKey} update appraised value to 200`); + transaction = contract1Org1.createTransaction('UpdateAsset'); + await transaction.submit(assetKey, 'blue', '10', 'Sam', '200'); + console.log(`${GREEN}<-- Submit UpdateAsset Result: committed, asset ${assetKey}${RESET}`); + } catch (updateError) { + console.log(`${RED}<-- Failed: UpdateAsset - ${updateError}${RESET}`); + } + try { + // R E A D + console.log(`${GREEN}--> Evaluate: ReadAsset, - ${assetKey} should now have appraised value of 200${RESET}`); + const resultBuffer = await contract1Org1.evaluateTransaction('ReadAsset', assetKey); + checkAsset(org1, resultBuffer, 'blue', '10', 'Sam', '200'); + } catch (readError) { + console.log(`${RED}<-- Failed: ReadAsset - ${readError}${RESET}`); + } + + try { + // T R A N S F E R + console.log(`${GREEN}--> Submit Transaction: TransferAsset ${assetKey} to Mary`); + transaction = contract1Org1.createTransaction('TransferAsset'); + await transaction.submit(assetKey, 'Mary'); + console.log(`${GREEN}<-- Submit TransferAsset Result: committed, asset ${assetKey}${RESET}`); + } catch (transferError) { + console.log(`${RED}<-- Failed: TransferAsset - ${transferError}${RESET}`); + } + try { + // R E A D + console.log(`${GREEN}--> Evaluate: ReadAsset, - ${assetKey} should now be owned by Mary${RESET}`); + const resultBuffer = await contract1Org1.evaluateTransaction('ReadAsset', assetKey); + checkAsset(org1, resultBuffer, 'blue', '10', 'Mary', '200'); + } catch (readError) { + console.log(`${RED}<-- Failed: ReadAsset - ${readError}${RESET}`); + } + + try { + // D E L E T E + console.log(`${GREEN}--> Submit Transaction: DeleteAsset ${assetKey}`); + transaction = contract1Org1.createTransaction('DeleteAsset'); + await transaction.submit(assetKey); + console.log(`${GREEN}<-- Submit DeleteAsset Result: committed, asset ${assetKey}${RESET}`); + } catch (deleteError) { + console.log(`${RED}<-- Failed: DeleteAsset - ${deleteError}${RESET}`); + if (deleteError.toString().includes('ENDORSEMENT_POLICY_FAILURE')) { + console.log(`${RED}Be sure that chaincode was deployed with the endorsement policy "OR('Org1MSP.peer','Org2MSP.peer')"${RESET}`) + } + } + try { + // R E A D + console.log(`${GREEN}--> Evaluate: ReadAsset, - ${assetKey} should now be deleted${RESET}`); + const resultBuffer = await contract1Org1.evaluateTransaction('ReadAsset', assetKey); + checkAsset(org1, resultBuffer, 'blue', '10', 'Mary', '200'); + console.log(`${RED}<-- Failed: ReadAsset - should not have read this asset${RESET}`); + } catch (readError) { + console.log(`${GREEN}<-- Success: ReadAsset - ${readError}${RESET}`); + } + + // all done with this listener + contract1Org1.removeContractListener(listener); + + // + // - - - - - - B L O C K E V E N T S with P R I V A T E D A T A + // + console.log(`${BLUE} **** BLOCK EVENTS with PRIVATE DATA ****${RESET}`); + const network2Org1 = await gateway2Org1.getNetwork(channelName); + const contract2Org1 = network2Org1.getContract(chaincodeName); + + randomNumber = Math.floor(Math.random() * 1000) + 1; + assetKey = `item-${randomNumber}`; + + let firstBlock = true; // simple indicator to track blocks + + try { + let listener; + + // create a block listener + listener = async (event) => { + if (firstBlock) { + console.log(`${GREEN}<-- Block Event Received - block number: ${event.blockNumber.toString()}` + + '\n### Note:' + + '\n This block event represents the current top block of the ledger.' + + `\n All block events after this one are events that represent new blocks added to the ledger${RESET}`); + firstBlock = false; + } else { + console.log(`${GREEN}<-- Block Event Received - block number: ${event.blockNumber.toString()}${RESET}`); + } + const transEvents = event.getTransactionEvents(); + for (const transEvent of transEvents) { + console.log(`*** transaction event: ${transEvent.transactionId}`); + if (transEvent.privateData) { + for (const namespace of transEvent.privateData.ns_pvt_rwset) { + console.log(` - private data: ${namespace.namespace}`); + for (const collection of namespace.collection_pvt_rwset) { + console.log(` - collection: ${collection.collection_name}`); + if (collection.rwset.reads) { + for (const read of collection.rwset.reads) { + console.log(` - read set - ${BLUE}key:${RESET} ${read.key} ${BLUE}value:${read.value.toString()}`); + } + } + if (collection.rwset.writes) { + for (const write of collection.rwset.writes) { + console.log(` - write set - ${BLUE}key:${RESET}${write.key} ${BLUE}is_delete:${RESET}${write.is_delete} ${BLUE}value:${RESET}${write.value.toString()}`); + } + } + } + } + } + if (transEvent.transactionData) { + showTransactionData(transEvent.transactionData); + } + } + }; + // now start the client side event service and register the listener + console.log(`${GREEN}--> Start private data block event stream to peer in Org1${RESET}`); + await network2Org1.addBlockListener(listener, {type: 'private'}); + } catch (eventError) { + console.log(`${RED}<-- Failed: Setup block events - ${eventError}${RESET}`); + } + + try { + // C R E A T E + console.log(`${GREEN}--> Submit Transaction: CreateAsset, ${assetKey} owned by Sam${RESET}`); + transaction = contract2Org1.createTransaction('CreateAsset'); + + // create the private data with salt and assign to the transaction + const randomNumber = Math.floor(Math.random() * 100) + 1; + const asset_properties = { + object_type: 'asset_properties', + asset_id: assetKey, + Price: '90', + salt: Buffer.from(randomNumber.toString()).toString('hex') + }; + const asset_properties_string = JSON.stringify(asset_properties); + transaction.setTransient({ + asset_properties: Buffer.from(asset_properties_string) + }); + // With the addition of private data to the transaction + // We must only send this to the organization that will be + // saving the private data or we will get an endorsement policy failure + transaction.setEndorsingOrganizations(org1); + // endorse and commit - private data (transient data) will be + // saved to the implicit collection on the peer + await transaction.submit(assetKey, 'blue', '10', 'Sam', '100'); + console.log(`${GREEN}<-- Submit CreateAsset Result: committed, asset ${assetKey}${RESET}`); + } catch (createError) { + console.log(`${RED}<-- Failed: CreateAsset - ${createError}${RESET}`); + } + await sleep(5000); // need to wait for event to be committed + try { + // R E A D + console.log(`${GREEN}--> Evaluate: ReadAsset, - ${assetKey} should be owned by Sam${RESET}`); + const resultBuffer = await contract2Org1.evaluateTransaction('ReadAsset', assetKey); + checkAsset(org1, resultBuffer, 'blue', '10', 'Sam', '100', '90'); + } catch (readError) { + console.log(`${RED}<-- Failed: ReadAsset - ${readError}${RESET}`); + } + + try { + // U P D A T E + console.log(`${GREEN}--> Submit Transaction: UpdateAsset ${assetKey} update appraised value to 200`); + transaction = contract2Org1.createTransaction('UpdateAsset'); + + // update the private data with new salt and assign to the transaction + const randomNumber = Math.floor(Math.random() * 100) + 1; + const asset_properties = { + object_type: 'asset_properties', + asset_id: assetKey, + Price: '90', + salt: Buffer.from(randomNumber.toString()).toString('hex') + }; + const asset_properties_string = JSON.stringify(asset_properties); + transaction.setTransient({ + asset_properties: Buffer.from(asset_properties_string) + }); + transaction.setEndorsingOrganizations(org1); + + await transaction.submit(assetKey, 'blue', '10', 'Sam', '200'); + console.log(`${GREEN}<-- Submit UpdateAsset Result: committed, asset ${assetKey}${RESET}`); + } catch (updateError) { + console.log(`${RED}<-- Failed: UpdateAsset - ${updateError}${RESET}`); + } + await sleep(5000); // need to wait for event to be committed + try { + // R E A D + console.log(`${GREEN}--> Evaluate: ReadAsset, - ${assetKey} should now have appraised value of 200${RESET}`); + const resultBuffer = await contract2Org1.evaluateTransaction('ReadAsset', assetKey); + checkAsset(org1, resultBuffer, 'blue', '10', 'Sam', '200', '90'); + } catch (readError) { + console.log(`${RED}<-- Failed: ReadAsset - ${readError}${RESET}`); + } + + try { + // T R A N S F E R + console.log(`${GREEN}--> Submit Transaction: TransferAsset ${assetKey} to Mary`); + transaction = contract2Org1.createTransaction('TransferAsset'); + + // update the private data with new salt and assign to the transaction + const randomNumber = Math.floor(Math.random() * 100) + 1; + const asset_properties = { + object_type: 'asset_properties', + asset_id: assetKey, + Price: '180', + salt: Buffer.from(randomNumber.toString()).toString('hex') + }; + const asset_properties_string = JSON.stringify(asset_properties); + transaction.setTransient({ + asset_properties: Buffer.from(asset_properties_string) + }); + transaction.setEndorsingOrganizations(org1); + + await transaction.submit(assetKey, 'Mary'); + console.log(`${GREEN}<-- Submit TransferAsset Result: committed, asset ${assetKey}${RESET}`); + } catch (transferError) { + console.log(`${RED}<-- Failed: TransferAsset - ${transferError}${RESET}`); + } + await sleep(5000); // need to wait for event to be committed + try { + // R E A D + console.log(`${GREEN}--> Evaluate: ReadAsset, - ${assetKey} should now be owned by Mary${RESET}`); + const resultBuffer = await contract2Org1.evaluateTransaction('ReadAsset', assetKey); + checkAsset(org1, resultBuffer, 'blue', '10', 'Mary', '200', '180'); + } catch (readError) { + console.log(`${RED}<-- Failed: ReadAsset - ${readError}${RESET}`); + } + + try { + // D E L E T E + console.log(`${GREEN}--> Submit Transaction: DeleteAsset ${assetKey}`); + transaction = contract2Org1.createTransaction('DeleteAsset'); + await transaction.submit(assetKey); + console.log(`${GREEN}<-- Submit DeleteAsset Result: committed, asset ${assetKey}${RESET}`); + } catch (deleteError) { + console.log(`${RED}<-- Failed: DeleteAsset - ${deleteError}${RESET}`); + } + await sleep(5000); // need to wait for event to be committed + try { + // R E A D + console.log(`${GREEN}--> Evaluate: ReadAsset, - ${assetKey} should now be deleted${RESET}`); + const resultBuffer = await contract2Org1.evaluateTransaction('ReadAsset', assetKey); + checkAsset(org1, resultBuffer, 'blue', '10', 'Mary', '200'); + console.log(`${RED}<-- Failed: ReadAsset - should not have read this asset${RESET}`); + } catch (readError) { + console.log(`${GREEN}<-- Success: ReadAsset - ${readError}${RESET}`); + } + + // all done with this listener + network2Org1.removeBlockListener(listener); + + } catch (runError) { + console.error(`Error in transaction: ${runError}`); + if (runError.stack) { + console.error(runError.stack); + } + } + } catch (error) { + console.error(`Error in setup: ${error}`); + if (error.stack) { + console.error(error.stack); + } + process.exit(1); + } + + await sleep(5000); + console.log(`${BLUE} **** END ****${RESET}`); + process.exit(0); +} +main(); diff --git a/asset-transfer-events/application-javascript/package.json b/asset-transfer-events/application-javascript/package.json new file mode 100644 index 00000000..2767aede --- /dev/null +++ b/asset-transfer-events/application-javascript/package.json @@ -0,0 +1,16 @@ +{ + "name": "asset-transfer-events", + "version": "1.0.0", + "description": "Javascript application that uses chaincode events and block events with private data", + "engines": { + "node": ">=12", + "npm": ">=5" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-ca-client": "^2.2.2", + "fabric-network": "^2.2.2" + } +} diff --git a/asset-transfer-events/chaincode-javascript/.eslintignore b/asset-transfer-events/chaincode-javascript/.eslintignore new file mode 100644 index 00000000..15958470 --- /dev/null +++ b/asset-transfer-events/chaincode-javascript/.eslintignore @@ -0,0 +1,5 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +coverage diff --git a/asset-transfer-events/chaincode-javascript/.eslintrc.js b/asset-transfer-events/chaincode-javascript/.eslintrc.js new file mode 100644 index 00000000..cb00fa96 --- /dev/null +++ b/asset-transfer-events/chaincode-javascript/.eslintrc.js @@ -0,0 +1,39 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ +'use strict'; + +module.exports = { + env: { + node: true, + mocha: true, + es6: true + }, + parserOptions: { + ecmaVersion: 8, + sourceType: 'script' + }, + extends: 'eslint:recommended', + rules: { + indent: ['error', 'tab'], + 'linebreak-style': ['error', 'unix'], + quotes: ['error', 'single'], + semi: ['error', 'always'], + 'no-unused-vars': ['error', { args: 'none' }], + 'no-console': 'off', + curly: 'error', + eqeqeq: 'error', + 'no-throw-literal': 'error', + strict: 'error', + 'no-var': 'error', + 'dot-notation': 'error', + 'no-trailing-spaces': 'error', + 'no-use-before-define': 'error', + 'no-useless-call': 'error', + 'no-with': 'error', + 'operator-linebreak': 'error', + yoda: 'error', + 'quote-props': ['error', 'as-needed'], + 'no-constant-condition': ['error', { checkLoops: false }] + } +}; diff --git a/asset-transfer-events/chaincode-javascript/.gitignore b/asset-transfer-events/chaincode-javascript/.gitignore new file mode 100644 index 00000000..eeace290 --- /dev/null +++ b/asset-transfer-events/chaincode-javascript/.gitignore @@ -0,0 +1,15 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + +# Coverage directory used by tools like istanbul +coverage + +# Report cache used by istanbul +.nyc_output + +# Dependency directories +node_modules/ +jspm_packages/ + +package-lock.json diff --git a/asset-transfer-events/chaincode-javascript/index.js b/asset-transfer-events/chaincode-javascript/index.js new file mode 100644 index 00000000..3244cedf --- /dev/null +++ b/asset-transfer-events/chaincode-javascript/index.js @@ -0,0 +1,12 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const assetTransferEvents = require('./lib/assetTransferEvents'); + +module.exports.AssetTransferEvents = assetTransferEvents; +module.exports.contracts = [assetTransferEvents]; diff --git a/asset-transfer-events/chaincode-javascript/lib/assetTransferEvents.js b/asset-transfer-events/chaincode-javascript/lib/assetTransferEvents.js new file mode 100644 index 00000000..27c5acbd --- /dev/null +++ b/asset-transfer-events/chaincode-javascript/lib/assetTransferEvents.js @@ -0,0 +1,128 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict'; + +const { Contract } = require('fabric-contract-api'); + +async function savePrivateData(ctx, assetKey) { + const clientOrg = ctx.clientIdentity.getMSPID(); + const peerOrg = ctx.stub.getMspID(); + const collection = '_implicit_org_' + peerOrg; + + if (clientOrg === peerOrg) { + const transientMap = ctx.stub.getTransient(); + if (transientMap) { + const properties = transientMap.get('asset_properties'); + if (properties) { + await ctx.stub.putPrivateData(collection, assetKey, properties); + } + } + } +} + +async function removePrivateData(ctx, assetKey) { + const clientOrg = ctx.clientIdentity.getMSPID(); + const peerOrg = ctx.stub.getMspID(); + const collection = '_implicit_org_' + peerOrg; + + if (clientOrg === peerOrg) { + const propertiesBuffer = await ctx.stub.getPrivateData(collection, assetKey); + if (propertiesBuffer && propertiesBuffer.length > 0) { + await ctx.stub.deletePrivateData(collection, assetKey); + } + } +} + +async function addPrivateData(ctx, assetKey, asset) { + const clientOrg = ctx.clientIdentity.getMSPID(); + const peerOrg = ctx.stub.getMspID(); + const collection = '_implicit_org_' + peerOrg; + + if (clientOrg === peerOrg) { + const propertiesBuffer = await ctx.stub.getPrivateData(collection, assetKey); + if (propertiesBuffer && propertiesBuffer.length > 0) { + const properties = JSON.parse(propertiesBuffer.toString()); + asset.asset_properties = properties; + } + } +} + +async function readState(ctx, id) { + const assetBuffer = await ctx.stub.getState(id); // get the asset from chaincode state + if (!assetBuffer || assetBuffer.length === 0) { + throw new Error(`The asset ${id} does not exist`); + } + const assetString = assetBuffer.toString(); + const asset = JSON.parse(assetString); + + return asset; +} + +class AssetTransferEvents extends Contract { + + // CreateAsset issues a new asset to the world state with given details. + async CreateAsset(ctx, id, color, size, owner, appraisedValue) { + const asset = { + ID: id, + Color: color, + Size: size, + Owner: owner, + AppraisedValue: appraisedValue, + }; + await savePrivateData(ctx, id); + const assetBuffer = Buffer.from(JSON.stringify(asset)); + + ctx.stub.setEvent('CreateAsset', assetBuffer); + return ctx.stub.putState(id, assetBuffer); + } + + // TransferAsset updates the owner field of an asset with the given id in + // the world state. + async TransferAsset(ctx, id, newOwner) { + const asset = await readState(ctx, id); + asset.Owner = newOwner; + const assetBuffer = Buffer.from(JSON.stringify(asset)); + await savePrivateData(ctx, id); + + ctx.stub.setEvent('TransferAsset', assetBuffer); + return ctx.stub.putState(id, assetBuffer); + } + + // ReadAsset returns the asset stored in the world state with given id. + async ReadAsset(ctx, id) { + const asset = await readState(ctx, id); + await addPrivateData(ctx, asset.ID, asset); + + return JSON.stringify(asset); + } + + // UpdateAsset updates an existing asset in the world state with provided parameters. + async UpdateAsset(ctx, id, color, size, owner, appraisedValue) { + const asset = await readState(ctx, id); + asset.Color = color; + asset.Size = size; + asset.Owner = owner; + asset.AppraisedValue = appraisedValue; + const assetBuffer = Buffer.from(JSON.stringify(asset)); + await savePrivateData(ctx, id); + + ctx.stub.setEvent('UpdateAsset', assetBuffer); + return ctx.stub.putState(id, assetBuffer); + } + + // DeleteAsset deletes an given asset from the world state. + async DeleteAsset(ctx, id) { + const asset = await readState(ctx, id); + const assetBuffer = Buffer.from(JSON.stringify(asset)); + await removePrivateData(ctx, id); + + ctx.stub.setEvent('DeleteAsset', assetBuffer); + return ctx.stub.deleteState(id); + } +} + +module.exports = AssetTransferEvents; diff --git a/asset-transfer-events/chaincode-javascript/package.json b/asset-transfer-events/chaincode-javascript/package.json new file mode 100644 index 00000000..143b35c7 --- /dev/null +++ b/asset-transfer-events/chaincode-javascript/package.json @@ -0,0 +1,49 @@ +{ + "name": "asset-transfer-events", + "version": "1.0.0", + "description": "Asset-Transfer-Events contract implemented in JavaScript", + "main": "index.js", + "engines": { + "node": ">=12", + "npm": ">=5" + }, + "scripts": { + "lint": "eslint .", + "pretest": "npm run lint", + "test": "nyc mocha --recursive", + "start": "fabric-chaincode-node start" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "fabric-contract-api": "^2.0.0", + "fabric-shim": "^2.0.0" + }, + "devDependencies": { + "chai": "^4.1.2", + "eslint": "^4.19.1", + "mocha": "^8.0.1", + "nyc": "^14.1.1", + "sinon": "^6.0.0", + "sinon-chai": "^3.2.0" + }, + "nyc": { + "exclude": [ + "coverage/**", + "test/**", + "index.js", + ".eslintrc.js" + ], + "reporter": [ + "text-summary", + "html" + ], + "all": true, + "check-coverage": true, + "statements": 100, + "branches": 100, + "functions": 100, + "lines": 100 + } +} diff --git a/asset-transfer-events/chaincode-javascript/test/assetTransferEvents.test.js b/asset-transfer-events/chaincode-javascript/test/assetTransferEvents.test.js new file mode 100644 index 00000000..b243cdb9 --- /dev/null +++ b/asset-transfer-events/chaincode-javascript/test/assetTransferEvents.test.js @@ -0,0 +1,224 @@ +'use strict'; +const sinon = require('sinon'); +const chai = require('chai'); +const sinonChai = require('sinon-chai'); +const expect = chai.expect; + +const { Context } = require('fabric-contract-api'); +const { ChaincodeStub, ClientIdentity } = require('fabric-shim'); + +const AssetTransfer = require('../lib/assetTransferEvents.js'); + +let assert = sinon.assert; +chai.use(sinonChai); + +describe('Asset Transfer Events Tests', () => { + let transactionContext, chaincodeStub, clientIdentity, asset; + let transientMap, asset_properties; + + beforeEach(() => { + transactionContext = new Context(); + + chaincodeStub = sinon.createStubInstance(ChaincodeStub); + chaincodeStub.getMspID.returns('org1'); + transactionContext.setChaincodeStub(chaincodeStub); + + clientIdentity = sinon.createStubInstance(ClientIdentity); + clientIdentity.getMSPID.returns('org1'); + transactionContext.clientIdentity = clientIdentity; + + chaincodeStub.putState.callsFake((key, value) => { + if (!chaincodeStub.states) { + chaincodeStub.states = {}; + } + chaincodeStub.states[key] = value; + }); + + chaincodeStub.getState.callsFake(async (key) => { + let ret; + if (chaincodeStub.states) { + ret = chaincodeStub.states[key]; + } + return Promise.resolve(ret); + }); + + chaincodeStub.deleteState.callsFake(async (key) => { + if (chaincodeStub.states) { + delete chaincodeStub.states[key]; + } + return Promise.resolve(key); + }); + + chaincodeStub.getStateByRange.callsFake(async () => { + function* internalGetStateByRange() { + if (chaincodeStub.states) { + // Shallow copy + const copied = Object.assign({}, chaincodeStub.states); + + for (let key in copied) { + yield {value: copied[key]}; + } + } + } + + return Promise.resolve(internalGetStateByRange()); + }); + + asset = { + ID: 'asset1', + Color: 'blue', + Size: 5, + Owner: 'Tomoko', + AppraisedValue: 300, + }; + const randomNumber = Math.floor(Math.random() * 100) + 1; + asset_properties = { + object_type: 'asset_properties', + asset_id: 'asset1', + Price: '90', + salt: Buffer.from(randomNumber.toString()).toString('hex') + }; + transientMap = { + asset_properties: Buffer.from(JSON.stringify(asset_properties)) + }; + }); + + describe('Test CreateAsset', () => { + it('should return error on CreateAsset', async () => { + chaincodeStub.putState.rejects('failed inserting key'); + + let assetTransfer = new AssetTransfer(); + try { + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + assert.fail('CreateAsset should have failed'); + } catch(err) { + expect(err.name).to.equal('failed inserting key'); + } + }); + + it('should return success on CreateAsset', async () => { + let assetTransfer = new AssetTransfer(); + + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + let ret = JSON.parse((await chaincodeStub.getState(asset.ID)).toString()); + expect(ret).to.eql(asset); + }); + it('should return success on CreateAsset with transient data', async () => { + let assetTransfer = new AssetTransfer(); + chaincodeStub.getTransient.returns(transientMap); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + let ret = JSON.parse((await chaincodeStub.getState(asset.ID)).toString()); + expect(ret).to.eql(asset); + }); + }); + + describe('Test ReadAsset', () => { + it('should return error on ReadAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + try { + await assetTransfer.ReadAsset(transactionContext, 'asset2'); + assert.fail('ReadAsset should have failed'); + } catch (err) { + expect(err.message).to.equal('The asset asset2 does not exist'); + } + }); + + it('should return success on ReadAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + const assetString = await assetTransfer.ReadAsset(transactionContext, 'asset1'); + const readAsset = JSON.parse(assetString); + expect(readAsset).to.eql(asset); + }); + + it('should return success on ReadAsset with private data', async () => { + asset.asset_properties = asset_properties; + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + chaincodeStub.getPrivateData.returns(Buffer.from(JSON.stringify(asset_properties))); + const assetString = await assetTransfer.ReadAsset(transactionContext, 'asset1'); + const readAsset = JSON.parse(assetString); + expect(readAsset).to.eql(asset); + }); + }); + + describe('Test UpdateAsset', () => { + it('should return error on UpdateAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + try { + await assetTransfer.UpdateAsset(transactionContext, 'asset2', 'orange', 10, 'Me', 500); + assert.fail('UpdateAsset should have failed'); + } catch (err) { + expect(err.message).to.equal('The asset asset2 does not exist'); + } + }); + + it('should return success on UpdateAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + await assetTransfer.UpdateAsset(transactionContext, 'asset1', 'orange', 10, 'Me', 500); + let ret = JSON.parse(await chaincodeStub.getState(asset.ID)); + let expected = { + ID: 'asset1', + Color: 'orange', + Size: 10, + Owner: 'Me', + AppraisedValue: 500 + }; + expect(ret).to.eql(expected); + }); + }); + + describe('Test DeleteAsset', () => { + it('should return error on DeleteAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + try { + await assetTransfer.DeleteAsset(transactionContext, 'asset2'); + assert.fail('DeleteAsset should have failed'); + } catch (err) { + expect(err.message).to.equal('The asset asset2 does not exist'); + } + }); + + it('should return success on DeleteAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + await assetTransfer.DeleteAsset(transactionContext, asset.ID); + let ret = await chaincodeStub.getState(asset.ID); + expect(ret).to.equal(undefined); + }); + }); + + describe('Test TransferAsset', () => { + it('should return error on TransferAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + try { + await assetTransfer.TransferAsset(transactionContext, 'asset2', 'Me'); + assert.fail('DeleteAsset should have failed'); + } catch (err) { + expect(err.message).to.equal('The asset asset2 does not exist'); + } + }); + + it('should return success on TransferAsset', async () => { + let assetTransfer = new AssetTransfer(); + await assetTransfer.CreateAsset(transactionContext, asset.ID, asset.Color, asset.Size, asset.Owner, asset.AppraisedValue); + + await assetTransfer.TransferAsset(transactionContext, asset.ID, 'Me'); + let ret = JSON.parse((await chaincodeStub.getState(asset.ID)).toString()); + expect(ret).to.eql(Object.assign({}, asset, {Owner: 'Me'})); + }); + }); +}); diff --git a/ci/azure-pipelines.yml b/ci/azure-pipelines.yml index 0314cf39..5d314a8f 100644 --- a/ci/azure-pipelines.yml +++ b/ci/azure-pipelines.yml @@ -238,3 +238,18 @@ jobs: - script: ../ci/scripts/run-test-network-secured.sh workingDirectory: test-network displayName: Run Test Network Secured Chaincode + + - job: TestNetworkEvents + displayName: Test Network + pool: + vmImage: ubuntu-18.04 + strategy: + matrix: + Events-Javascript: + CHAINCODE_NAME: events + CHAINCODE_LANGUAGE: javascript + steps: + - template: templates/install-deps.yml + - script: ../ci/scripts/run-test-network-events.sh + workingDirectory: test-network + displayName: Run Test Network Events Chaincode diff --git a/ci/scripts/run-test-network-events.sh b/ci/scripts/run-test-network-events.sh new file mode 100755 index 00000000..5899c0c4 --- /dev/null +++ b/ci/scripts/run-test-network-events.sh @@ -0,0 +1,36 @@ +set -euo pipefail + +FABRIC_VERSION=${FABRIC_VERSION:-2.2} +CHAINCODE_LANGUAGE=${CHAINCODE_LANGUAGE:-javascript} +CHAINCODE_NAME=${CHAINCODE_NAME:-events} + +function print() { + GREEN='\033[0;32m' + NC='\033[0m' + echo + echo -e "${GREEN}${1}${NC}" +} + +function createNetwork() { + print "Creating network" + ./network.sh up createChannel -ca + print "Deploying ${CHAINCODE_NAME} chaincode" + ./network.sh deployCC -ccn "${CHAINCODE_NAME}" -ccl "${CHAINCODE_LANGUAGE}" -ccep "OR('Org1MSP.peer','Org2MSP.peer')" +} + +function stopNetwork() { + print "Stopping network" + ./network.sh down +} + +# Run Javascript application +createNetwork +print "Initializing Javascript application" +pushd ../asset-transfer-events/application-javascript +npm install +print "Executing app.js" +node app.js +popd +stopNetwork +print "Remove wallet storage" +rm -R ../asset-transfer-events/application-javascript/wallet diff --git a/test-network/scripts/deployCC.sh b/test-network/scripts/deployCC.sh index 2125a794..edea87a6 100755 --- a/test-network/scripts/deployCC.sh +++ b/test-network/scripts/deployCC.sh @@ -42,6 +42,9 @@ if [ "$CC_SRC_PATH" = "NA" ]; then if [ "$CC_NAME" = "basic" ]; then println $'\e[0;32m'asset-transfer-basic$'\e[0m' chaincode CC_SRC_PATH="../asset-transfer-basic" + elif [ "$CC_NAME" = "events" ]; then + println $'\e[0;32m'asset-transfer-events$'\e[0m' chaincode + CC_SRC_PATH="../asset-transfer-events" elif [ "$CC_NAME" = "secured" ]; then println $'\e[0;32m'asset-transfer-secured-agreeement$'\e[0m' chaincode CC_SRC_PATH="../asset-transfer-secured-agreement" @@ -55,7 +58,7 @@ if [ "$CC_SRC_PATH" = "NA" ]; then println $'\e[0;32m'asset-transfer-sbe$'\e[0m' chaincode CC_SRC_PATH="../asset-transfer-sbe" else - fatalln "The chaincode name ${CC_NAME} is not supported by this script. Supported chaincode names are: basic, ledger, private, sbe, secured" + fatalln "The chaincode name ${CC_NAME} is not supported by this script. Supported chaincode names are: basic, events, ledger, private, sbe, secured" fi # now see what language it is written in