diff --git a/asset-transfer-secured-agreement/README.md b/asset-transfer-secured-agreement/README.md new file mode 100644 index 00000000..7ebae1d8 --- /dev/null +++ b/asset-transfer-secured-agreement/README.md @@ -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 +``` \ No newline at end of file diff --git a/asset-transfer-secured-agreement/application-gateway-typescript/.eslintrc.json b/asset-transfer-secured-agreement/application-gateway-typescript/.eslintrc.json new file mode 100644 index 00000000..fb2391e0 --- /dev/null +++ b/asset-transfer-secured-agreement/application-gateway-typescript/.eslintrc.json @@ -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" + } + ] + } + } + ] + } \ No newline at end of file diff --git a/asset-transfer-secured-agreement/application-gateway-typescript/.gitignore b/asset-transfer-secured-agreement/application-gateway-typescript/.gitignore new file mode 100644 index 00000000..99e5af9f --- /dev/null +++ b/asset-transfer-secured-agreement/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-secured-agreement/application-gateway-typescript/package.json b/asset-transfer-secured-agreement/application-gateway-typescript/package.json new file mode 100644 index 00000000..71fa0da7 --- /dev/null +++ b/asset-transfer-secured-agreement/application-gateway-typescript/package.json @@ -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" + } +} diff --git a/asset-transfer-secured-agreement/application-gateway-typescript/src/app.ts b/asset-transfer-secured-agreement/application-gateway-typescript/src/app.ts new file mode 100644 index 00000000..70ed7383 --- /dev/null +++ b/asset-transfer-secured-agreement/application-gateway-typescript/src/app.ts @@ -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 { + + // 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; +}); diff --git a/asset-transfer-secured-agreement/application-gateway-typescript/src/connect.ts b/asset-transfer-secured-agreement/application-gateway-typescript/src/connect.ts new file mode 100644 index 00000000..e10f8bd6 --- /dev/null +++ b/asset-transfer-secured-agreement/application-gateway-typescript/src/connect.ts @@ -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 { + 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-secured-agreement/application-gateway-typescript/src/contractWrapper.ts b/asset-transfer-secured-agreement/application-gateway-typescript/src/contractWrapper.ts new file mode 100644 index 00000000..626e99f5 --- /dev/null +++ b/asset-transfer-secured-agreement/application-gateway-typescript/src/contractWrapper.ts @@ -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 { + 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 { + 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(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 { + 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(resultString); + const result: AssetProperties = { + assetId: json.assetID, + color: json.color, + size: json.size, + }; + console.log('*** Result:', result); + } + + + public async changePublicDescription(asset: Asset): Promise { + 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 { + + 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 { + 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(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 { + + 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 { + + 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(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 { + + 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(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 { + + 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}`); + } +} \ No newline at end of file diff --git a/asset-transfer-secured-agreement/application-gateway-typescript/src/utils.ts b/asset-transfer-secured-agreement/application-gateway-typescript/src/utils.ts new file mode 100644 index 00000000..6ff968fb --- /dev/null +++ b/asset-transfer-secured-agreement/application-gateway-typescript/src/utils.ts @@ -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(data: string): T { + return JSON.parse(data); +} diff --git a/asset-transfer-secured-agreement/application-gateway-typescript/tsconfig.json b/asset-transfer-secured-agreement/application-gateway-typescript/tsconfig.json new file mode 100644 index 00000000..2052fb6e --- /dev/null +++ b/asset-transfer-secured-agreement/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-secured.sh b/ci/scripts/run-test-network-secured.sh index cce1721a..6450ee98 100755 --- a/ci/scripts/run-test-network-secured.sh +++ b/ci/scripts/run-test-network-secured.sh @@ -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