diff --git a/.editorconfig b/.editorconfig index 5ec8533458..d455cb8be5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,9 +4,10 @@ root = true charset = utf-8 indent_style = space insert_final_newline = true +max_line_length = 120 trim_trailing_whitespace = true -[*.ts] +[*.{mj,cj,j,t}s] indent_size = 4 quote_type = single diff --git a/asset-transfer-basic/README.md b/asset-transfer-basic/README.md index 94bb2668f8..96892cd454 100644 --- a/asset-transfer-basic/README.md +++ b/asset-transfer-basic/README.md @@ -37,42 +37,71 @@ Note that the asset transfer implemented by the smart contract is a simplified s 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 TypeScript chaincode implementation - ./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-typescript/ -ccl typescript - # To deploy the Go chaincode implementation - ./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-go/ -ccl go + - To deploy the **TypeScript** chaincode implementation: - # To deploy the Java chaincode implementation - ./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-java/ -ccl java - ``` + ```shell + ./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-typescript/ -ccl typescript + ``` + + - To deploy the **JavaScript** chaincode implementation: + + ```shell + ./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-javascript/ -ccl javascript + ``` + + - To deploy the **Go** chaincode implementation: + + ```shell + ./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-go/ -ccl go + ``` + + - To deploy the **Java** chaincode implementation: + ```shell + ./network.sh deployCC -ccn basic -ccp ../asset-transfer-basic/chaincode-java/ -ccl java + ``` 1. Run the application (from the `asset-transfer-basic` folder). - ``` - # To run the Typescript sample application - cd application-gateway-typescript - npm install - npm start - - # To run the Go sample application - cd application-gateway-go - go run . - - # To run the Java sample application - cd application-gateway-java - ./gradlew run - ``` + + - To run the **TypeScript** sample application: + + ```shell + cd application-gateway-typescript + npm install + npm start + ``` + + - To run the **JavaScript** sample application: + + ```shell + cd application-gateway-javascript + npm install + npm start + ``` + + - To run the **Go** sample application: + + ```shell + cd application-gateway-go + go run . + ``` + + - To run the **Java** sample application: + ```shell + cd application-gateway-java + ./gradlew run + ``` ## 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. -``` +```shell ./network.sh down -``` \ No newline at end of file +``` diff --git a/asset-transfer-basic/application-gateway-javascript/.gitignore b/asset-transfer-basic/application-gateway-javascript/.gitignore new file mode 100644 index 0000000000..24a6417161 --- /dev/null +++ b/asset-transfer-basic/application-gateway-javascript/.gitignore @@ -0,0 +1,11 @@ +# +# SPDX-License-Identifier: Apache-2.0 +# + + +# Coverage directory used by tools like istanbul +coverage + +# Dependency directories +node_modules/ +jspm_packages/ diff --git a/asset-transfer-basic/application-gateway-javascript/.npmrc b/asset-transfer-basic/application-gateway-javascript/.npmrc new file mode 100644 index 0000000000..b6f27f1359 --- /dev/null +++ b/asset-transfer-basic/application-gateway-javascript/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/asset-transfer-basic/application-gateway-javascript/eslint.config.mjs b/asset-transfer-basic/application-gateway-javascript/eslint.config.mjs new file mode 100644 index 0000000000..861b4f0d3e --- /dev/null +++ b/asset-transfer-basic/application-gateway-javascript/eslint.config.mjs @@ -0,0 +1,15 @@ +import js from '@eslint/js'; +import globals from 'globals'; + +export default [ + js.configs.recommended, + { + languageOptions: { + ecmaVersion: 2023, + sourceType: 'commonjs', + globals: { + ...globals.node, + }, + }, + }, +]; diff --git a/asset-transfer-basic/application-gateway-javascript/package.json b/asset-transfer-basic/application-gateway-javascript/package.json new file mode 100644 index 0000000000..54b341bd2d --- /dev/null +++ b/asset-transfer-basic/application-gateway-javascript/package.json @@ -0,0 +1,25 @@ +{ + "name": "asset-transfer-basic", + "version": "1.0.0", + "description": "Asset Transfer Basic Application implemented in JavaScript using fabric-gateway", + "engines": { + "node": ">=18" + }, + "scripts": { + "lint": "eslint src", + "pretest": "npm run lint", + "start": "node src/app.js" + }, + "engineStrict": true, + "author": "Hyperledger", + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.10", + "@hyperledger/fabric-gateway": "^1.5" + }, + "devDependencies": { + "@eslint/js": "^9.5.0", + "eslint": "^9.5.0", + "globals": "^15.6.0" + } +} diff --git a/asset-transfer-basic/application-gateway-javascript/src/app.js b/asset-transfer-basic/application-gateway-javascript/src/app.js new file mode 100644 index 0000000000..28270beb4d --- /dev/null +++ b/asset-transfer-basic/application-gateway-javascript/src/app.js @@ -0,0 +1,246 @@ +/* + * Copyright IBM Corp. All Rights Reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +const grpc = require('@grpc/grpc-js'); +const { connect, signers } = require('@hyperledger/fabric-gateway'); +const crypto = require('node:crypto'); +const fs = require('node:fs/promises'); +const path = require('node:path'); +const { TextDecoder } = require('node:util'); + +const channelName = envOrDefault('CHANNEL_NAME', 'mychannel'); +const chaincodeName = envOrDefault('CHAINCODE_NAME', 'basic'); +const mspId = envOrDefault('MSP_ID', 'Org1MSP'); + +// Path to crypto materials. +const cryptoPath = envOrDefault( + 'CRYPTO_PATH', + path.resolve(__dirname, '..', '..', '..', 'test-network', 'organizations', 'peerOrganizations', 'org1.example.com') +); + +// Path to user private key directory. +const keyDirectoryPath = envOrDefault( + 'KEY_DIRECTORY_PATH', + path.resolve(cryptoPath, 'users', 'User1@org1.example.com', 'msp', 'keystore') +); + +// Path to user certificate directory. +const certDirectoryPath = envOrDefault( + 'CERT_DIRECTORY_PATH', + path.resolve(cryptoPath, 'users', 'User1@org1.example.com', 'msp', 'signcerts') +); + +// Path to peer tls certificate. +const tlsCertPath = envOrDefault( + 'TLS_CERT_PATH', + path.resolve(cryptoPath, 'peers', 'peer0.org1.example.com', 'tls', 'ca.crt') +); + +// Gateway peer endpoint. +const peerEndpoint = envOrDefault('PEER_ENDPOINT', 'localhost:7051'); + +// Gateway peer SSL host name override. +const peerHostAlias = envOrDefault('PEER_HOST_ALIAS', 'peer0.org1.example.com'); + +const utf8Decoder = new TextDecoder(); +const assetId = `asset${String(Date.now())}`; + +async function main() { + displayInputParameters(); + + // The gRPC client connection should be shared by all Gateway connections to this endpoint. + const client = await newGrpcConnection(); + + const gateway = connect({ + client, + identity: await newIdentity(), + signer: await newSigner(), + // Default timeouts for different gRPC calls + 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 { + // Get a network instance representing the channel where the smart contract is deployed. + const network = gateway.getNetwork(channelName); + + // Get the smart contract from the network. + const contract = network.getContract(chaincodeName); + + // Initialize a set of asset data on the ledger using the chaincode 'InitLedger' function. + await initLedger(contract); + + // Return all the current assets on the ledger. + await getAllAssets(contract); + + // Create a new asset on the ledger. + await createAsset(contract); + + // Update an existing asset asynchronously. + await transferAssetAsync(contract); + + // Get the asset details by assetID. + await readAssetByID(contract); + + // Update an asset which does not exist. + await updateNonExistentAsset(contract); + } finally { + gateway.close(); + client.close(); + } +} + +main().catch((error) => { + console.error('******** FAILED to run the application:', error); + process.exitCode = 1; +}); + +async function newGrpcConnection() { + const tlsRootCert = await fs.readFile(tlsCertPath); + const tlsCredentials = grpc.credentials.createSsl(tlsRootCert); + return new grpc.Client(peerEndpoint, tlsCredentials, { + 'grpc.ssl_target_name_override': peerHostAlias, + }); +} + +async function newIdentity() { + const certPath = await getFirstDirFileName(certDirectoryPath); + const credentials = await fs.readFile(certPath); + return { mspId, credentials }; +} + +async function getFirstDirFileName(dirPath) { + const files = await fs.readdir(dirPath); + const file = files[0]; + if (!file) { + throw new Error(`No files in directory: ${dirPath}`); + } + return path.join(dirPath, file); +} + +async function newSigner() { + const keyPath = await getFirstDirFileName(keyDirectoryPath); + const privateKeyPem = await fs.readFile(keyPath); + const privateKey = crypto.createPrivateKey(privateKeyPem); + return signers.newPrivateKeySigner(privateKey); +} + +/** + * This type of transaction would typically only be run once by an application the first time it was started after its + * initial deployment. A new version of the chaincode deployed later would likely not need to run an "init" function. + */ +async function initLedger(contract) { + console.log('\n--> Submit Transaction: InitLedger, function creates the initial set of assets on the ledger'); + + await contract.submitTransaction('InitLedger'); + + console.log('*** Transaction committed successfully'); +} + +/** + * Evaluate a transaction to query ledger state. + */ +async function getAllAssets(contract) { + console.log('\n--> Evaluate Transaction: GetAllAssets, function returns all the current assets on the ledger'); + + const resultBytes = await contract.evaluateTransaction('GetAllAssets'); + + const resultJson = utf8Decoder.decode(resultBytes); + const result = JSON.parse(resultJson); + console.log('*** Result:', result); +} + +/** + * Submit a transaction synchronously, blocking until it has been committed to the ledger. + */ +async function createAsset(contract) { + console.log( + '\n--> Submit Transaction: CreateAsset, creates new asset with ID, Color, Size, Owner and AppraisedValue arguments' + ); + + await contract.submitTransaction('CreateAsset', assetId, 'yellow', '5', 'Tom', '1300'); + + 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) { + 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(); + if (!status.successful) { + throw new Error(`Transaction ${status.transactionId} failed to commit with status code ${String(status.code)}`); + } + + console.log('*** Transaction committed successfully'); +} + +async function readAssetByID(contract) { + 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); + console.log('*** Result:', result); +} + +/** + * submitTransaction() will throw an error containing details of any error responses from the smart contract. + */ +async function updateNonExistentAsset(contract) { + console.log('\n--> Submit Transaction: UpdateAsset asset70, asset70 does not exist and should return an error'); + + 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); + } +} + +/** + * envOrDefault() will return the value of an environment variable, or a default value if the variable is undefined. + */ +function envOrDefault(key, defaultValue) { + return process.env[key] || defaultValue; +} + +/** + * displayInputParameters() will print the global scope parameters used by the main driver routine. + */ +function displayInputParameters() { + console.log(`channelName: ${channelName}`); + console.log(`chaincodeName: ${chaincodeName}`); + console.log(`mspId: ${mspId}`); + console.log(`cryptoPath: ${cryptoPath}`); + console.log(`keyDirectoryPath: ${keyDirectoryPath}`); + console.log(`certDirectoryPath: ${certDirectoryPath}`); + console.log(`tlsCertPath: ${tlsCertPath}`); + console.log(`peerEndpoint: ${peerEndpoint}`); + console.log(`peerHostAlias: ${peerHostAlias}`); +} diff --git a/ci/scripts/run-test-network-basic.sh b/ci/scripts/run-test-network-basic.sh index 023acaddd9..026d26c803 100755 --- a/ci/scripts/run-test-network-basic.sh +++ b/ci/scripts/run-test-network-basic.sh @@ -38,8 +38,8 @@ set -x createNetwork -# Run Go gateway application -print "Initializing Go gateway application" +# Run Go application +print "Initializing Go application" export CHAINCODE_NAME=go_gateway deployChaincode pushd ../asset-transfer-basic/application-gateway-go @@ -48,8 +48,8 @@ go run . popd -# Run gateway typescript application -print "Initializing Typescript gateway application" +# Run TypeScript application +print "Initializing TypeScript application" export CHAINCODE_NAME=typescript_gateway deployChaincode pushd ../asset-transfer-basic/application-gateway-typescript @@ -59,7 +59,18 @@ npm start popd -# Run Java application using gateway +# Run JavaScript application +print "Initializing JavaScript application" +export CHAINCODE_NAME=javascript_gateway +deployChaincode +pushd ../asset-transfer-basic/application-gateway-javascript +npm install +print "Start application" +npm start +popd + + +# Run Java application print "Initializing Java application" export CHAINCODE_NAME=java_gateway deployChaincode