From f01eeab663aeb7bca1c549e302648e2dc77406df Mon Sep 17 00:00:00 2001 From: sapthasurendran <48531319+sapthasurendran@users.noreply.github.com> Date: Wed, 9 Mar 2022 14:21:48 +0530 Subject: [PATCH] Private data samples migration (#574) Signed-off-by: sapthasurendran Updated application flow Signed-off-by: sapthasurendran Add grpc dependency in package.json Signed-off-by: sapthasurendran Update CI pipelines to run new app Updated application description in package.json Fixed chaincode name Code Refactor Signed-off-by: sapthasurendran --- asset-transfer-private-data/README.md | 78 +++++ .../.eslintrc.json | 45 +++ .../application-gateway-typescript/.gitignore | 14 + .../application-gateway-typescript/README.md | 10 + .../package.json | 32 ++ .../application-gateway-typescript/src/app.ts | 286 ++++++++++++++++++ .../src/connect.ts | 128 ++++++++ .../tsconfig.json | 18 ++ ci/scripts/run-test-network-private.sh | 13 + 9 files changed, 624 insertions(+) create mode 100644 asset-transfer-private-data/README.md create mode 100644 asset-transfer-private-data/application-gateway-typescript/.eslintrc.json create mode 100644 asset-transfer-private-data/application-gateway-typescript/.gitignore create mode 100644 asset-transfer-private-data/application-gateway-typescript/README.md create mode 100644 asset-transfer-private-data/application-gateway-typescript/package.json create mode 100644 asset-transfer-private-data/application-gateway-typescript/src/app.ts create mode 100644 asset-transfer-private-data/application-gateway-typescript/src/connect.ts create mode 100644 asset-transfer-private-data/application-gateway-typescript/tsconfig.json diff --git a/asset-transfer-private-data/README.md b/asset-transfer-private-data/README.md new file mode 100644 index 00000000..860acbab --- /dev/null +++ b/asset-transfer-private-data/README.md @@ -0,0 +1,78 @@ +# Asset transfer private data sample + +The asset transfer private data sample demonstrates: + +- Usage of organization private data collections +- Read data from the organization private data collection. +- Store data in organization private data collection. + +For more information about private data, visit the +[Private Data](https://hyperledger-fabric.readthedocs.io/en/latest/private-data-arch.html) +page in the Fabric documentation. + +## About the sample + +This sample includes smart contract and application code in multiple languages. In a use-case similar to basic asset transfer (see [asset-transfer-basic](../asset-transfer-basic) folder) this sample shows sending and receiving of asset along with its private data owned by organizations during create / delete of an asset , and during transfer of an asset to a new owner. + +### Application + +Please refer the below link to understand the application flow. +https://hyperledger-fabric.readthedocs.io/en/latest/private-data/private-data.html#example-scenario-asset-transfer-using-private-data-collections + +### Smart Contract + +The smart contract (in folder `chaincode-xyz`) implements the following functions to support the application: + +CreateAsset +AgreeToTransfer +TransferAsset +DeleteAsset +DeleteTranferAgreement + +ReadAsset +ReadAssetPrivateDetails +ReadTransferAgreement +GetAssetByRange +QueryAssetByOwner +QueryAssets +getQueryResultForQueryString + +## Running the sample + +Like other samples, the Fabric test network is used to deploy and run this sample. Follow these steps in order: + +1. Create the test network and a channel (from the `test-network` folder). + ``` + ./network.sh up createChannel -c mychannel -ca + ``` + +2. Deploy one of the smart contract implementations (from the `test-network` folder). + ``` + # To deploy the Java chaincode implementation + ./network.sh deployCC -ccn private -ccp ../asset-transfer-private-data/chaincode-java -ccl java -ccep "OR('Org1MSP.peer','Org2MSP.peer')" -cccg '../asset-transfer-private-data/chaincode-java/collections_config.json' -ccep "OR('Org1MSP.peer','Org2MSP.peer')" + + # To deploy the go chaincode implementation + ./network.sh deployCC -ccn private -ccp ../asset-transfer-private-data/chaincode-go -ccl go -ccep "OR('Org1MSP.peer','Org2MSP.peer')" -cccg '../asset-transfer-private-data/chaincode-go/collections_config.json' -ccep "OR('Org1MSP.peer','Org2MSP.peer')" + ``` + +3. Run the application (from the `asset-transfer-private-data` folder). + ``` + # To run the Javascript sample application + cd application-javascript + npm install + node app.js + + # To run the Typescript sample application + cd application-gateway-typescript + npm install + npm start + + ``` + +## Clean up + +When you are finished, you can bring down the test network (from the `test-network` folder). The command will remove all the nodes of the test network, and delete any ledger data that you created. + +``` +./network.sh down +``` \ No newline at end of file diff --git a/asset-transfer-private-data/application-gateway-typescript/.eslintrc.json b/asset-transfer-private-data/application-gateway-typescript/.eslintrc.json new file mode 100644 index 00000000..cc7230a8 --- /dev/null +++ b/asset-transfer-private-data/application-gateway-typescript/.eslintrc.json @@ -0,0 +1,45 @@ +{ + "env": { + "node": true, + "es6": true + }, + "root": true, + "ignorePatterns": [ + "dist/" + ], + "extends": [ + "eslint:recommended" + ], + "rules": { + "indent": [ + "error", + 4 + ], + "quotes": [ + "error", + "single" + ] + }, + "overrides": [ + { + "files": [ + "**/*.ts" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "sourceType": "module", + "ecmaFeatures": { + "impliedStrict": true + } + }, + "plugins": [ + "@typescript-eslint" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ] + } + ] + } \ No newline at end of file diff --git a/asset-transfer-private-data/application-gateway-typescript/.gitignore b/asset-transfer-private-data/application-gateway-typescript/.gitignore new file mode 100644 index 00000000..99e5af9f --- /dev/null +++ b/asset-transfer-private-data/application-gateway-typescript/.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/ + +# Compiled TypeScript files +dist diff --git a/asset-transfer-private-data/application-gateway-typescript/README.md b/asset-transfer-private-data/application-gateway-typescript/README.md new file mode 100644 index 00000000..94cc79c0 --- /dev/null +++ b/asset-transfer-private-data/application-gateway-typescript/README.md @@ -0,0 +1,10 @@ +# Asset Transfer Private Data Sample + +This app uses fabric-samples/test-network based setup and the companion chaincode asset-transfer-private-data/chaincode-go/ with chaincode endorsement policy as "OR('Org1MSP.peer','Org2MSP.peer')" + +For this usecase illustration, we will use both Org1 & Org2 client identity from this same app +In real world the Org1 & Org2 identity will be used in different apps to achieve asset transfer. + +For more details refer: +https://hyperledger-fabric.readthedocs.io/en/release-2.4/private_data_tutorial.html#pd-use-case + diff --git a/asset-transfer-private-data/application-gateway-typescript/package.json b/asset-transfer-private-data/application-gateway-typescript/package.json new file mode 100644 index 00000000..272fd65c --- /dev/null +++ b/asset-transfer-private-data/application-gateway-typescript/package.json @@ -0,0 +1,32 @@ +{ + "name": "asset-transfer-private-data", + "version": "1.0.0", + "description": "Asset transfer private data application implemented in typeScript using fabric-gateway", + "main": "dist/index.js", + "typings": "dist/index.d.ts", + "engines": { + "node": ">=14" + }, + "scripts": { + "build": "tsc", + "build:watch": "tsc -w", + "lint": "eslint . --ext .ts", + "prepare": "npm run build", + "pretest": "npm run lint", + "start": "node dist/app.js" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.5.0", + "@hyperledger/fabric-gateway": "^1.0.0" + }, + "devDependencies": { + "@tsconfig/node14": "^1.0.1", + "@typescript-eslint/eslint-plugin": "^5.6.0", + "@typescript-eslint/parser": "^5.6.0", + "eslint": "^8.4.1", + "typescript": "~4.5.2" + } +} diff --git a/asset-transfer-private-data/application-gateway-typescript/src/app.ts b/asset-transfer-private-data/application-gateway-typescript/src/app.ts new file mode 100644 index 00000000..6a7e5729 --- /dev/null +++ b/asset-transfer-private-data/application-gateway-typescript/src/app.ts @@ -0,0 +1,286 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { connect, Contract } from '@hyperledger/fabric-gateway'; +import { TextDecoder } from 'util'; +import { + certPathOrg1, certPathOrg2, keyDirectoryPathOrg1, keyDirectoryPathOrg2, newGrpcConnection, newIdentity, + newSigner, peerEndpointOrg1, peerEndpointOrg2, peerNameOrg1, peerNameOrg2, tlsCertPathOrg1, tlsCertPathOrg2 +} from './connect'; + +const channelName = 'mychannel'; +const chaincodeName = 'private'; +const mspIdOrg1 = 'Org1MSP'; +const mspIdOrg2 = 'Org2MSP'; + +const utf8Decoder = new TextDecoder(); + +// Collection Names +const org1PrivateCollectionName = 'Org1MSPPrivateCollection'; +const org2PrivateCollectionName = 'Org2MSPPrivateCollection'; + +const RED = '\x1b[31m\n'; +const RESET = '\x1b[0m'; + +// Use a unique key so that we can run multiple times +const now = Date.now(); +const assetID1 = `asset${now}`; +const assetID2 = `asset${now + 1}`; + +async function main(): Promise { + const clientOrg1 = await newGrpcConnection( + tlsCertPathOrg1, + peerEndpointOrg1, + peerNameOrg1 + ); + + const gatewayOrg1 = connect({ + client: clientOrg1, + identity: await newIdentity(certPathOrg1, mspIdOrg1), + signer: await newSigner(keyDirectoryPathOrg1), + }); + + const clientOrg2 = await newGrpcConnection( + tlsCertPathOrg2, + peerEndpointOrg2, + peerNameOrg2 + ); + + const gatewayOrg2 = connect({ + client: clientOrg2, + identity: await newIdentity(certPathOrg2, mspIdOrg2), + signer: await newSigner(keyDirectoryPathOrg2), + }); + + try { + // Get the smart contract as an Org1 client. + const contractOrg1 = gatewayOrg1 + .getNetwork(channelName) + .getContract(chaincodeName); + + // Get the smart contract as an Org2 client. + const contractOrg2 = gatewayOrg2 + .getNetwork(channelName) + .getContract(chaincodeName); + + console.log('\n~~~~~~~~~~~~~~~~ As Org1 Client ~~~~~~~~~~~~~~~~'); + + // Create new assets on the ledger. + await createAssets(contractOrg1); + + // Read asset from the Org1's private data collection with ID in the given range. + await getAssetsByRange(contractOrg1); + + try{ + //Attempt to transfer asset without prior aprroval from Org2, transaction expected to fail. + console.log('\nAttempt TransferAsset without prior AgreeToTransfer'); + await transferAsset(contractOrg1, assetID1); + doFail('TransferAsset transaction succeeded when it was expected to fail'); + } + catch(e){ + console.log(`*** Received expected error: ${e}`); + } + + console.log('\n~~~~~~~~~~~~~~~~ As Org2 Client ~~~~~~~~~~~~~~~~'); + + // Read the asset by ID. + await readAssetByID(contractOrg2, assetID1); + + // Make agreement to transfer the asset from Org1 to Org2. + await agreeToTransfer(contractOrg2, assetID1); + + console.log('\n~~~~~~~~~~~~~~~~ As Org1 Client ~~~~~~~~~~~~~~~~'); + + // Read transfer agreement. + await readTransferAgreement(contractOrg1, assetID1); + + // Transfer asset to Org2. + await transferAsset(contractOrg1, assetID1); + + // Again ReadAsset : results will show that the buyer identity now owns the asset. + await readAssetByID(contractOrg1, assetID1); + + // Confirm that transfer removed the private details from the Org1 collection. + const org1ReadSuccess = await readAssetPrivateDetails(contractOrg1, assetID1, org1PrivateCollectionName); + if (org1ReadSuccess) { + doFail(`Asset private data still exists in ${org1PrivateCollectionName}`); + } + + console.log('\n~~~~~~~~~~~~~~~~ As Org2 Client ~~~~~~~~~~~~~~~~'); + + // Org2 can read asset private details: Org2 is owner, and private details exist in new owner's Collection + const org2ReadSuccess = await readAssetPrivateDetails(contractOrg2, assetID1, org2PrivateCollectionName); + if (!org2ReadSuccess) { + doFail(`Asset private data not found in ${org2PrivateCollectionName}`); + } + + try { + console.log('\nAttempt DeleteAsset using non-owner organization'); + await deleteAsset(contractOrg2, assetID2); + doFail('DeleteAsset transaction succeeded when it was expected to fail'); + } catch (e) { + console.log(`*** Received expected error: ${e}`); + } + + console.log('\n~~~~~~~~~~~~~~~~ As Org1 Client ~~~~~~~~~~~~~~~~'); + + // Delete AssetID2 as Org1. + await deleteAsset(contractOrg1, assetID2); + } finally { + gatewayOrg1.close(); + clientOrg1.close(); + + gatewayOrg2.close(); + clientOrg2.close(); + } +} + +main().catch((error) => { + console.error('******** FAILED to run the application:', error); + process.exitCode = 1; +}); + +/** + * Submit a transaction synchronously, blocking until it has been committed to the ledger. + */ +async function createAssets(contract: Contract): Promise { + const assetType = 'ValuableAsset'; + + console.log(`\n--> Submit Transaction: CreateAsset, ID: ${assetID1}`); + + const asset1Data = { + objectType: assetType, + assetID: assetID1, + color: 'green', + size: 20, + appraisedValue: 100, + }; + + await contract.submit('CreateAsset', { + transientData: { asset_properties: JSON.stringify(asset1Data) }, + }); + + console.log('*** Transaction committed successfully'); + console.log(`\n--> Submit Transaction: CreateAsset, ID: ${assetID2}`); + + const asset2Data = { + objectType: assetType, + assetID: assetID2, + color: 'blue', + size: 35, + appraisedValue: 727, + }; + + await contract.submit('CreateAsset', { + transientData: { asset_properties: JSON.stringify(asset2Data) }, + }); + + console.log('*** Transaction committed successfully'); +} + +async function getAssetsByRange(contract: Contract): Promise { + // GetAssetByRange returns assets on the ledger with ID in the range of startKey (inclusive) and endKey (exclusive). + console.log(`\n--> Evaluate Transaction: ReadAssetPrivateDetails from ${org1PrivateCollectionName}`); + + const resultBytes = await contract.evaluateTransaction( + 'GetAssetByRange', + assetID1, + `asset${now + 2}` + ); + + const resultString = utf8Decoder.decode(resultBytes); + if (!resultString) { + doFail('Received empty query list for readAssetPrivateDetailsOrg1'); + } + const result = JSON.parse(resultString); + console.log('*** Result:', result); +} + +async function readAssetByID(contract: Contract, assetID: string): Promise { + console.log(`\n--> Evaluate Transaction: ReadAsset, ID: ${assetID}`); + const resultBytes = await contract.evaluateTransaction('ReadAsset', assetID); + + const resultString = utf8Decoder.decode(resultBytes); + if (!resultString) { + doFail('Received empty result for ReadAsset'); + } + const result = JSON.parse(resultString); + console.log('*** Result:', result); +} + +async function agreeToTransfer(contract: Contract, assetID: string): Promise { + // Buyer from Org2 agrees to buy the asset// + // To purchase the asset, the buyer needs to agree to the same value as the asset owner + + const dataForAgreement = { assetID, appraisedValue: 100 }; + console.log('\n--> Submit Transaction: AgreeToTransfer, payload:', dataForAgreement); + + await contract.submit('AgreeToTransfer', { + transientData: { asset_value: JSON.stringify(dataForAgreement) }, + }); + + console.log('*** Transaction committed successfully'); +} + +async function readTransferAgreement(contract: Contract, assetID: string): Promise { + console.log(`\n--> Evaluate Transaction: ReadTransferAgreement, ID: ${assetID}`); + + const resultBytes = await contract.evaluateTransaction( + 'ReadTransferAgreement', + assetID + ); + + const resultString = utf8Decoder.decode(resultBytes); + if (!resultString) { + doFail('Received no result for ReadTransferAgreement'); + } + const result = JSON.parse(resultString); + console.log('*** Result:', result); +} + +async function transferAsset(contract: Contract, assetID: string): Promise { + console.log(`\n--> Submit Transaction: TransferAsset, ID: ${assetID}`); + + const buyerDetails = { assetID, buyerMSP: mspIdOrg2 }; + await contract.submit('TransferAsset', { + transientData: { asset_owner: JSON.stringify(buyerDetails) }, + }); + + console.log('*** Transaction committed successfully'); +} + +async function deleteAsset(contract: Contract, assetID: string): Promise { + console.log('\n--> Submit Transaction: DeleteAsset, ID:', assetID); + const dataForDelete = { assetID }; + await contract.submit('DeleteAsset', { + transientData: { asset_delete: JSON.stringify(dataForDelete) }, + }); + + console.log('*** Transaction committed successfully'); +} +async function readAssetPrivateDetails(contract: Contract, assetID: string, collectionName: string): Promise { + console.log(`\n--> Evaluate Transaction: ReadAssetPrivateDetails from ${collectionName}, ID: ${assetID}`); + + const resultBytes = await contract.evaluateTransaction( + 'ReadAssetPrivateDetails', + collectionName, + assetID + ); + + const resultJson = utf8Decoder.decode(resultBytes); + if (!resultJson) { + console.log('*** No result'); + return false; + } + const result = JSON.parse(resultJson); + console.log('*** Result:', result); + return true; +} + +export function doFail(msgString: string): never { + console.error(`${RED}\t${msgString}${RESET}`); + throw new Error(msgString); +} \ No newline at end of file diff --git a/asset-transfer-private-data/application-gateway-typescript/src/connect.ts b/asset-transfer-private-data/application-gateway-typescript/src/connect.ts new file mode 100644 index 00000000..fbf8e250 --- /dev/null +++ b/asset-transfer-private-data/application-gateway-typescript/src/connect.ts @@ -0,0 +1,128 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as grpc from '@grpc/grpc-js'; +import { Identity, Signer, signers } from '@hyperledger/fabric-gateway'; +import * as crypto from 'crypto'; +import { promises as fs } from 'fs'; +import * as path from 'path'; + +// Path to org1 crypto materials. +const cryptoPathOrg1 = path.resolve( + __dirname, + '..', + '..', + '..', + 'test-network', + 'organizations', + 'peerOrganizations', + 'org1.example.com' +); + +// Path to org1 user private key directory. +export const keyDirectoryPathOrg1 = path.resolve( + cryptoPathOrg1, + 'users', + 'User1@org1.example.com', + 'msp', + 'keystore' +); + +// Path to org1 user certificate. +export const certPathOrg1 = path.resolve( + cryptoPathOrg1, + 'users', + 'User1@org1.example.com', + 'msp', + 'signcerts', + 'cert.pem' +); + +// Path to org1 peer tls certificate. +export const tlsCertPathOrg1 = path.resolve( + cryptoPathOrg1, + 'peers', + 'peer0.org1.example.com', + 'tls', + 'ca.crt' +); + +// Path to org2 crypto materials. +export const cryptoPathOrg2 = path.resolve( + __dirname, + '..', + '..', + '..', + 'test-network', + 'organizations', + 'peerOrganizations', + 'org2.example.com' +); + +// Path to org2 user private key directory. +export const keyDirectoryPathOrg2 = path.resolve( + cryptoPathOrg2, + 'users', + 'User1@org2.example.com', + 'msp', + 'keystore' +); + +// Path to org2 user certificate. +export const certPathOrg2 = path.resolve( + cryptoPathOrg2, + 'users', + 'User1@org2.example.com', + 'msp', + 'signcerts', + 'cert.pem' +); + +// Path to org2 peer tls certificate. +export const tlsCertPathOrg2 = path.resolve( + cryptoPathOrg2, + 'peers', + 'peer0.org2.example.com', + 'tls', + 'ca.crt' +); + +// Gateway peer endpoint. +export const peerEndpointOrg1 = 'localhost:7051'; +export const peerEndpointOrg2 = 'localhost:9051'; + +// Gateway peer container name. +export const peerNameOrg1 = 'peer0.org1.example.com'; +export const peerNameOrg2 = 'peer0.org2.example.com'; + + +export async function newGrpcConnection( + tlsCertPath: string, + peerEndpoint: string, + peerName: string +): Promise { + const tlsRootCert = await fs.readFile(tlsCertPath); + const tlsCredentials = grpc.credentials.createSsl(tlsRootCert); + return new grpc.Client(peerEndpoint, tlsCredentials, { + 'grpc.ssl_target_name_override': peerName, + }); +} + +export async function newIdentity( + certPath: string, + mspId: string +): Promise { + const credentials = await fs.readFile(certPath); + return { mspId, credentials }; +} + +export async function newSigner(keyDirectoryPath: string): Promise { + const files = await fs.readdir(keyDirectoryPath); + const keyPath = path.resolve(keyDirectoryPath, files[0]); + const privateKeyPem = await fs.readFile(keyPath); + const privateKey = crypto.createPrivateKey(privateKeyPem); + return signers.newPrivateKeySigner(privateKey); +} \ No newline at end of file diff --git a/asset-transfer-private-data/application-gateway-typescript/tsconfig.json b/asset-transfer-private-data/application-gateway-typescript/tsconfig.json new file mode 100644 index 00000000..2052fb6e --- /dev/null +++ b/asset-transfer-private-data/application-gateway-typescript/tsconfig.json @@ -0,0 +1,18 @@ +{ + "extends":"@tsconfig/node14/tsconfig.json", + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "outDir": "dist", + "moduleResolution": "node", + "declaration": true, + "sourceMap": true, + "noImplicitAny": true + }, + "include": [ + "./src/**/*" + ], + "exclude": [ + "./src/**/*.spec.ts" + ] +} diff --git a/ci/scripts/run-test-network-private.sh b/ci/scripts/run-test-network-private.sh index af2d22aa..83283925 100755 --- a/ci/scripts/run-test-network-private.sh +++ b/ci/scripts/run-test-network-private.sh @@ -32,3 +32,16 @@ print "Executing app.js" node app.js popd stopNetwork + + +# Run typescript gateway application +createNetwork +print "Initializing typescript application" +pushd ../asset-transfer-private-data/application-gateway-typescript +npm install +print "Build typescript app" +npm run build +print "Executing app.js" +npm start +popd +stopNetwork