Private data samples migration (#574)

Signed-off-by: sapthasurendran <saptha.surendran@ibm.com>

Updated application flow

Signed-off-by: sapthasurendran <saptha.surendran@ibm.com>

Add grpc dependency in package.json

Signed-off-by: sapthasurendran <saptha.surendran@ibm.com>

Update CI pipelines to run new app
Updated application description in package.json
Fixed chaincode name
Code Refactor
Signed-off-by: sapthasurendran <saptha.surendran@ibm.com>
This commit is contained in:
sapthasurendran 2022-03-09 14:21:48 +05:30 committed by GitHub
parent 70cce456d4
commit f01eeab663
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 624 additions and 0 deletions

View file

@ -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
```

View file

@ -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"
]
}
]
}

View file

@ -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

View file

@ -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

View file

@ -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"
}
}

View file

@ -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<void> {
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<void> {
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<void> {
// 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<void> {
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<void> {
// 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<void> {
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<void> {
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<void> {
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<boolean> {
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);
}

View file

@ -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<grpc.Client> {
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<Identity> {
const credentials = await fs.readFile(certPath);
return { mspId, credentials };
}
export async function newSigner(keyDirectoryPath: string): Promise<Signer> {
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);
}

View file

@ -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"
]
}

View file

@ -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