mirror of
https://github.com/hyperledger/fabric-samples.git
synced 2026-06-20 00:25:09 +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
|
||||
print "Remove wallet storage"
|
||||
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