From 5816904fa6850ebf92fa20d349ff78fdad7bde34 Mon Sep 17 00:00:00 2001 From: Peter Somogyvari Date: Thu, 9 Nov 2023 20:37:01 +0000 Subject: [PATCH] fix(connector-besu): error handling of DeployContractSolidityBytecodeEndpoint 1. Uses the new utility function from the core package in the catch block so that HTTP `statusCode` is matching the intent of the thrower (e.g. correctly differentiates between user-error and developer error) 2. Updates the `deployContract` method of the besu connector so that it correctly specifies the intent of the errors thrown as either user error or developer error via setting the `statusCode` property of the HTTP errors to either 4xx or 5xx depending on the cause. 3. Provides a template for future similar changes (of which we'll need dozens to update all the REST API endpoints) Depends on #2868 Related to but does not conclude: https://github.com/hyperledger/cacti/issues/1747 Signed-off-by: Peter Somogyvari --- .../package.json | 2 + .../plugin-ledger-connector-besu.ts | 161 +++++++++--------- ...loy-contract-solidity-bytecode-endpoint.ts | 15 +- .../openapi/openapi-validation.test.ts | 2 +- yarn.lock | 9 + 5 files changed, 105 insertions(+), 84 deletions(-) diff --git a/packages/cactus-plugin-ledger-connector-besu/package.json b/packages/cactus-plugin-ledger-connector-besu/package.json index e027b4c084..989f2522f1 100644 --- a/packages/cactus-plugin-ledger-connector-besu/package.json +++ b/packages/cactus-plugin-ledger-connector-besu/package.json @@ -59,6 +59,7 @@ "@hyperledger/cactus-core-api": "2.0.0-alpha.2", "axios": "1.5.1", "express": "4.18.2", + "http-errors": "2.0.0", "joi": "17.9.1", "openapi-types": "9.1.0", "prom-client": "13.2.0", @@ -76,6 +77,7 @@ "@hyperledger/cactus-plugin-keychain-memory": "2.0.0-alpha.2", "@hyperledger/cactus-test-tooling": "2.0.0-alpha.2", "@types/express": "4.17.19", + "@types/http-errors": "2.0.4", "key-encoder": "2.0.3", "socket.io": "4.5.4", "web3-core": "1.6.1", diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts index 8f4d902af4..350d6649a4 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/plugin-ledger-connector-besu.ts @@ -5,6 +5,7 @@ import type { Server as SocketIoServer } from "socket.io"; import type { Socket as SocketIoSocket } from "socket.io"; import type { Express } from "express"; import { Optional } from "typescript-optional"; +import createHttpError from "http-errors"; import OAS from "../json/openapi.json"; @@ -814,93 +815,101 @@ export class PluginLedgerConnectorBesu const fnTag = `${this.className}#deployContract()`; Checks.truthy(req, `${fnTag} req`); if (isWeb3SigningCredentialNone(req.web3SigningCredential)) { - throw new Error(`${fnTag} Cannot deploy contract with pre-signed TX`); - } - const { contractName } = req; - if (req.keychainId != undefined && req.contractName != undefined) { - const keychainPlugin = this.pluginRegistry.findOneByKeychainId( - req.keychainId, - ); - Checks.truthy( - keychainPlugin, - `${fnTag} keychain for ID:"${req.keychainId}"`, + throw createHttpError[400]( + `${fnTag} Cannot deploy contract with pre-signed TX`, ); - if (!keychainPlugin.has(req.contractName)) { - throw new Error( - `${fnTag} Cannot create an instance of the contract because the contractName and the contractName on the keychain does not match`, - ); - } - const networkId = await this.web3.eth.net.getId(); + } + const { keychainId, contractName } = req; + if (!keychainId || !req.contractName) { + const errorMessage = `${fnTag} Cannot deploy contract without keychainId and the contractName.`; + throw createHttpError[400](errorMessage); + } - const tmpContract = new this.web3.eth.Contract(req.contractAbi); - const deployment = tmpContract.deploy({ - data: req.bytecode, - arguments: req.constructorArgs, - }); + const keychainPlugin = this.pluginRegistry.findOneByKeychainId(keychainId); - const abi = deployment.encodeABI(); - const data = abi.startsWith("0x") ? abi : `0x${abi}`; - this.log.debug(`Deploying "${req.contractName}" with data %o`, data); + if (!keychainPlugin) { + const errorMessage = + `${fnTag} The plugin registry does not contain` + + ` a keychain plugin for ID:"${req.keychainId}"`; + throw createHttpError[400](errorMessage); + } - const web3SigningCredential = req.web3SigningCredential as - | Web3SigningCredentialPrivateKeyHex - | Web3SigningCredentialCactusKeychainRef; + if (!keychainPlugin.has(contractName)) { + const errorMessage = + `${fnTag} Cannot create an instance of the contract instance because` + + `the contractName in the request does not exist on the keychain`; + throw new createHttpError[400](errorMessage); + } - const runTxResponse = await this.transact({ - transactionConfig: { - data, - from: web3SigningCredential.ethAccount, - gas: req.gas, - gasPrice: req.gasPrice, - }, - consistencyStrategy: { - blockConfirmations: 0, - receiptType: ReceiptType.NodeTxPoolAck, - timeoutMs: req.timeoutMs || 60000, - }, - web3SigningCredential, - privateTransactionConfig: req.privateTransactionConfig, - }); + const networkId = await this.web3.eth.net.getId(); - const keychainHasContract = await keychainPlugin.has(contractName); - if (keychainHasContract) { - this.log.debug(`Keychain has the contract, updating networks...`); - - const { transactionReceipt: receipt } = runTxResponse; - const { status, contractAddress } = receipt; - - if (status && contractAddress) { - const networkInfo = { address: contractAddress }; - const contractStr = await keychainPlugin.get(contractName); - const contractJSON = JSON.parse(contractStr); - this.log.debug("Contract JSON: \n%o", JSON.stringify(contractJSON)); - const contract = new this.web3.eth.Contract( - contractJSON.abi, - contractAddress, - ); - this.contracts[contractName] = contract; + const tmpContract = new this.web3.eth.Contract(req.contractAbi); + const deployment = tmpContract.deploy({ + data: req.bytecode, + arguments: req.constructorArgs, + }); - const network = { [networkId]: networkInfo }; - contractJSON.networks = network; + const abi = deployment.encodeABI(); + const data = abi.startsWith("0x") ? abi : `0x${abi}`; + this.log.debug(`Deploying "${req.contractName}" with data %o`, data); - await keychainPlugin.set(contractName, JSON.stringify(contractJSON)); - } - } else { - throw new Error( - `${fnTag} Cannot create an instance of the contract because the contractName and the contractName on the keychain does not match`, - ); - } + const web3SigningCredential = req.web3SigningCredential as + | Web3SigningCredentialPrivateKeyHex + | Web3SigningCredentialCactusKeychainRef; - // creating solidity byte code response - const deployResponse: DeployContractSolidityBytecodeV1Response = { - transactionReceipt: runTxResponse.transactionReceipt, - }; + const runTxResponse = await this.transact({ + transactionConfig: { + data, + from: web3SigningCredential.ethAccount, + gas: req.gas, + gasPrice: req.gasPrice, + }, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NodeTxPoolAck, + timeoutMs: req.timeoutMs || 60000, + }, + web3SigningCredential, + privateTransactionConfig: req.privateTransactionConfig, + }); + + const keychainHasContract = await keychainPlugin.has(contractName); - return deployResponse; + if (!keychainHasContract) { + const errorMessage = + `${fnTag} Cannot create an instance of the contract instance because` + + `the contractName in the request does not exist on the keychain`; + throw new createHttpError[400](errorMessage); } - throw new Error( - `${fnTag} Cannot deploy contract without keychainId and the contractName`, - ); + + this.log.debug(`Keychain has the contract, updating networks...`); + + const { transactionReceipt: receipt } = runTxResponse; + const { status, contractAddress } = receipt; + + if (status && contractAddress) { + const networkInfo = { address: contractAddress }; + const contractStr = await keychainPlugin.get(contractName); + const contractJSON = JSON.parse(contractStr); + this.log.debug("Contract JSON: \n%o", JSON.stringify(contractJSON)); + const contract = new this.web3.eth.Contract( + contractJSON.abi, + contractAddress, + ); + this.contracts[contractName] = contract; + + const network = { [networkId]: networkInfo }; + contractJSON.networks = network; + + await keychainPlugin.set(contractName, JSON.stringify(contractJSON)); + } + + // creating solidity byte code response + const deployResponse: DeployContractSolidityBytecodeV1Response = { + transactionReceipt: runTxResponse.transactionReceipt, + }; + + return deployResponse; } public async signTransaction( diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/deploy-contract-solidity-bytecode-endpoint.ts b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/deploy-contract-solidity-bytecode-endpoint.ts index 1d655e5e97..b65c9c5e84 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/deploy-contract-solidity-bytecode-endpoint.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/deploy-contract-solidity-bytecode-endpoint.ts @@ -1,4 +1,4 @@ -import { Express, Request, Response } from "express"; +import type { Express, Request, Response } from "express"; import { IWebServiceEndpoint, @@ -14,7 +14,10 @@ import { IAsyncProvider, } from "@hyperledger/cactus-common"; -import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; +import { + handleRestEndpointException, + registerWebServiceEndpoint, +} from "@hyperledger/cactus-core"; import { PluginLedgerConnectorBesu } from "../plugin-ledger-connector-besu"; import { DeployContractSolidityBytecodeV1Request } from "../generated/openapi/typescript-axios"; @@ -86,6 +89,7 @@ export class DeployContractSolidityBytecodeEndpoint } public async handleRequest(req: Request, res: Response): Promise { + const fnTag = `${this.className}#handleRequest()`; const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; this.log.debug(reqTag); const reqBody: DeployContractSolidityBytecodeV1Request = req.body; @@ -93,11 +97,8 @@ export class DeployContractSolidityBytecodeEndpoint const resBody = await this.options.connector.deployContract(reqBody); res.json(resBody); } catch (ex) { - this.log.error(`Crash while serving ${reqTag}`, ex); - res.status(500).json({ - message: "Internal Server Error", - error: ex?.stack || ex?.message, - }); + const errorMsg = `${reqTag} ${fnTag} Failed to deploy contract:`; + handleRestEndpointException({ errorMsg, log: this.log, error: ex, res }); } } } diff --git a/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/openapi/openapi-validation.test.ts b/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/openapi/openapi-validation.test.ts index 5aa8c9a9db..ad47ac029f 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/openapi/openapi-validation.test.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/openapi/openapi-validation.test.ts @@ -119,7 +119,7 @@ test(testCase, async (t: Test) => { }); const listenOptions: IListenOptions = { - hostname: "localhost", + hostname: "127.0.0.1", port: 0, server, }; diff --git a/yarn.lock b/yarn.lock index 12586c29f9..480e57863d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7637,8 +7637,10 @@ __metadata: "@hyperledger/cactus-plugin-keychain-memory": 2.0.0-alpha.2 "@hyperledger/cactus-test-tooling": 2.0.0-alpha.2 "@types/express": 4.17.19 + "@types/http-errors": 2.0.4 axios: 1.5.1 express: 4.18.2 + http-errors: 2.0.0 joi: 17.9.1 key-encoder: 2.0.3 openapi-types: 9.1.0 @@ -12891,6 +12893,13 @@ __metadata: languageName: node linkType: hard +"@types/http-errors@npm:2.0.4": + version: 2.0.4 + resolution: "@types/http-errors@npm:2.0.4" + checksum: 1f3d7c3b32c7524811a45690881736b3ef741bf9849ae03d32ad1ab7062608454b150a4e7f1351f83d26a418b2d65af9bdc06198f1c079d75578282884c4e8e3 + languageName: node + linkType: hard + "@types/http-proxy@npm:^1.17.8": version: 1.17.9 resolution: "@types/http-proxy@npm:1.17.9"