From 37ead7dd25b22dbd3926bb46626fbccc70ff95ee Mon Sep 17 00:00:00 2001 From: sapthasurendran Date: Fri, 17 Dec 2021 17:08:51 +0530 Subject: [PATCH] Updated ci pipelines to include the app Readme update Wait for events to complete Refactor code for events replay Signed-off-by: sapthasurendran --- asset-transfer-events/README.md | 11 +- .../package.json | 1 + .../application-gateway-typescript/src/app.ts | 375 +++++------------- .../src/connect.ts | 49 +++ ci/scripts/run-test-network-events.sh | 14 +- 5 files changed, 164 insertions(+), 286 deletions(-) create mode 100644 asset-transfer-events/application-gateway-typescript/src/connect.ts diff --git a/asset-transfer-events/README.md b/asset-transfer-events/README.md index a91568dc..9f8d4feb 100644 --- a/asset-transfer-events/README.md +++ b/asset-transfer-events/README.md @@ -43,12 +43,12 @@ Note that the asset transfer implemented by the smart contract is a simplified s 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 one of the smart contract implementations (from the `test-network` folder). - ``` + ``` # To deploy the JavaScript chaincode implementation ./network.sh deployCC -ccn events -ccp ../asset-transfer-events/chaincode-javascript/ -ccl javascript -ccep "OR('Org1MSP.peer','Org2MSP.peer')" @@ -61,6 +61,11 @@ Like other samples, the Fabric test network is used to deploy and run this sampl # To run the Go sample application cd application-gateway-go go run . + + # To run the Typescript sample application + cd application-gateway-typescript + npm install + npm start ``` ## Clean up @@ -69,4 +74,4 @@ When you are finished, you can bring down the test network (from the `test-netwo ``` ./network.sh down -``` +``` \ No newline at end of file diff --git a/asset-transfer-events/application-gateway-typescript/package.json b/asset-transfer-events/application-gateway-typescript/package.json index 8952037f..c766be23 100755 --- a/asset-transfer-events/application-gateway-typescript/package.json +++ b/asset-transfer-events/application-gateway-typescript/package.json @@ -19,6 +19,7 @@ "author": "Hyperledger", "license": "Apache-2.0", "dependencies": { + "@grpc/grpc-js": "^1.5.0", "@hyperledger/fabric-gateway": "^1.0.0" }, "devDependencies": { diff --git a/asset-transfer-events/application-gateway-typescript/src/app.ts b/asset-transfer-events/application-gateway-typescript/src/app.ts index 0f9ac55d..36128004 100755 --- a/asset-transfer-events/application-gateway-typescript/src/app.ts +++ b/asset-transfer-events/application-gateway-typescript/src/app.ts @@ -4,60 +4,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -// pre-requisites: -// - fabric-sample two organization test-network setup with two peers, ordering service, -// and 2 certificate authorities -// ===> from directory test-network -// ./network.sh up createChannel -ca -// -// - Use the asset-transfer-events/chaincode-javascript chaincode deployed on -// the channel "mychannel". The following deploy command will package, install, -// approve, and commit the javascript chaincode, all the actions it takes -// to deploy a chaincode to a channel. -// ===> from directory test-network -// ./network.sh deployCC -ccn events -ccp ../asset-transfer-events/chaincode-javascript/ -ccl javascript -ccep "OR('Org1MSP.peer','Org2MSP.peer')" -// -// - Be sure that node.js is installed -// ===> from directory asset-transfer-events/application-javascript -// node -v -// - npm installed code dependencies -// ===> from directory asset-transfer-events/application-javascript -// npm install -// - to build this test application -// ===> from directory asset-transfer-events/application-javascript -// npm prepare -// - to run this test application -// ===> from directory asset-transfer-events/application-javascript -// npm start - import * as grpc from '@grpc/grpc-js'; -import { connect, Contract, Identity, Network, Signer, signers } from '@hyperledger/fabric-gateway'; -import * as crypto from 'crypto'; -import { promises as fs } from 'fs'; -import * as path from 'path'; +import { ChaincodeEvent, CloseableAsyncIterable, connect, Contract, GatewayError, Network } from '@hyperledger/fabric-gateway'; import { TextDecoder } from 'util'; +import { newGrpcConnection, newIdentity, newSigner } from './connect'; + const channelName = 'mychannel'; const chaincodeName = 'events'; -const mspId = 'Org1MSP'; - -// Path to crypto materials. -const cryptoPath = path.resolve(__dirname, '..', '..', '..', 'test-network', 'organizations', 'peerOrganizations', 'org1.example.com'); - -// Path to user private key directory. -const keyDirectoryPath = path.resolve(cryptoPath, 'users', 'User1@org1.example.com', 'msp', 'keystore'); - -// Path to user certificate. -const certPath = path.resolve(cryptoPath, 'users', 'User1@org1.example.com', 'msp', 'signcerts', 'cert.pem'); - -// Path to peer tls certificate. -const tlsCertPath = path.resolve(cryptoPath, 'peers', 'peer0.org1.example.com', 'tls', 'ca.crt'); - -// Gateway peer endpoint. -const peerEndpoint = 'localhost:7051'; const utf8Decoder = new TextDecoder(); -let assetId = `asset${Date.now()}`; +const now = Date.now(); +const assetId = `asset${now}`; + async function main(): Promise { // The gRPC client connection should be shared by all Gateway connections to this endpoint. @@ -67,10 +26,23 @@ async function main(): Promise { client, identity: await newIdentity(), signer: await newSigner(), + evaluateOptions: () => { + return { deadline: Date.now() + 5000 }; // 5 seconds + }, + endorseOptions: () => { + return { deadline: Date.now() + 15000 }; // 15 seconds + }, + submitOptions: () => { + return { deadline: Date.now() + 5000 }; // 5 seconds + }, + commitStatusOptions: () => { + return { deadline: Date.now() + 60000 }; // 1 minute + }, }); - try { + let events: CloseableAsyncIterable | undefined; + try { // Get a network instance representing the channel where the smart contract is deployed. const network = gateway.getNetwork(channelName); @@ -78,288 +50,135 @@ async function main(): Promise { const contract = network.getContract(chaincodeName); //Start Listening to events. - startEventListening(network) + events = await startEventListening(network); // Create a new asset on the ledger. - await createAsset(contract); + const firstBlockNumber = await createAsset(contract); - // Update an existing asset asynchronously. - await transferAssetAsync(contract); + // Update an existing asset. + await updateAsset(contract) - // Get the asset details by assetID. - await readAssetByID(contract); + // Transfer an existing asset. + await transferAsset(contract); // Delete asset by assetID. await deleteAssetByID(contract); - // Update an asset which does not exist. - await updateNonExistentAsset(contract) - - console.log('************* BLOCK EVENTS with PRIVATE DATA **************'); - - //Generate new assetID - assetId = `asset${Date.now()}`; - - // Create a new asset with private data on the ledger. - await createAssetPrivate(contract); - - // Update an existing asset with private data asynchronously. - await transferAssetAsyncPrivate(contract); - - // Get the asset details along with the private data by assetID. - await readAssetByIDPrivate(contract); + // Replay all the events received. + await replayChaincodeEvents(network,firstBlockNumber) } finally { + events?.close(); gateway.close(); client.close(); } } -main().catch(error => console.error('******** FAILED to run the application:', error)); +main().catch(error => { + console.error('******** FAILED to run the application:', error); + process.exitCode = 1; +}); -async function newGrpcConnection(): Promise { - const tlsRootCert = await fs.readFile(tlsCertPath); - const tlsCredentials = grpc.credentials.createSsl(tlsRootCert); - return new grpc.Client(peerEndpoint, tlsCredentials, { - 'grpc.ssl_target_name_override': 'peer0.org1.example.com', - }); -} +/** + * Start listening to events. + */ +async function startEventListening(network: Network): Promise> { + console.log('\n*** Start chaincode event listening\n'); -async function newIdentity(): Promise { - const credentials = await fs.readFile(certPath); - return { mspId, credentials }; -} - -async function newSigner(): 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); -} - -async function startEventListening(network: Network) { - console.log('Read chaincode events'); const events = await network.getChaincodeEvents(chaincodeName); + + readEvents(events); + return events; +} + +/** + * Read events. + */ +async function readEvents(events: CloseableAsyncIterable): Promise { try { for await (const event of events) { const payload = utf8Decoder.decode(event.payload); - console.log(`Received event name: ${event.eventName}, payload: ${payload}, txID: ${event.transactionId}, blockNumber:${event.blockNumber}`); + console.log(`\n<-- Chaincode event received, name: ${event.eventName}, payload: ${payload}, txID: ${event.transactionId}, blockNumber:${event.blockNumber}`); + } + } catch (error: unknown) { + if (!(error instanceof GatewayError) || error.code !== grpc.status.CANCELLED) { + throw error; } - } finally { - // Ensure event iterator is closed when done reading. - events.close(); } } /** - * Submit a transaction synchronously, blocking until it has been committed to the ledger. + * Submit a transaction asynchronously to create a new asset. */ -async function createAsset(contract: Contract): Promise { - console.log('\n--> Submit Transaction: CreateAsset, creates new asset with ID, Color, Size, Owner and AppraisedValue arguments'); +async function createAsset(contract: Contract): Promise { + console.log(`\n --> Submit Transaction: CreateAsset, creates ${assetId} owned by Tom with appraised value 100`); - await contract.submitTransaction( - 'CreateAsset', - assetId, - 'yellow', - '5', - 'Tom', - '1300', - ); + const result = await contract.submitAsync('CreateAsset', + {arguments: [assetId,'yellow','5','Tom','100',]}); - console.log('*** Transaction committed successfully'); -} - -/** - * Submit transaction asynchronously, allowing the application to process the smart contract response (e.g. update a UI) - * while waiting for the commit notification. - */ -async function transferAssetAsync(contract: Contract): Promise { - console.log('\n--> Async Submit Transaction: TransferAsset, updates existing asset owner'); - - const commit = await contract.submitAsync('TransferAsset', { - arguments: [assetId, 'Saptha'], - }); - const oldOwner = utf8Decoder.decode(commit.getResult()); - - console.log(`*** Successfully submitted transaction to transfer ownership from ${oldOwner} to Saptha`); - console.log('*** Waiting for transaction commit'); - - const status = await commit.getStatus(); + const status = await result.getStatus(); if (!status.successful) { - throw new Error(`Transaction ${status.transactionId} failed to commit with status code ${status.code}`); + throw new Error(`failed to commit transaction ${status.transactionId} with status code ${status.code}`); } - console.log('*** Transaction committed successfully'); + console.log('\n*** CreateAsset committed successfully\n') + + return status.blockNumber; +} +/** + * Submit transaction synchronously, to updateAsset the asset appraised value. + */ +async function updateAsset(contract: Contract): Promise { + console.log(`\n--> Submit transaction: UpdateAsset, ${assetId} update appraised value to 200`); + + await contract.submitTransaction('UpdateAsset', + assetId,'yellow','5','Tom','200',); + + console.log('\n*** UpdateAsset committed successfully\n') } -// Evaluate a transaction to query ledger state by given ID. -async function readAssetByID(contract: Contract): Promise { - console.log('\n--> Evaluate Transaction: ReadAsset, function returns asset attributes'); +/** + * Submit transaction synchronously, to transfer the asset. + */ +async function transferAsset(contract: Contract): Promise { + console.log(`\n--> Submit transaction: TransferAsset, ${assetId} to Saptha\n`); - const resultBytes = await contract.evaluateTransaction('ReadAsset', assetId); + await contract.submitTransaction('TransferAsset', + assetId, 'Saptha',); - const resultJson = utf8Decoder.decode(resultBytes); - const result = JSON.parse(resultJson); - checkAsset(mspId, result,'yellow','5','Saptha','1300') - - console.log('*** Result:', result); + console.log('\n*** TransferAsset committed successfully\n') } /** * Submit a transaction synchronously to delete an asset by ID. */ async function deleteAssetByID(contract:Contract): Promise{ - console.log('\n--> Submit Transaction: DeleteAsset asset70'); + console.log(`\n--> Submit transaction: DeleteAsset ${assetId}`); - await contract.submitTransaction( - 'DeleteAsset', + await contract.submitTransaction('DeleteAsset', assetId ); - console.log('*** Transaction committed successfully'); + console.log('\n*** DeleteAsset committed successfully\n') } /** - * submitTransaction() will throw an error containing details of any error responses from the smart contract. - */ -async function updateNonExistentAsset(contract: Contract): Promise{ - console.log('\n--> Submit Transaction: UpdateAsset asset70, asset70 does not exist and should return an error'); + * Replay all the events from the start block. + */ +async function replayChaincodeEvents(network:Network,startBlock:bigint):Promise{ + const events = await network.getChaincodeEvents(chaincodeName, { + startBlock + }); try { - await contract.submitTransaction( - 'UpdateAsset', - 'asset70', - 'blue', - '5', - 'Tomoko', - '300', - ); - console.log('******** FAILED to return an error'); - } catch (error) { - console.log('*** Successfully caught the error: \n', error); - } -} - -/** - * Verify asset details. - */ -function checkAsset(mspId:string, asset:{ - ID: string, - Color: string, - Size: string, - Owner: string, - AppraisedValue: string, - asset_properties:{ - object_type: string, - asset_id: string, - Price: string, - salt: string - } -}, color:string, size:string, owner:string, appraisedValue:string, price?:string) { - console.log(`<-- Query results from ${mspId}`); - - console.log(`*** verify asset ${asset.ID}`); - - if (asset) { - if (asset.Color === color) { - console.log(`*** asset ${asset.ID} has color ${asset.Color}`); - } else { - console.log(`*** asset ${asset.ID} has color of ${asset.Color}`); - } - if (asset.Size === size) { - console.log(`*** asset ${asset.ID} has size ${asset.Size}`); - } else { - console.log(`*** Failed size check from ${mspId} - asset ${asset.ID} has size of ${asset.Size}`); - } - if (asset.Owner === owner) { - console.log(`*** asset ${asset.ID} owned by ${asset.Owner}`); - } else { - console.log(`*** Failed owner check from ${mspId} - asset ${asset.ID} owned by ${asset.Owner}`); - } - if (asset.AppraisedValue === appraisedValue) { - console.log(`*** asset ${asset.ID} has appraised value ${asset.AppraisedValue}`); - } else { - console.log(`*** Failed appraised value check from ${mspId} - asset ${asset.ID} has appraised value of ${asset.AppraisedValue}`); - } - if (price) { - if (asset.asset_properties && asset.asset_properties.Price === price) { - console.log(`*** asset ${asset.ID} has price ${asset.asset_properties.Price}`); - } else { - console.log(`*** Failed price check from ${mspId} - asset ${asset.ID} has price of ${asset.asset_properties.Price}`); + for await (const event of events) { + const payload = utf8Decoder.decode(event.payload); + console.log(`<-- Chaincode event replayed: ${event.eventName}, payload: ${payload}`); + if(event.eventName === 'DeleteAsset'){ + break } } + }finally { + events.close() } -} - -/** - * Submit transaction, blocking until the transaction has been committed on the ledger. - The 'transient' data will not get written to the ledger, and is used to send sensitive data to the trusted endorsing peers. - The gateway will only send this to peers that are included in the ownership policy of all collections accessed by the chaincode function. - */ -async function createAssetPrivate(contract: Contract): Promise { - console.log('\n--> Submit Transaction: CreateAsset, creates new asset with ID, Color, Size, Owner and AppraisedValue arguments'); - - // create the private data with salt and assign to the transaction - const asset_properties = { - object_type: 'asset_properties', - asset_id: assetId, - Price: '90', - salt: Buffer.from(Date.now().toString()).toString('hex') - }; - - await contract.submit( - 'CreateAsset', - { arguments:[assetId, 'blue', '10', 'James', '100'], - transientData: asset_properties, - endorsingOrganizations: [mspId] - } - ); - - console.log('*** Transaction committed successfully'); -} - -/** - * Submit transaction asynchronously, allowing the application to process the smart contract response (e.g. update a UI) - * while waiting for the commit notification. - */ -async function transferAssetAsyncPrivate(contract: Contract): Promise { - console.log('\n--> Async Submit Transaction: TransferAsset, updates existing asset owner'); - - // update the private data with new salt and assign to the transaction - const asset_properties = { - object_type: 'asset_properties', - asset_id: assetId, - Price: '90', - salt: Buffer.from(Date.now().toString()).toString('hex') - }; - const commit = await contract.submitAsync('TransferAsset', { - arguments: [assetId, 'David'], - transientData: asset_properties, - endorsingOrganizations: [mspId] - }); - const oldOwner = utf8Decoder.decode(commit.getResult()); - - console.log(`*** Successfully submitted transaction to transfer ownership from ${oldOwner} to Davids`); - console.log('*** Waiting for transaction commit'); - - const status = await commit.getStatus(); - if (!status.successful) { - throw new Error(`Transaction ${status.transactionId} failed to commit with status code ${status.code}`); - } - - console.log('*** Transaction committed successfully'); -} - -// Evaluate a transaction to query ledger state by given ID. -async function readAssetByIDPrivate(contract: Contract): Promise { - console.log('\n--> Evaluate Transaction: ReadAsset, function returns asset attributes'); - - const resultBytes = await contract.evaluateTransaction('ReadAsset', assetId); - const resultJson = utf8Decoder.decode(resultBytes); - - const result = JSON.parse(resultJson); - checkAsset(mspId, result,'blue','10','David','100') - - console.log('*** Result:', result); } \ No newline at end of file diff --git a/asset-transfer-events/application-gateway-typescript/src/connect.ts b/asset-transfer-events/application-gateway-typescript/src/connect.ts new file mode 100644 index 00000000..9290c7db --- /dev/null +++ b/asset-transfer-events/application-gateway-typescript/src/connect.ts @@ -0,0 +1,49 @@ +/* + * 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'; + +const mspId = 'Org1MSP'; + +// Path to crypto materials. +const cryptoPath = path.resolve(__dirname, '..', '..', '..', 'test-network', 'organizations', 'peerOrganizations', 'org1.example.com'); + +// Path to user private key directory. +const keyDirectoryPath = path.resolve(cryptoPath, 'users', 'User1@org1.example.com', 'msp', 'keystore'); + +// Path to user certificate. +const certPath = path.resolve(cryptoPath, 'users', 'User1@org1.example.com', 'msp', 'signcerts', 'cert.pem'); + +// Path to peer tls certificate. +const tlsCertPath = path.resolve(cryptoPath, 'peers', 'peer0.org1.example.com', 'tls', 'ca.crt'); + +// Gateway peer endpoint. +const peerEndpoint = 'localhost:7051'; + +export async function newGrpcConnection(): Promise { + const tlsRootCert = await fs.readFile(tlsCertPath); + const tlsCredentials = grpc.credentials.createSsl(tlsRootCert); + return new grpc.Client(peerEndpoint, tlsCredentials, { + 'grpc.ssl_target_name_override': 'peer0.org1.example.com', + }); +} + +export async function newIdentity(): Promise { + const credentials = await fs.readFile(certPath); + return { mspId, credentials }; +} + +export async function newSigner(): 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); +} diff --git a/ci/scripts/run-test-network-events.sh b/ci/scripts/run-test-network-events.sh index 01c656f1..e91c3db4 100755 --- a/ci/scripts/run-test-network-events.sh +++ b/ci/scripts/run-test-network-events.sh @@ -35,11 +35,15 @@ stopNetwork print "Remove wallet storage" rm -R ../asset-transfer-events/application-javascript/wallet -# Run Go gateway application + +# Run typescript gateway application createNetwork -print "Initializing Go gateway application" -pushd ../asset-transfer-events/application-gateway-go -print "Executing application" -go run . +print "Initializing typescript application" +pushd ../asset-transfer-events/application-gateway-typescript +npm install +print "Build app" +npm run build +print "Executing dist/app.js" +npm start popd stopNetwork