mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-22 17:45:10 +00:00
Secured agreement samples using gateway (#630)
* initial commit Signed-off-by: sapthasurendran <saptha.surendran@ibm.com> Code refactor Signed-off-by: sapthasurendran <saptha.surendran@ibm.com> * readme Signed-off-by: sapthasurendran <saptha.surendran@ibm.com> * lint fix Signed-off-by: sapthasurendran <saptha.surendran@ibm.com> * adopted best practises Signed-off-by: sapthasurendran <saptha.surendran@ibm.com> * code refactor Signed-off-by: sapthasurendran <saptha.surendran@ibm.com> * updated azure pipeline to include the app Signed-off-by: sapthasurendran <saptha.surendran@ibm.com> * mapped json and client object Signed-off-by: sapthasurendran <saptha.surendran@ibm.com> * Moved contract interactions to contractWrapper Signed-off-by: sapthasurendran <saptha.surendran@ibm.com> * salt value unexported Signed-off-by: sapthasurendran <saptha.surendran@ibm.com> * moved try catch from contract wrapper to app Signed-off-by: sapthasurendran <saptha.surendran@ibm.com> * contract wrapper refactor moved interfaces from utils to contract wrapper Signed-off-by: sapthasurendran <saptha.surendran@ibm.com> * exported data objects Signed-off-by: sapthasurendran <saptha.surendran@ibm.com>
This commit is contained in:
parent
8662b10c58
commit
4681fe7865
10 changed files with 847 additions and 0 deletions
64
asset-transfer-secured-agreement/README.md
Normal file
64
asset-transfer-secured-agreement/README.md
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Asset transfer secured agreement sample
|
||||||
|
|
||||||
|
The asset transfer events sample demonstrates how to transfer a private asset between two organizations without publicly sharing data .
|
||||||
|
|
||||||
|
## About the sample
|
||||||
|
|
||||||
|
This sample includes smart contract and application code in multiple languages. This sample shows how Fabric features state based endorsement, private data, and access control to provide secured transactions.
|
||||||
|
|
||||||
|
### Application
|
||||||
|
|
||||||
|
Refer [Secured asset transfer in Fabric](https://hyperledger-fabric.readthedocs.io/en/latest/secured_asset_transfer/secured_private_asset_transfer_tutorial.html) for application details .
|
||||||
|
|
||||||
|
### Smart Contract
|
||||||
|
|
||||||
|
The smart contract (in folder `chaincode-go`) implements the following functions to support the application:
|
||||||
|
|
||||||
|
- CreateAsset
|
||||||
|
- ChangePublicDescription
|
||||||
|
- AgreeToSell
|
||||||
|
- AgreeToBuy
|
||||||
|
- VerifyAssetProperties
|
||||||
|
- TransferAsset
|
||||||
|
- ReadAsset
|
||||||
|
- GetAssetPrivateProperties
|
||||||
|
- GetAssetSalesPrice
|
||||||
|
- GetAssetBidPrice
|
||||||
|
- QueryAssetSaleAgreements
|
||||||
|
- QueryAssetBuyAgreements
|
||||||
|
- QueryAssetHistory
|
||||||
|
|
||||||
|
## 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
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Deploy the smart contract implementations.
|
||||||
|
```
|
||||||
|
# To deploy the go chaincode implementation
|
||||||
|
./network.sh deployCC -ccn secured -ccp ../asset-transfer-secured-agreement/chaincode-go/ -ccl go -ccep "OR('Org1MSP.peer','Org2MSP.peer')"
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Run the application (from the `asset-transfer-secured-agreement` folder).
|
||||||
|
```
|
||||||
|
# To run the Typescript sample application
|
||||||
|
cd application-gateway-typescript
|
||||||
|
npm install
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# To run the Javascript sample application
|
||||||
|
cd application-javascript
|
||||||
|
node app.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,102 @@
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"project": [
|
||||||
|
"./tsconfig.json"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/comma-spacing": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"@typescript-eslint/explicit-function-return-type": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allowExpressions": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/func-call-spacing": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"@typescript-eslint/member-delimiter-style": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"@typescript-eslint/indent": [
|
||||||
|
"error",
|
||||||
|
4,
|
||||||
|
{
|
||||||
|
"SwitchCase": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/prefer-nullish-coalescing": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"@typescript-eslint/prefer-optional-chain": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"@typescript-eslint/prefer-reduce-type-parameter": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"@typescript-eslint/prefer-return-this-type": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"@typescript-eslint/quotes": [
|
||||||
|
"error",
|
||||||
|
"single"
|
||||||
|
],
|
||||||
|
"@typescript-eslint/type-annotation-spacing": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"@typescript-eslint/semi": [
|
||||||
|
"error"
|
||||||
|
],
|
||||||
|
"@typescript-eslint/space-before-function-paren": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"anonymous": "never",
|
||||||
|
"named": "never",
|
||||||
|
"asyncArrow": "always"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
14
asset-transfer-secured-agreement/application-gateway-typescript/.gitignore
vendored
Normal file
14
asset-transfer-secured-agreement/application-gateway-typescript/.gitignore
vendored
Normal 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
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"name": "asset-transfer-basic",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Asset Transfer Secured Agreement 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": {
|
||||||
|
"@hyperledger/fabric-gateway": "^1.0.0",
|
||||||
|
"@grpc/grpc-js": "^1.5.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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
/*
|
||||||
|
* Copyright IBM Corp. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { connect } from '@hyperledger/fabric-gateway';
|
||||||
|
|
||||||
|
import { newGrpcConnection, newIdentity, newSigner, tlsCertPathOrg1, peerEndpointOrg1, peerNameOrg1, certPathOrg1, mspIdOrg1, keyDirectoryPathOrg1, tlsCertPathOrg2, peerEndpointOrg2, peerNameOrg2, certPathOrg2, mspIdOrg2, keyDirectoryPathOrg2 } from './connect';
|
||||||
|
import { ContractWrapper } from './contractWrapper';
|
||||||
|
import { RED, RESET } from './utils';
|
||||||
|
|
||||||
|
const channelName = 'mychannel';
|
||||||
|
const chaincodeName = 'secured';
|
||||||
|
|
||||||
|
//Use a random key so that we can run multiple times
|
||||||
|
const now = Date.now().toString();
|
||||||
|
const assetKey = `asset${now}`;
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
|
||||||
|
// The gRPC client connection from org1 should be shared by all Gateway connections to this endpoint.
|
||||||
|
const clientOrg1 = await newGrpcConnection(
|
||||||
|
tlsCertPathOrg1,
|
||||||
|
peerEndpointOrg1,
|
||||||
|
peerNameOrg1
|
||||||
|
);
|
||||||
|
|
||||||
|
const gatewayOrg1 = connect({
|
||||||
|
client: clientOrg1,
|
||||||
|
identity: await newIdentity(certPathOrg1, mspIdOrg1),
|
||||||
|
signer: await newSigner(keyDirectoryPathOrg1),
|
||||||
|
});
|
||||||
|
|
||||||
|
// The gRPC client connection from org2 should be shared by all Gateway connections to this endpoint.
|
||||||
|
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 from the network for Org1.
|
||||||
|
const contractOrg1 = gatewayOrg1.getNetwork(channelName).getContract(chaincodeName);
|
||||||
|
const contractWrapperOrg1 = new ContractWrapper(contractOrg1, mspIdOrg1);
|
||||||
|
|
||||||
|
// Get the smart contract from the network for Org2.
|
||||||
|
const contractOrg2 = gatewayOrg2.getNetwork(channelName).getContract(chaincodeName);
|
||||||
|
const contractWrapperOrg2 = new ContractWrapper(contractOrg2, mspIdOrg2);
|
||||||
|
|
||||||
|
// Create an asset by organization Org1, this only requires the owning organization to endorse.
|
||||||
|
await contractWrapperOrg1.createAsset({ assetId: assetKey,
|
||||||
|
ownerOrg: mspIdOrg1,
|
||||||
|
publicDescription: `Asset ${assetKey} owned by ${mspIdOrg1} is not for sale`}, { ObjectType: 'asset_properties', Color: 'blue', Size: 35 });
|
||||||
|
|
||||||
|
// Read the public details by org1.
|
||||||
|
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg1);
|
||||||
|
|
||||||
|
// Read the public details by org2.
|
||||||
|
await contractWrapperOrg2.readAsset(assetKey, mspIdOrg1);
|
||||||
|
|
||||||
|
// Org1 should be able to read the private data details of the asset.
|
||||||
|
await contractWrapperOrg1.getAssetPrivateProperties(assetKey, mspIdOrg1);
|
||||||
|
|
||||||
|
// Org2 is not the owner and does not have the private details, read expected to fail.
|
||||||
|
try {
|
||||||
|
await contractWrapperOrg2.getAssetPrivateProperties(assetKey, mspIdOrg1);
|
||||||
|
} catch(e) {
|
||||||
|
console.log(`${RED}*** Failed: getAssetPrivateProperties - ${e}${RESET}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Org1 updates the assets public description.
|
||||||
|
await contractWrapperOrg1.changePublicDescription({assetId: assetKey,
|
||||||
|
ownerOrg: mspIdOrg1,
|
||||||
|
publicDescription: `Asset ${assetKey} owned by ${mspIdOrg1} is for sale`});
|
||||||
|
|
||||||
|
// Read the public details by org1.
|
||||||
|
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg1);
|
||||||
|
|
||||||
|
// Read the public details by org2.
|
||||||
|
await contractWrapperOrg2.readAsset(assetKey, mspIdOrg1);
|
||||||
|
|
||||||
|
// This is an update to the public state and requires the owner(Org1) to endorse and sent by the owner org client (Org1).
|
||||||
|
// Since the client is from Org2, which is not the owner, this will fail.
|
||||||
|
try{
|
||||||
|
await contractWrapperOrg2.changePublicDescription({assetId: assetKey,
|
||||||
|
ownerOrg: mspIdOrg1,
|
||||||
|
publicDescription: `Asset ${assetKey} owned by ${mspIdOrg2} is NOT for sale`});
|
||||||
|
} catch(e) {
|
||||||
|
console.log(`${RED}*** Failed: changePublicDescription - ${e}${RESET}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the public details by org1.
|
||||||
|
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg1);
|
||||||
|
|
||||||
|
// Read the public details by org2.
|
||||||
|
await contractWrapperOrg2.readAsset(assetKey, mspIdOrg1);
|
||||||
|
|
||||||
|
// Agree to a sell by org1.
|
||||||
|
await contractWrapperOrg1.agreeToSell({
|
||||||
|
assetId: assetKey,
|
||||||
|
price: 110,
|
||||||
|
tradeId: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check the private information about the asset from Org2. Org1 would have to send Org2 asset details,
|
||||||
|
// so the hash of the details may be checked by the chaincode.
|
||||||
|
await contractWrapperOrg2.verifyAssetProperties({ assetId:assetKey, color:'blue', size:35});
|
||||||
|
|
||||||
|
// Agree to a buy by org2.
|
||||||
|
await contractWrapperOrg2.agreeToBuy( {assetId: assetKey,
|
||||||
|
price: 100,
|
||||||
|
tradeId: now});
|
||||||
|
|
||||||
|
// Org1 should be able to read the sale price of this asset.
|
||||||
|
await contractWrapperOrg1.getAssetSalesPrice(assetKey, mspIdOrg1);
|
||||||
|
|
||||||
|
// Org2 has not set a sale price and this should fail.
|
||||||
|
try{
|
||||||
|
await contractWrapperOrg2.getAssetSalesPrice(assetKey, mspIdOrg1);
|
||||||
|
} catch(e) {
|
||||||
|
console.log(`${RED}*** Failed: getAssetSalesPrice - ${e}${RESET}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Org1 has not agreed to buy so this should fail.
|
||||||
|
try{
|
||||||
|
await contractWrapperOrg1.getAssetBidPrice(assetKey, mspIdOrg2);
|
||||||
|
} catch(e) {
|
||||||
|
console.log(`${RED}*** Failed: getAssetBidPrice - ${e}${RESET}`);
|
||||||
|
}
|
||||||
|
// Org2 should be able to see the price it has agreed.
|
||||||
|
await contractWrapperOrg2.getAssetBidPrice(assetKey, mspIdOrg2);
|
||||||
|
|
||||||
|
// Org1 will try to transfer the asset to Org2
|
||||||
|
// This will fail due to the sell price and the bid price are not the same.
|
||||||
|
try{
|
||||||
|
await contractWrapperOrg1.transferAsset({ObjectType: 'asset_properties', Color: 'blue', Size: 35}, { assetId: assetKey, price: 110, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2);
|
||||||
|
} catch(e) {
|
||||||
|
console.log(`${RED}*** Failed: transferAsset - ${e}${RESET}`);
|
||||||
|
}
|
||||||
|
// Agree to a sell by Org1, the seller will agree to the bid price of Org2.
|
||||||
|
await contractWrapperOrg1.agreeToSell({assetId:assetKey, price:100, tradeId:now});
|
||||||
|
|
||||||
|
// Read the public details by org1.
|
||||||
|
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg1);
|
||||||
|
|
||||||
|
// Read the public details by org2.
|
||||||
|
await contractWrapperOrg2.readAsset(assetKey, mspIdOrg1);
|
||||||
|
|
||||||
|
// Org1 should be able to read the private data details of the asset.
|
||||||
|
await contractWrapperOrg1.getAssetPrivateProperties(assetKey, mspIdOrg1);
|
||||||
|
|
||||||
|
// Org1 should be able to read the sale price of this asset.
|
||||||
|
await contractWrapperOrg1.getAssetSalesPrice(assetKey, mspIdOrg1);
|
||||||
|
|
||||||
|
// Org2 should be able to see the price it has agreed.
|
||||||
|
await contractWrapperOrg2.getAssetBidPrice(assetKey, mspIdOrg2);
|
||||||
|
|
||||||
|
// Org2 user will try to transfer the asset to Org1.
|
||||||
|
// This will fail as the owner is Org1.
|
||||||
|
try{
|
||||||
|
await contractWrapperOrg2.transferAsset({ObjectType: 'asset_properties', Color: 'blue', Size: 35}, { assetId: assetKey, price: 100, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2);
|
||||||
|
} catch(e) {
|
||||||
|
console.log(`${RED}*** Failed: transferAsset - ${e}${RESET}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Org1 will transfer the asset to Org2.
|
||||||
|
// This will now complete as the sell price and the bid price are the same.
|
||||||
|
await contractWrapperOrg1.transferAsset({ObjectType: 'asset_properties', Color: 'blue', Size: 35}, { assetId: assetKey, price: 100, tradeId: now}, [ mspIdOrg1, mspIdOrg2 ], mspIdOrg1, mspIdOrg2);
|
||||||
|
|
||||||
|
// Read the public details by org1.
|
||||||
|
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg2);
|
||||||
|
|
||||||
|
// Read the public details by org2.
|
||||||
|
await contractWrapperOrg2.readAsset(assetKey, mspIdOrg2);
|
||||||
|
|
||||||
|
// Org2 should be able to read the private data details of this asset.
|
||||||
|
await contractWrapperOrg2.getAssetPrivateProperties(assetKey, mspIdOrg2);
|
||||||
|
|
||||||
|
// Org1 should not be able to read the private data details of this asset, expected to fail.
|
||||||
|
try{
|
||||||
|
await contractWrapperOrg1.getAssetPrivateProperties(assetKey, mspIdOrg2);
|
||||||
|
} catch(e) {
|
||||||
|
console.log(`${RED}*** Failed: getAssetPrivateProperties - ${e}${RESET}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is an update to the public state and requires only the owner to endorse.
|
||||||
|
// Org2 wants to indicate that the items is no longer for sale.
|
||||||
|
await contractWrapperOrg2.changePublicDescription( {assetId: assetKey, ownerOrg: mspIdOrg2, publicDescription: `Asset ${assetKey} owned by ${mspIdOrg2} is NOT for sale`});
|
||||||
|
|
||||||
|
// Read the public details by org1.
|
||||||
|
await contractWrapperOrg1.readAsset(assetKey, mspIdOrg2);
|
||||||
|
|
||||||
|
// Read the public details by org2.
|
||||||
|
await contractWrapperOrg2.readAsset(assetKey, mspIdOrg2);
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
gatewayOrg1.close();
|
||||||
|
gatewayOrg2.close();
|
||||||
|
clientOrg1.close();
|
||||||
|
clientOrg2.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(error => {
|
||||||
|
console.error('******** FAILED to run the application:', error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
// MSP Id's of Organizations
|
||||||
|
export const mspIdOrg1 = 'Org1MSP';
|
||||||
|
export const mspIdOrg2 = 'Org2MSP';
|
||||||
|
|
||||||
|
// Path to org1 crypto materials.
|
||||||
|
export const cryptoPathOrg1 = path.resolve(__dirname, '..', '..', '..', 'test-network', 'organizations', 'peerOrganizations', 'org1.example.com');
|
||||||
|
|
||||||
|
// Path to user private key directory.
|
||||||
|
export const keyDirectoryPathOrg1 = path.resolve(cryptoPathOrg1, 'users', 'User1@org1.example.com', 'msp', 'keystore');
|
||||||
|
|
||||||
|
// Path to user certificate.
|
||||||
|
export const certPathOrg1 = path.resolve(cryptoPathOrg1, 'users', 'User1@org1.example.com', 'msp', 'signcerts', 'cert.pem');
|
||||||
|
|
||||||
|
// Path to 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';
|
||||||
|
|
||||||
|
//Collection Names
|
||||||
|
export const org1PrivateCollectionName = 'Org1MSPPrivateCollection';
|
||||||
|
export const org2PrivateCollectionName = 'Org2MSPPrivateCollection';
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,273 @@
|
||||||
|
/*
|
||||||
|
* Copyright IBM Corp. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
import { Contract } from '@hyperledger/fabric-gateway';
|
||||||
|
import { TextDecoder } from 'util';
|
||||||
|
import { GREEN, parse, RED, RESET } from './utils';
|
||||||
|
import crpto from 'crypto';
|
||||||
|
|
||||||
|
const randomBytes = crpto.randomBytes(256).toString('hex');
|
||||||
|
|
||||||
|
interface AssetJSON {
|
||||||
|
objectType: string;
|
||||||
|
assetID: string;
|
||||||
|
ownerOrg: string;
|
||||||
|
publicDescription: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AssetPropertiesJSON {
|
||||||
|
objectType: string;
|
||||||
|
assetID: string;
|
||||||
|
color: string;
|
||||||
|
size: number;
|
||||||
|
salt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AssetPriceJSON {
|
||||||
|
assetID: string;
|
||||||
|
price: number;
|
||||||
|
tradeID: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssetPrivateData {
|
||||||
|
ObjectType: string;
|
||||||
|
Color: string;
|
||||||
|
Size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Asset {
|
||||||
|
assetId: string;
|
||||||
|
ownerOrg: string;
|
||||||
|
publicDescription: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssetProperties {
|
||||||
|
assetId: string;
|
||||||
|
color: string;
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssetPrice {
|
||||||
|
assetId: string;
|
||||||
|
price: number;
|
||||||
|
tradeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ContractWrapper {
|
||||||
|
|
||||||
|
readonly #contract: Contract;
|
||||||
|
readonly #org: string;
|
||||||
|
readonly #utf8Decoder = new TextDecoder();
|
||||||
|
readonly #randomBytes: string = randomBytes;
|
||||||
|
|
||||||
|
public constructor(contract: Contract, org: string) {
|
||||||
|
this.#contract = contract;
|
||||||
|
this.#org = org;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createAsset(asset: Asset, privateData: AssetPrivateData): Promise<void> {
|
||||||
|
console.log(`${GREEN}--> Submit Transaction: CreateAsset, ${asset.assetId} as ${asset.ownerOrg} - endorsed by Org1.${RESET}`);
|
||||||
|
const assetPropertiesJSON: AssetPropertiesJSON = {
|
||||||
|
objectType: 'asset_properties',
|
||||||
|
assetID: asset.assetId,
|
||||||
|
color: privateData.Color,
|
||||||
|
size: privateData.Size,
|
||||||
|
salt: this.#randomBytes };
|
||||||
|
|
||||||
|
await this.#contract.submit('CreateAsset', {
|
||||||
|
arguments: [asset.assetId, asset.publicDescription],
|
||||||
|
transientData: { asset_properties: JSON.stringify(assetPropertiesJSON)},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`*** Result: committed, asset ${asset.assetId} is owned by Org1`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async readAsset(assetKey: string, ownerOrg: string): Promise<void> {
|
||||||
|
console.log(`${GREEN}--> Evaluate Transactions: ReadAsset as ${this.#org}, - ${assetKey} should be owned by ${ownerOrg}.${RESET}`);
|
||||||
|
|
||||||
|
const resultBytes = await this.#contract.evaluateTransaction('ReadAsset', assetKey);
|
||||||
|
|
||||||
|
const result = this.#utf8Decoder.decode(resultBytes);
|
||||||
|
if (result.length !== 0) {
|
||||||
|
const json = parse<AssetJSON>(result);
|
||||||
|
if (json.ownerOrg === ownerOrg) {
|
||||||
|
console.log(`*** Result from ${this.#org} - asset ${json.assetID} owned by ${json.ownerOrg} DESC: ${json.publicDescription}`);
|
||||||
|
} else {
|
||||||
|
console.log(`${RED}*** Failed owner check from ${this.#org} - asset ${json.assetID} owned by ${json.ownerOrg} DESC:${json.publicDescription}.${RESET}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('No Asset Found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAssetPrivateProperties(assetKey: string, ownerOrg: string): Promise<void> {
|
||||||
|
console.log(`${GREEN}--> Evaluate Transaction: GetAssetPrivateProperties, - ${assetKey} from organization ${this.#org}.${RESET}`);
|
||||||
|
if(this.#org !== ownerOrg) {
|
||||||
|
console.log(`${GREEN}* Expected to fail as ${this.#org} is not the owner and does not have the private details.${RESET}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultBytes = await this.#contract.evaluateTransaction('GetAssetPrivateProperties', assetKey);
|
||||||
|
|
||||||
|
const resultString = this.#utf8Decoder.decode(resultBytes);
|
||||||
|
const json = parse<AssetPropertiesJSON>(resultString);
|
||||||
|
const result: AssetProperties = {
|
||||||
|
assetId: json.assetID,
|
||||||
|
color: json.color,
|
||||||
|
size: json.size,
|
||||||
|
};
|
||||||
|
console.log('*** Result:', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async changePublicDescription(asset: Asset): Promise<void> {
|
||||||
|
console.log(`${GREEN}--> Submit Transaction: ChangePublicDescription ${asset.assetId}, as ${this.#org} - endorse by ${this.#org}.${RESET}`);
|
||||||
|
if (asset.ownerOrg !== this.#org) {
|
||||||
|
console.log(`${GREEN}* Expected to fail as ${this.#org} is not the owner.${RESET}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.#contract.submit('ChangePublicDescription', {
|
||||||
|
arguments:[asset.assetId, asset.publicDescription],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`*** Result: committed, Desc: ${asset.publicDescription}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async agreeToSell(assetPrice: AssetPrice): Promise<void> {
|
||||||
|
|
||||||
|
console.log(`${GREEN}--> Submit Transaction: AgreeToSell, ${assetPrice.assetId} as ${this.#org} - endorsed by ${this.#org}.${RESET}`);
|
||||||
|
const assetPriceJSON: AssetPriceJSON = {
|
||||||
|
assetID:assetPrice.assetId,
|
||||||
|
price:assetPrice.price,
|
||||||
|
tradeID:assetPrice.tradeId
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.#contract.submit('AgreeToSell', {
|
||||||
|
arguments:[assetPrice.assetId],
|
||||||
|
transientData: {asset_price: JSON.stringify(assetPriceJSON)}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`*** Result: committed, ${this.#org} has agreed to sell asset ${assetPrice.assetId} for ${assetPrice.price}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async verifyAssetProperties(assetProperties: AssetProperties): Promise<void> {
|
||||||
|
console.log(`${GREEN}--> Evalute: VerifyAssetProperties, ${assetProperties.assetId} as ${this.#org} - endorsed by ${this.#org}.${RESET}`);
|
||||||
|
const assetPropertiesJSON: AssetPropertiesJSON = {objectType: 'asset_properties',
|
||||||
|
assetID: assetProperties.assetId,
|
||||||
|
color: assetProperties.color,
|
||||||
|
size: assetProperties.size,
|
||||||
|
salt: this.#randomBytes };
|
||||||
|
|
||||||
|
const resultBytes = await this.#contract.evaluate('VerifyAssetProperties', {
|
||||||
|
arguments:[assetPropertiesJSON.assetID],
|
||||||
|
transientData: {asset_properties: JSON.stringify(assetPropertiesJSON)},
|
||||||
|
});
|
||||||
|
|
||||||
|
const resultString = this.#utf8Decoder.decode(resultBytes);
|
||||||
|
if (resultString.length !== 0) {
|
||||||
|
const json = parse<AssetPropertiesJSON>(resultString);
|
||||||
|
const result: AssetProperties = {
|
||||||
|
assetId: json.assetID,
|
||||||
|
color: json.color,
|
||||||
|
size: json.size
|
||||||
|
};
|
||||||
|
if (result) {
|
||||||
|
console.log(`*** Success VerifyAssetProperties, private information about asset ${assetProperties.assetId} has been verified by ${this.#org}`);
|
||||||
|
} else {
|
||||||
|
console.log(`*** Failed: VerifyAssetProperties, private information about asset ${assetProperties.assetId} has not been verified by ${this.#org}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
throw new Error(`Private information about asset ${assetProperties.assetId} has not been verified by ${this.#org}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async agreeToBuy(assetPrice: AssetPrice, ): Promise<void> {
|
||||||
|
|
||||||
|
console.log(`${GREEN}--> Submit Transaction: AgreeToBuy, ${assetPrice.assetId} as ${this.#org} - endorsed by ${this.#org}.${RESET}`);
|
||||||
|
const assetPriceJSON: AssetPriceJSON = {
|
||||||
|
assetID: assetPrice.assetId,
|
||||||
|
price: assetPrice.price,
|
||||||
|
tradeID: assetPrice.tradeId
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.#contract.submit('AgreeToBuy', {
|
||||||
|
arguments:[assetPrice.assetId],
|
||||||
|
transientData: {asset_price: JSON.stringify(assetPriceJSON)}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`*** Result: committed, ${this.#org} has agreed to buy asset ${assetPrice.assetId} for 100`);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAssetSalesPrice(assetKey: string, ownerOrg: string): Promise<void> {
|
||||||
|
|
||||||
|
console.log(`${GREEN}--> Evaluate Transaction: GetAssetSalesPrice, - ${assetKey} from organization ${this.#org}.${RESET}`);
|
||||||
|
if(this.#org !== ownerOrg) {
|
||||||
|
console.log(`${GREEN}* Expected to fail as ${this.#org} has not set a sale price.${RESET}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultBytes = await this.#contract.evaluateTransaction('GetAssetSalesPrice', assetKey);
|
||||||
|
|
||||||
|
const resultString = this.#utf8Decoder.decode(resultBytes);
|
||||||
|
const json = parse<AssetPriceJSON>(resultString);
|
||||||
|
|
||||||
|
const result: AssetPrice = {
|
||||||
|
assetId: json.assetID,
|
||||||
|
price: json.price,
|
||||||
|
tradeId: json.tradeID
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('*** Result: GetAssetSalesPrice', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAssetBidPrice(assetKey: string, buyerOrgID: string): Promise<void> {
|
||||||
|
|
||||||
|
console.log(`${GREEN}--> Evaluate Transaction: GetAssetBidPrice, - ${assetKey} from organization ${this.#org}.${RESET}`);
|
||||||
|
if(this.#org !== buyerOrgID){
|
||||||
|
console.log(`${GREEN}* Expected to fail as ${this.#org} has not agreed to buy.${RESET}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultBytes = await this.#contract.evaluateTransaction('GetAssetBidPrice', assetKey);
|
||||||
|
|
||||||
|
const resultString = this.#utf8Decoder.decode(resultBytes);
|
||||||
|
const json = parse<AssetPriceJSON>(resultString);
|
||||||
|
const result: AssetPrice = {
|
||||||
|
assetId: json.assetID,
|
||||||
|
price: json.price,
|
||||||
|
tradeId: json.tradeID,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('*** Result: GetAssetBidPrice', result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async transferAsset( privateData: AssetPrivateData, assetPrice: AssetPrice, endorsingOrganizations: string[], ownerOrgID: string, buyerOrgID: string): Promise<void> {
|
||||||
|
|
||||||
|
console.log(`${GREEN}--> Submit Transaction: TransferAsset, ${assetPrice.assetId} as ${this.#org } - endorsed by ${this.#org}.${RESET}`);
|
||||||
|
|
||||||
|
if (this.#org !== ownerOrgID) {
|
||||||
|
console.log(`${GREEN}* Expected to fail as the owner is ${ownerOrgID}.${RESET}`);
|
||||||
|
} else if (assetPrice.price === 110) {
|
||||||
|
console.log(`${GREEN}* Expected to fail as sell price and the bid price are not the same.${RESET}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const assetPropertiesJSON: AssetPropertiesJSON = {objectType: 'asset_properties',
|
||||||
|
assetID: assetPrice.assetId,
|
||||||
|
color: privateData.Color,
|
||||||
|
size: privateData.Size,
|
||||||
|
salt: this.#randomBytes };
|
||||||
|
|
||||||
|
const assetPriceJSON: AssetPriceJSON = { assetID: assetPrice.assetId, price:assetPrice.price, tradeID:assetPrice.tradeId};
|
||||||
|
|
||||||
|
await this.#contract.submit('TransferAsset', {
|
||||||
|
arguments:[assetPropertiesJSON.assetID, buyerOrgID],
|
||||||
|
transientData: {
|
||||||
|
asset_properties: JSON.stringify(assetPropertiesJSON),
|
||||||
|
asset_price: JSON.stringify(assetPriceJSON)},
|
||||||
|
endorsingOrganizations:endorsingOrganizations
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`${GREEN}*** Result: committed, ${this.#org} has transfered the asset ${assetPrice.assetId} to ${buyerOrgID}.${RESET}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* Copyright IBM Corp. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const RED = '\x1b[31m\n';
|
||||||
|
export const GREEN = '\x1b[32m\n';
|
||||||
|
export const RESET = '\x1b[0m';
|
||||||
|
|
||||||
|
export function parse<T>(data: string): T {
|
||||||
|
return JSON.parse(data);
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -34,3 +34,15 @@ popd
|
||||||
stopNetwork
|
stopNetwork
|
||||||
print "Remove wallet storage"
|
print "Remove wallet storage"
|
||||||
rm -R ../asset-transfer-secured-agreement/application-javascript/wallet
|
rm -R ../asset-transfer-secured-agreement/application-javascript/wallet
|
||||||
|
|
||||||
|
# Run Typescript Gateway application
|
||||||
|
createNetwork
|
||||||
|
print "Initializing typescript application"
|
||||||
|
pushd ../asset-transfer-secured-agreement/application-gateway-typescript
|
||||||
|
npm install
|
||||||
|
print "Build app"
|
||||||
|
npm run build
|
||||||
|
print "Executing dist/app.js"
|
||||||
|
npm start
|
||||||
|
popd
|
||||||
|
stopNetwork
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue