From 8ced9fcff39143acc8d94d62777466fbeb00d515 Mon Sep 17 00:00:00 2001 From: ruzell22 Date: Thu, 15 Aug 2024 13:27:28 +0800 Subject: [PATCH] fix(connector-besu): do not crash if ledger unreachable - send HTTP 503 Primary Changes --------------- 1. Yarn patch to web3-eth-acconts@1.6.1 so that it does not crash nodejs process Without the catch block, the rejection is an unhandled rejection that bubbles up to the top of the callstack where NodeJS itself catches it and then crashes the entire process. (used to be that it just logged a warning but since some of the newer versions it crashes which allows us to find these bugs in our code / library's code) 2. It is returning a 503 instead of a 500 3. Added a static retry-after header value of 5 seconds 4. Added test case in test-ledger which has run-transaction at the end that is expected to give error 503 when the backing ledger is unavailable Fixes: #3406 Co-authored-by: Peter Somogyvari Signed-off-by: ruzell22 Signed-off-by: Peter Somogyvari --- ...b3-eth-accounts-npm-1.6.1-c95f31ca81.patch | 13 + package.json | 1 + .../src/main/typescript/http/http-header.ts | 157 ++++ .../src/main/typescript/public-api.ts | 2 + .../package.json | 6 +- .../src/main/json/openapi.tpl.json | 128 +++ ...bsocket-provider-abnormal-closure-error.ts | 38 + .../web-services/run-transaction-endpoint.ts | 37 +- ...et-provider-abnormal-closure-error.test.ts | 41 + .../openapi/openapi-validation.test.ts | 819 ++++++++++++++++++ .../run-transaction-endpoint.test.ts | 129 +++ yarn.lock | 77 +- 12 files changed, 1417 insertions(+), 31 deletions(-) create mode 100644 .yarn/patches/web3-eth-accounts-npm-1.6.1-c95f31ca81.patch create mode 100644 packages/cactus-common/src/main/typescript/http/http-header.ts create mode 100644 packages/cactus-plugin-ledger-connector-besu/src/main/typescript/common/is-web3-websocket-provider-abnormal-closure-error.ts create mode 100644 packages/cactus-plugin-ledger-connector-besu/src/test/typescript/unit/common/is-web3-websocket-provider-abnormal-closure-error.test.ts create mode 100644 packages/cactus-test-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/openapi/openapi-validation.test.ts create mode 100644 packages/cactus-test-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-validator-besu/run-transaction-endpoint.test.ts diff --git a/.yarn/patches/web3-eth-accounts-npm-1.6.1-c95f31ca81.patch b/.yarn/patches/web3-eth-accounts-npm-1.6.1-c95f31ca81.patch new file mode 100644 index 00000000000..984e9016937 --- /dev/null +++ b/.yarn/patches/web3-eth-accounts-npm-1.6.1-c95f31ca81.patch @@ -0,0 +1,13 @@ +diff --git a/lib/index.js b/lib/index.js +index de2853c247b334e6b22cee42b3e597281c44efdc..787aecf6758228c092f1a3cd8097f64e76cc573d 100644 +--- a/lib/index.js ++++ b/lib/index.js +@@ -344,7 +344,7 @@ function _handleTxPricing(_this, tx) { + throw Error("Network doesn't support eip-1559"); + resolve({ gasPrice }); + } +- }); ++ }).catch((ex) => reject(ex)); + } + } + catch (error) { diff --git a/package.json b/package.json index c175dda0ba2..1c759093939 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ "ws": ">=1.1.5", "xml2js": ">=0.5.0", "yargs-parser": ">=18.1.1", + "web3-eth-accounts@npm:1.6.1": "patch:web3-eth-accounts@npm%3A1.6.1#~/.yarn/patches/web3-eth-accounts-npm-1.6.1-c95f31ca81.patch", "zod": ">=3.22.3" }, "devDependencies": { diff --git a/packages/cactus-common/src/main/typescript/http/http-header.ts b/packages/cactus-common/src/main/typescript/http/http-header.ts new file mode 100644 index 00000000000..d1a77e10102 --- /dev/null +++ b/packages/cactus-common/src/main/typescript/http/http-header.ts @@ -0,0 +1,157 @@ +/** + * A list of well-known headers as published on Wikipedia. + * @see https://en.wikipedia.org/wiki/List_of_HTTP_header_fields + * + * TODO Finish documenting each enum item and make sure to also include + * the examples provided on the linked Wikipedia page above. The first + * few headers are documented this way but we need all of them like that. + * + * TODO Ensure that there are no typos in the header names. + */ +export enum HttpHeader { + // Standard request fields + + /** + * Acceptable instance-manipulations for the request. + * @example A-IM: feed + * @see https://datatracker.ietf.org/doc/html/rfc3229 + */ + A_IM = "A-IM", + /** + * Media type(s) that is/are acceptable for the response. See Content negotiation. + * @example Accept: text/html + * @see https://datatracker.ietf.org/doc/html/rfc9110 + */ + Accept = "Accept", + /** + * Character sets that are acceptable. + * @example Accept-Charset: utf-8 + * @see https://datatracker.ietf.org/doc/html/rfc9110 + */ + AcceptCharset = "Accept-Charset", + /** + * Acceptable version in time. + * @example Accept-Datetime: Thu, 31 May 2007 20:35:00 GMT + * @see https://datatracker.ietf.org/doc/html/rfc7089 + */ + AcceptDatetime = "Accept-Datetime", + AcceptEncoding = "Accept-Encoding", + AcceptLanguage = "Accept-Language", + AccessControlAllowOrigin = "Access-Control-Allow-Origin", + AccessControlAllowCredentials = "Access-Control-Allow-Credentials", + AccessControlExposeHeaders = "Access-Control-Expose-Headers", + AccessControlMaxAge = "Access-Control-Max-Age", + AccessControlAllowMethods = "Access-Control-Allow-Methods", + AccessControlAllowHeaders = "Access-Control-Allow-Headers", + Authorization = "Authorization", + CacheControl = "Cache-Control", + Connection = "Connection", + ContentDisposition = "Content-Disposition", + ContentEncoding = "Content-Encoding", + ContentLength = "Content-Length", + ContentLocation = "Content-Location", + ContentMD5 = "Content-MD5", + ContentType = "Content-Type", + Cookie = "Cookie", + Date = "Date", + Expect = "Expect", + Forwarded = "Forwarded", + From = "From", + Host = "Host", + IfMatch = "If-Match", + IfModifiedSince = "If-Modified-Since", + IfNoneMatch = "If-None-Match", + IfRange = "If-Range", + IfUnmodifiedSince = "If-Unmodified-Since", + MaxForwards = "Max-Forwards", + Origin = "Origin", + Pragma = "Pragma", + Prefer = "Prefer", + ProxyAuthorization = "Proxy-Authorization", + Range = "Range", + Referer = "Referer", + TE = "TE", + Trailer = "Trailer", + TransferEncoding = "Transfer-Encoding", + Upgrade = "Upgrade", + UserAgent = "User-Agent", + + // Common non-standard request fields + UpgradeInsecureRequests = "Upgrade-Insecure-Requests", + XRequestedWith = "X-Requested-With", + DNT = "DNT", + XForwardedFor = "X-Forwarded-For", + XForwardedHost = "X-Forwarded-Host", + XForwardedProto = "X-Forwarded-Proto", + FrontEndHttps = "Front-End-Https", + XHttpMethodOverride = "X-Http-Method-Override", + XAttDeviceId = "X-Att-DeviceId", + XWapProfile = "X-Wap-Profile", + ProxyConnection = "Proxy-Connection", + XUIDH = "X-UIDH", + XCsrfToken = "X-Csrf-Token", + XRequestId = "X-Request-ID", // Alternative X-Request-Id + CorrelationId = "X-Correlation-ID", // Alternative Correlation-ID + SaveData = "Save-Data", + SecGpc = "Sec-GPC", + + // Standard response fields + AcceptCH = "Accept-CH", + AcceptPatch = "Accept-Patch", + AltSvc = "Alt-Svc", + Age = "Age", + Allow = "Allow", + Expires = "Expires", + IM = "IM", + LastModified = "Last-Modified", + Link = "Link", + Location = "Location", + P3P = "P3P", + ProxyAuthenticate = "Proxy-Authenticate", + PublicKeyPins = "Public-Key-Pins", + /** + * f an entity is temporarily unavailable, this instructs the client + * to try again later. Value could be a specified period of time + * (in seconds) or a HTTP-date. + * + * There are two accepted formats when it comes to the values of the header: + * ```http + * Retry-After: + * Retry-After: + * ``` + * + * `` + * A date after which to retry. See the Date header for more details on the HTTP date format. + * + * `` + * A non-negative decimal integer indicating the seconds to delay after the response is received. + * + * @example Retry-After: 120 + * @example Retry-After: Fri, 07 Nov 2014 23:59:59 GMT + * + * @see https://datatracker.ietf.org/doc/html/rfc9110#section-10.2.3 + */ + RetryAfter = "Retry-After", + Server = "Server", + SetCookie = "Set-Cookie", + StrictTransportSecurity = "Strict-Transport-Security", + Tk = "Tk", + Vary = "Vary", + Via = "Via", // Same as request field + /** + * Indicates the authentication scheme that should be used to access the requested entity. + * @example WWW-Authenticate: Basic + * @see https://datatracker.ietf.org/doc/html/rfc9110 + */ + WWWAuthenticate = "WWW-Authenticate", + XFrameOptions = "X-Frame-Options", + + // Common non-standard response fields + ContentSecurityPolicy = "Content-Security-Policy", + ExpectCT = "Expect-CT", + NEL = "NEL", + PermissionsPolicy = "Permissions-Policy", + Refresh = "Refresh", + ReportTo = "Report-To", + Timing_Allow_Origin = "Timing-Allow-Origin", +} diff --git a/packages/cactus-common/src/main/typescript/public-api.ts b/packages/cactus-common/src/main/typescript/public-api.ts index 18e4ceb6c8d..6dd0e177d06 100755 --- a/packages/cactus-common/src/main/typescript/public-api.ts +++ b/packages/cactus-common/src/main/typescript/public-api.ts @@ -47,3 +47,5 @@ export { } from "./http/express-http-verb-method-name"; export { isGrpcStatusObjectWithCode } from "./grpc/is-grpc-status-object-with-code"; + +export { HttpHeader } from "./http/http-header"; diff --git a/packages/cactus-plugin-ledger-connector-besu/package.json b/packages/cactus-plugin-ledger-connector-besu/package.json index 432992f5c7d..025b74d2c1f 100644 --- a/packages/cactus-plugin-ledger-connector-besu/package.json +++ b/packages/cactus-plugin-ledger-connector-besu/package.json @@ -80,7 +80,8 @@ "web3-eth": "1.6.1", "web3-eth-contract": "1.6.1", "web3-utils": "1.6.1", - "web3js-quorum": "22.4.0" + "web3js-quorum": "22.4.0", + "websocket-event-codes": "1.1.0" }, "devDependencies": { "@hyperledger/cactus-plugin-keychain-memory": "2.0.0", @@ -105,7 +106,8 @@ "tsx": "4.16.2", "uuid": "10.0.0", "web3-core": "1.6.1", - "web3-eth": "1.6.1" + "web3-eth": "1.6.1", + "web3-eth-accounts": "patch:web3-eth-accounts@npm%3A1.6.1#~/.yarn/patches/web3-eth-accounts-npm-1.6.1-c95f31ca81.patch" }, "engines": { "node": ">=18", diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/json/openapi.tpl.json b/packages/cactus-plugin-ledger-connector-besu/src/main/json/openapi.tpl.json index 545d518403c..50f7b3457ad 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/json/openapi.tpl.json +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/json/openapi.tpl.json @@ -1013,6 +1013,14 @@ "transactionInputData": {}, "callOutput": {} } + }, + "BackingLedgerUnavailableError": { + "properties": { + "message": { + "type": "string", + "example": "Backing Ledger Unavailable\n" + } + } } } }, @@ -1038,6 +1046,16 @@ } } } + }, + "503": { + "description": "API is unable to reach the validator", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/BackingLedgerUnavailableError" + } + } + } } } } @@ -1072,6 +1090,16 @@ } } } + }, + "503": { + "description": "API is unable to reach the validator", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/BackingLedgerUnavailableError" + } + } + } } } } @@ -1106,6 +1134,16 @@ } } } + }, + "503": { + "description": "API is unable to reach the validator", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/BackingLedgerUnavailableError" + } + } + } } } } @@ -1140,6 +1178,16 @@ } } } + }, + "503": { + "description": "API is unable to reach the validator", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/BackingLedgerUnavailableError" + } + } + } } } } @@ -1174,6 +1222,16 @@ } } } + }, + "503": { + "description": "API is unable to reach the validator", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/BackingLedgerUnavailableError" + } + } + } } } } @@ -1208,6 +1266,16 @@ } } } + }, + "503": { + "description": "API is unable to reach the validator", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/BackingLedgerUnavailableError" + } + } + } } } } @@ -1242,6 +1310,16 @@ } } } + }, + "503": { + "description": "API is unable to reach the validator", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/BackingLedgerUnavailableError" + } + } + } } } } @@ -1276,6 +1354,16 @@ } } } + }, + "503": { + "description": "API is unable to reach the validator", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/BackingLedgerUnavailableError" + } + } + } } } } @@ -1310,6 +1398,16 @@ } } } + }, + "503": { + "description": "API is unable to reach the validator", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/BackingLedgerUnavailableError" + } + } + } } } } @@ -1349,6 +1447,16 @@ }, "404": { "description": "Not able to find the corresponding tranaction from the transaction hash" + }, + "503": { + "description": "API is unable to reach the validator", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/BackingLedgerUnavailableError" + } + } + } } } } @@ -1374,6 +1482,16 @@ } } } + }, + "503": { + "description": "API is unable to reach the validator", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/BackingLedgerUnavailableError" + } + } + } } } } @@ -1408,6 +1526,16 @@ } } } + }, + "503": { + "description": "API is unable to reach the validator", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/BackingLedgerUnavailableError" + } + } + } } } } diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/common/is-web3-websocket-provider-abnormal-closure-error.ts b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/common/is-web3-websocket-provider-abnormal-closure-error.ts new file mode 100644 index 00000000000..82739d2ffe0 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/common/is-web3-websocket-provider-abnormal-closure-error.ts @@ -0,0 +1,38 @@ +import { ABNORMAL_CLOSURE } from "websocket-event-codes"; + +export const WEB3_CONNECTION_NOT_OPEN_ON_SEND = "connection not open on send()"; + +/** + * Checks if an error was thrown due to the web3js websocket provider disconnecting. + * + * @param err - The error object to check. + * @returns `true` if the error is an instance of `Error`, has a `message` + * property indicating a websocket provider abnormal closure error. + * Otherwise, returns `false`. + * + * **Example:** + * ```typescript + * try { + * // ... code that might throw an error + * } catch (err: unknown) { + * if (isWeb3WebsocketProviderAbnormalClosureError(err)) { + * // Error is specifically due to websocket provider abnormal closure + * console.error("Websocket provider abnormal closure error:", err); + * } else { + * // Handle other types of errors + * console.error("Unknown error:", err); + * } + * } + * ``` + */ +export function isWeb3WebsocketProviderAbnormalClosureError( + err: unknown, +): err is Error & { code: typeof ABNORMAL_CLOSURE } { + if (!err) { + return false; + } + if (!(err instanceof Error)) { + return false; + } + return err.message.includes(WEB3_CONNECTION_NOT_OPEN_ON_SEND); +} diff --git a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/run-transaction-endpoint.ts b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/run-transaction-endpoint.ts index 096be62d005..6387906209b 100644 --- a/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/run-transaction-endpoint.ts +++ b/packages/cactus-plugin-ledger-connector-besu/src/main/typescript/web-services/run-transaction-endpoint.ts @@ -1,4 +1,5 @@ import { Express, Request, Response } from "express"; +import { HttpStatusCode } from "axios"; import { Logger, @@ -6,18 +7,23 @@ import { LogLevelDesc, LoggerProvider, IAsyncProvider, + HttpHeader, } from "@hyperledger/cactus-common"; import { IEndpointAuthzOptions, IExpressRequestHandler, IWebServiceEndpoint, } from "@hyperledger/cactus-core-api"; -import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; +import { + handleRestEndpointException, + registerWebServiceEndpoint, +} from "@hyperledger/cactus-core"; import { PluginLedgerConnectorBesu } from "../plugin-ledger-connector-besu"; import OAS from "../../json/openapi.json"; import { RunTransactionRequest } from "../generated/openapi/typescript-axios"; +import { isWeb3WebsocketProviderAbnormalClosureError } from "../common/is-web3-websocket-provider-abnormal-closure-error"; export interface IRunTransactionEndpointOptions { logLevel?: LogLevelDesc; @@ -82,19 +88,36 @@ export class RunTransactionEndpoint implements IWebServiceEndpoint { return this.handleRequest.bind(this); } + private handleLedgerNotAccessibleError(res: Response): void { + const fn = "handleLedgerNotAccessibleError()"; + this.log.debug( + "%s WebSocketProvider disconnected from ledger. Sending HttpStatusCode.ServiceUnavailable...", + fn, + ); + + res + .header(HttpHeader.RetryAfter, "5") + .status(HttpStatusCode.ServiceUnavailable) + .json({ + success: false, + error: "Could not establish connection to the backing ledger.", + }); + } + public async handleRequest(req: Request, res: Response): Promise { const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; + const { log } = this; this.log.debug(reqTag); const reqBody: RunTransactionRequest = req.body; try { const resBody = await this.options.connector.transact(reqBody); res.json({ success: true, data: resBody }); - } catch (ex) { - this.log.error(`Crash while serving ${reqTag}`, ex); - res.status(500).json({ - message: "Internal Server Error", - error: ex?.stack || ex?.message, - }); + } catch (ex: unknown) { + if (isWeb3WebsocketProviderAbnormalClosureError(ex)) { + return this.handleLedgerNotAccessibleError(res); + } + const errorMsg = `request handler fn crashed for: ${reqTag}`; + await handleRestEndpointException({ errorMsg, log, error: ex, res }); } } } diff --git a/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/unit/common/is-web3-websocket-provider-abnormal-closure-error.test.ts b/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/unit/common/is-web3-websocket-provider-abnormal-closure-error.test.ts new file mode 100644 index 00000000000..99182e239de --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-besu/src/test/typescript/unit/common/is-web3-websocket-provider-abnormal-closure-error.test.ts @@ -0,0 +1,41 @@ +import "jest-extended"; + +import { isWeb3WebsocketProviderAbnormalClosureError } from "../../../../main/typescript/common/is-web3-websocket-provider-abnormal-closure-error"; +import { WEB3_CONNECTION_NOT_OPEN_ON_SEND } from "../../../../main/typescript/common/is-web3-websocket-provider-abnormal-closure-error"; + +describe("isWeb3WebsocketProviderAbnormalClosureError", () => { + it("should return false for non-error values", () => { + expect(isWeb3WebsocketProviderAbnormalClosureError(null)).toBe(false); + expect(isWeb3WebsocketProviderAbnormalClosureError(undefined)).toBe(false); + expect(isWeb3WebsocketProviderAbnormalClosureError(123)).toBe(false); + expect(isWeb3WebsocketProviderAbnormalClosureError("some string")).toBe( + false, + ); + expect(isWeb3WebsocketProviderAbnormalClosureError(Symbol("symbol"))).toBe( + false, + ); + }); + + it("should return false for error objects without code property", () => { + const errorWithoutCode = new Error("Some generic error"); + expect(isWeb3WebsocketProviderAbnormalClosureError(errorWithoutCode)).toBe( + false, + ); + }); + + it("should return false for error objects with incorrect code property", () => { + const errorWithIncorrectCode: Error = new Error("Some error"); + + (errorWithIncorrectCode as unknown as Record).code = + "some_other_code"; + + expect( + isWeb3WebsocketProviderAbnormalClosureError(errorWithIncorrectCode), + ).toBe(false); + }); + + it("it returns true when the correct error message is set", () => { + const err = new Error(WEB3_CONNECTION_NOT_OPEN_ON_SEND); + expect(isWeb3WebsocketProviderAbnormalClosureError(err)).toBeTrue(); + }); +}); diff --git a/packages/cactus-test-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/openapi/openapi-validation.test.ts b/packages/cactus-test-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/openapi/openapi-validation.test.ts new file mode 100644 index 00000000000..b913ddef521 --- /dev/null +++ b/packages/cactus-test-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/openapi/openapi-validation.test.ts @@ -0,0 +1,819 @@ +import "jest-extended"; +import { v4 as uuidv4 } from "uuid"; +import { Server as SocketIoServer } from "socket.io"; +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { + Web3SigningCredentialType, + PluginLedgerConnectorBesu, + BesuApiClient, + IPluginLedgerConnectorBesuOptions, + ReceiptType, + RunTransactionRequest, + InvokeContractV1Request, + EthContractInvocationType, + DeployContractSolidityBytecodeV1Request, + SignTransactionRequest, + GetBalanceV1Request, + GetPastLogsV1Request, + GetBlockV1Request, + GetBesuRecordV1Request, + RunTransactionResponse, +} from "@hyperledger/cactus-plugin-ledger-connector-besu/src/main/typescript/public-api"; +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; +import { + BesuTestLedger, + pruneDockerAllIfGithubAction, +} from "@hyperledger/cactus-test-tooling"; +import KeyEncoder from "key-encoder"; +import { + IListenOptions, + KeyFormat, + LogLevelDesc, + LoggerProvider, + Secp256k1Keys, + Servers, +} from "@hyperledger/cactus-common"; +import { Constants } from "@hyperledger/cactus-core-api"; +import express from "express"; +import bodyParser from "body-parser"; +import http from "http"; +import HelloWorldContractJson from "../../../../solidity/hello-world-contract/HelloWorld.json"; +import { AddressInfo } from "net"; +import { BesuApiClientOptions } from "@hyperledger/cactus-plugin-ledger-connector-besu/src/main/typescript/public-api"; + +import { installOpenapiValidationMiddleware } from "@hyperledger/cactus-core"; +import OAS from "@hyperledger/cactus-plugin-ledger-connector-besu/src/main/json/openapi.json"; +import { Account } from "web3-core"; + +const logLevel: LogLevelDesc = "TRACE"; +const testCase = "able to validate OpenAPI requests"; + +const log = LoggerProvider.getOrCreate({ + label: "connector-besu-openapi-validation.test.ts", + level: logLevel, +}); + +describe("PluginLedgerConnectorBesu", () => { + const fDeploy = "deployContractSolBytecodeV1"; + const fInvoke = "invokeContractV1"; + const fRun = "runTransactionV1"; + const fSign = "signTransactionV1"; + const fBalance = "getBalanceV1"; + const fBlock = "getBlockV1"; + const fPastLogs = "getPastLogsV1"; + const fRecord = "getBesuRecordV1"; + const cOk = "without bad request error"; + const cWithoutParams = "not sending all required parameters"; + const cInvalidParams = "sending invalid parameters"; + + const keyEncoder: KeyEncoder = new KeyEncoder("secp256k1"); + const keychainIdForSigned = uuidv4(); + const keychainIdForUnsigned = uuidv4(); + const keychainRefForSigned = uuidv4(); + const keychainRefForUnsigned = uuidv4(); + + let besuTestLedger: BesuTestLedger; + let testEthAccount2: Account; + let firstHighNetWorthAccount: string; + let apiClient: BesuApiClient; + let testEthAccount1: Account; + let httpServer: http.Server; + + beforeAll(async () => { + await pruneDockerAllIfGithubAction({ logLevel }); + }); + + afterAll(async () => { + await besuTestLedger.stop(); + await besuTestLedger.destroy(); + }); + + afterAll(async () => await Servers.shutdown(httpServer)); + + beforeAll(async () => { + besuTestLedger = new BesuTestLedger(); + await besuTestLedger.start(); + + const rpcApiHttpHost = await besuTestLedger.getRpcApiHttpHost(); + const rpcApiWsHost = await besuTestLedger.getRpcApiWsHost(); + + testEthAccount1 = await besuTestLedger.createEthTestAccount(); + testEthAccount2 = await besuTestLedger.createEthTestAccount(); + firstHighNetWorthAccount = besuTestLedger.getGenesisAccountPubKey(); + + // keychainPlugin for signed transactions + const { privateKey } = Secp256k1Keys.generateKeyPairsBuffer(); + const keyHex = privateKey.toString("hex"); + const pem = keyEncoder.encodePrivate(keyHex, KeyFormat.Raw, KeyFormat.PEM); + const signedKeychainPlugin = new PluginKeychainMemory({ + instanceId: uuidv4(), + keychainId: keychainIdForSigned, + backend: new Map([[keychainRefForSigned, pem]]), + logLevel, + }); + + // keychainPlugin for unsigned transactions + const keychainEntryValue = testEthAccount1.privateKey; + const unsignedKeychainPlugin = new PluginKeychainMemory({ + instanceId: uuidv4(), + keychainId: keychainIdForUnsigned, + backend: new Map([[keychainRefForUnsigned, keychainEntryValue]]), + logLevel, + }); + unsignedKeychainPlugin.set( + HelloWorldContractJson.contractName, + JSON.stringify(HelloWorldContractJson), + ); + + const pluginRegistry = new PluginRegistry({ + plugins: [signedKeychainPlugin, unsignedKeychainPlugin], + }); + + const options: IPluginLedgerConnectorBesuOptions = { + instanceId: uuidv4(), + rpcApiHttpHost, + rpcApiWsHost, + pluginRegistry, + logLevel, + }; + const connector = new PluginLedgerConnectorBesu(options); + pluginRegistry.add(connector); + + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + httpServer = http.createServer(expressApp); + + const wsApi = new SocketIoServer(httpServer, { + path: Constants.SocketIoConnectionPathV1, + }); + + const listenOptions: IListenOptions = { + hostname: "127.0.0.1", + port: 0, + server: httpServer, + }; + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + + const { address, port } = addressInfo; + const apiHost = `http://${address}:${port}`; + + const wsBasePath = apiHost + Constants.SocketIoConnectionPathV1; + log.info("WS base path: " + wsBasePath); + + const besuApiClientOptions = new BesuApiClientOptions({ + basePath: apiHost, + }); + apiClient = new BesuApiClient(besuApiClientOptions); + + await installOpenapiValidationMiddleware({ + logLevel, + app: expressApp, + apiSpec: OAS, + }); + + await connector.getOrCreateWebServices(); + await connector.registerWebServices(expressApp, wsApi); + }); + + test(`${testCase} - ${fDeploy} - ${cOk}`, async () => { + const parameters = { + keychainId: keychainIdForUnsigned, + contractName: HelloWorldContractJson.contractName, + contractAbi: HelloWorldContractJson.abi, + constructorArgs: [], + web3SigningCredential: { + ethAccount: testEthAccount1.address, + secret: testEthAccount1.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + bytecode: HelloWorldContractJson.bytecode, + gas: 1000000, + }; + const res = await apiClient.deployContractSolBytecodeV1( + parameters as DeployContractSolidityBytecodeV1Request, + ); + expect(res).toBeTruthy(); + expect(res.data).toBeTruthy(); + expect(res.status).toEqual(200); + }); + + test(`${testCase} - ${fDeploy} - ${cWithoutParams}`, async () => { + const parameters = { + keychainId: keychainIdForUnsigned, + contractAbi: HelloWorldContractJson.abi, + constructorArgs: [], + web3SigningCredential: { + ethAccount: testEthAccount1.address, + secret: testEthAccount1.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + }; + + await expect( + apiClient.deployContractSolBytecodeV1( + parameters as unknown as DeployContractSolidityBytecodeV1Request, + ), + ).rejects.toMatchObject({ + response: { + status: 400, + data: expect.arrayContaining([ + expect.objectContaining({ + path: expect.stringContaining("/body/contractName"), + }), + expect.objectContaining({ + path: expect.stringContaining("/body/bytecode"), + }), + expect.not.objectContaining({ + path: expect.stringContaining("/body/gas"), + }), + ]), + }, + }); + }); + + test(`${testCase} - ${fDeploy} - ${cInvalidParams}`, async () => { + const parameters = { + keychainId: keychainIdForUnsigned, + contractName: HelloWorldContractJson.contractName, + contractAbi: HelloWorldContractJson.abi, + constructorArgs: [], + web3SigningCredential: { + ethAccount: testEthAccount1.address, + secret: testEthAccount1.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + bytecode: HelloWorldContractJson.bytecode, + gas: 1000000, + fake: 4, + }; + + await expect( + apiClient.deployContractSolBytecodeV1( + parameters as DeployContractSolidityBytecodeV1Request, + ), + ).rejects.toMatchObject({ + response: { + status: 400, + data: expect.arrayContaining([ + expect.objectContaining({ path: "/body/fake" }), + ]), + }, + }); + }); + + test(`${testCase} - ${fInvoke} - ${cOk}`, async () => { + const parameters = { + contractName: "HelloWorld", + keychainId: keychainIdForUnsigned, + invocationType: EthContractInvocationType.Call, + methodName: "sayHello", + params: [], + signingCredential: { + ethAccount: testEthAccount1.address, + secret: testEthAccount1.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + }; + const res = await apiClient.invokeContractV1( + parameters as InvokeContractV1Request, + ); + expect(res).toBeTruthy(); + expect(res.data).toBeTruthy(); + expect(res.status).toEqual(200); + }); + + test(`${testCase} - ${fInvoke} - ${cWithoutParams}`, async () => { + const parameters = { + keychainId: keychainIdForUnsigned, + invocationType: EthContractInvocationType.Call, + methodName: "sayHello", + params: [], + signingCredential: { + ethAccount: testEthAccount1.address, + secret: testEthAccount1.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + }; + + await expect( + apiClient.invokeContractV1(parameters as any as InvokeContractV1Request), + ).rejects.toMatchObject({ + response: { + status: 400, + data: expect.arrayContaining([ + expect.objectContaining({ + path: expect.stringContaining("/body/contractName"), + }), + expect.not.objectContaining({ + path: expect.stringContaining("/body/gas"), + }), + ]), + }, + }); + }); + + test(`${testCase} - ${fInvoke} - ${cInvalidParams}`, async () => { + const parameters = { + contractName: "HelloWorld", + keychainId: keychainIdForUnsigned, + invocationType: EthContractInvocationType.Call, + methodName: "sayHello", + params: [], + signingCredential: { + ethAccount: testEthAccount1.address, + secret: testEthAccount1.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + fake: 4, + }; + + await expect( + apiClient.invokeContractV1(parameters as InvokeContractV1Request), + ).rejects.toMatchObject({ + response: { + status: 400, + data: expect.arrayContaining([ + expect.objectContaining({ + path: expect.stringContaining("/body/fake"), + }), + ]), + }, + }); + }); + + test(`${testCase} - ${fRun} - ${cOk}`, async () => { + const parameters = { + web3SigningCredential: { + ethAccount: testEthAccount1.address, + secret: testEthAccount1.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + transactionConfig: { + from: testEthAccount1.address, + to: testEthAccount2.address, + value: 10e7, + gas: 1000000, + }, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NodeTxPoolAck, + timeoutMs: 5000, + }, + }; + const res = await apiClient.runTransactionV1( + parameters as RunTransactionRequest, + ); + expect(res).toBeTruthy(); + expect(res.data).toBeTruthy(); + expect(res.data).toBeObject(); + expect(res.status).toEqual(200); + }); + + test(`${testCase} - ${fRun} - ${cWithoutParams}`, async () => { + const parameters = { + web3SigningCredential: { + ethAccount: testEthAccount1.address, + secret: testEthAccount1.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + transactionConfig: { + from: testEthAccount1.address, + to: testEthAccount2.address, + value: 10e7, + gas: 1000000, + }, + }; + + await expect( + apiClient.runTransactionV1(parameters as RunTransactionRequest), + ).rejects.toMatchObject({ + response: { + status: 400, + data: expect.arrayContaining([ + expect.objectContaining({ + path: expect.stringContaining("/body/consistencyStrategy"), + }), + ]), + }, + }); + }); + + test(`${testCase} - ${fRun} - ${cInvalidParams}`, async () => { + const parameters = { + web3SigningCredential: { + ethAccount: testEthAccount1.address, + secret: testEthAccount1.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + transactionConfig: { + from: testEthAccount1.address, + to: testEthAccount2.address, + value: 10e7, + gas: 1000000, + }, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NodeTxPoolAck, + timeoutMs: 5000, + }, + fake: 4, + }; + + await expect( + apiClient.runTransactionV1(parameters as RunTransactionRequest), + ).rejects.toMatchObject({ + response: { + status: 400, + data: expect.arrayContaining([ + expect.objectContaining({ + path: expect.stringContaining("/body/fake"), + }), + ]), + }, + }); + }); + + test(`${testCase} - ${fSign} - ${cOk}`, async () => { + const runTxRes = await apiClient.runTransactionV1({ + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.LedgerBlockAck, + timeoutMs: 5000, + }, + transactionConfig: { + from: testEthAccount1.address, + to: testEthAccount2.address, + value: 1, + gas: 10000000, + }, + web3SigningCredential: { + ethAccount: testEthAccount1.address, + secret: testEthAccount1.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + }); + + expect(runTxRes).toBeTruthy(); + expect(runTxRes).toBeObject(); + expect(runTxRes.status).toBeTruthy(); + expect(runTxRes.status).toEqual(200); + expect(runTxRes.data).toBeTruthy(); + + const body = runTxRes.data as unknown as { + readonly data: RunTransactionResponse; + }; + expect(body.data).toBeObject(); + expect(body.data.transactionReceipt).toBeObject(); + + const parameters = { + keychainId: keychainIdForSigned, + keychainRef: keychainRefForSigned, + transactionHash: (runTxRes.data as any).data.transactionReceipt + .transactionHash, + }; + + const res = await apiClient.signTransactionV1( + parameters as SignTransactionRequest, + ); + + expect(res).toBeTruthy(); + expect(res).toBeObject(); + expect(res.data).toBeTruthy(); + expect(res.data).toBeObject(); + expect(res.status).toEqual(200); + }); + + test(`${testCase} - ${fSign} - ${cWithoutParams}`, async () => { + const runTxRes = await apiClient.runTransactionV1({ + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.LedgerBlockAck, + timeoutMs: 5000, + }, + transactionConfig: { + from: testEthAccount1.address, + to: testEthAccount2.address, + value: 1, + gas: 10000000, + }, + web3SigningCredential: { + ethAccount: testEthAccount1.address, + secret: testEthAccount1.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + }); + expect(runTxRes).toBeTruthy(); + expect(runTxRes.status).toEqual(200); + expect(runTxRes.data).toBeTruthy(); + expect((runTxRes.data as any).data.transactionReceipt).toBeTruthy(); + + const parameters = { + keychainRef: keychainRefForSigned, + transactionHash: (runTxRes.data as any).data.transactionReceipt + .transactionHash, + }; + + await expect( + apiClient.signTransactionV1(parameters as SignTransactionRequest), + ).rejects.toMatchObject({ + response: { + status: 400, + data: expect.arrayContaining([ + expect.objectContaining({ + path: expect.stringContaining("/body/keychainId"), + }), + ]), + }, + }); + }); + + test(`${testCase} - ${fSign} - ${cInvalidParams}`, async () => { + const runTxRes = await apiClient.runTransactionV1({ + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.LedgerBlockAck, + timeoutMs: 5000, + }, + transactionConfig: { + from: testEthAccount1.address, + to: testEthAccount2.address, + value: 1, + gas: 10000000, + }, + web3SigningCredential: { + ethAccount: testEthAccount1.address, + secret: testEthAccount1.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + }); + expect(runTxRes).toBeTruthy(); + expect(runTxRes.status).toEqual(200); + expect(runTxRes.data).toBeTruthy(); + expect((runTxRes.data as any).data.transactionReceipt).toBeTruthy(); + + const parameters = { + keychainId: keychainIdForSigned, + keychainRef: keychainRefForSigned, + transactionHash: (runTxRes.data as any).data.transactionReceipt + .transactionHash, + fake: 4, + }; + + await expect( + apiClient.signTransactionV1(parameters as SignTransactionRequest), + ).rejects.toMatchObject({ + response: { + status: 400, + data: expect.arrayContaining([ + expect.objectContaining({ + path: expect.stringContaining("/body/fake"), + }), + ]), + }, + }); + }); + + test(`${testCase} - ${fBalance} - ${cOk}`, async () => { + const parameters = { address: firstHighNetWorthAccount }; + const res = await apiClient.getBalanceV1(parameters as GetBalanceV1Request); + expect(res.status).toEqual(200); + expect(res.data.balance).toBeTruthy(); + }); + + test(`${testCase} - ${fBalance} - ${cWithoutParams}`, async () => { + const parameters = {}; // Empty parameters object + + await expect( + apiClient.getBalanceV1(parameters as GetBalanceV1Request), + ).rejects.toMatchObject({ + response: { + status: 400, + data: expect.arrayContaining([ + expect.objectContaining({ + path: expect.stringContaining("/body/address"), + }), + ]), + }, + }); + }); + + test(`${testCase} - ${fBalance} - ${cInvalidParams}`, async () => { + const parameters = { + address: firstHighNetWorthAccount, + fake: 4, + }; + + await expect( + apiClient.getBalanceV1(parameters as GetBalanceV1Request), + ).rejects.toMatchObject({ + response: { + status: 400, + data: expect.arrayContaining([ + expect.objectContaining({ + path: expect.stringContaining("/body/fake"), + }), + ]), + }, + }); + }); + + test(`${testCase} - ${fBlock} - ${cOk}`, async () => { + const parameters = { blockHashOrBlockNumber: 0 }; + const res = await apiClient.getBlockV1(parameters as GetBlockV1Request); + expect(res.status).toEqual(200); + expect(res.data.block).toBeTruthy(); + }); + + test(`${testCase} - ${fBlock} - ${cWithoutParams}`, async () => { + const parameters = {}; // Empty parameters object + + await expect( + apiClient.getBlockV1(parameters as GetBlockV1Request), + ).rejects.toMatchObject({ + response: { + status: 400, + data: expect.arrayContaining([ + expect.objectContaining({ + path: expect.stringContaining("/body/blockHashOrBlockNumber"), + }), + ]), + }, + }); + }); + + test(`${testCase} - ${fBlock} - ${cInvalidParams}`, async () => { + const parameters = { + blockHashOrBlockNumber: 0, + fake: 4, + }; + + await expect( + apiClient.getBlockV1(parameters as GetBlockV1Request), + ).rejects.toMatchObject({ + response: { + status: 400, + data: expect.arrayContaining([ + expect.objectContaining({ + path: expect.stringContaining("/body/fake"), + }), + ]), + }, + }); + }); + + test(`${testCase} - ${fPastLogs} - ${cOk}`, async () => { + const parameters = { address: firstHighNetWorthAccount }; + const res = await apiClient.getPastLogsV1( + parameters as GetPastLogsV1Request, + ); + expect(res.status).toEqual(200); + expect(res.data.logs).toBeTruthy(); + }); + + test(`${testCase} - ${fPastLogs} - ${cWithoutParams}`, async () => { + try { + const parameters = {}; + const response = await apiClient.getPastLogsV1( + parameters as GetPastLogsV1Request, + ); + console.log( + "e.response.status should be 400 but actually is,", + response.status, + ); + } catch (e) { + expect(e.response.status).toEqual(400); + const fields = e.response.data.map((param: { readonly path: string }) => + param.path.replace("/body/", ""), + ); + expect(fields.includes("address")).toBeTrue(); + } + + //since status code is actually 200 refactored approach does not work + + // const parameters = {}; // Empty parameters object + + // await expect(apiClient.getPastLogsV1(parameters as GetPastLogsV1Request)) + // .rejects.toMatchObject({ + // response: { + // status: 400, + // data: expect.arrayContaining([ + // expect.objectContaining({ path: expect.stringContaining("/body/address") }) + // ]) + // } + // }); + }); + + test(`${testCase} - ${fPastLogs} - ${cInvalidParams}`, async () => { + const parameters = { + address: firstHighNetWorthAccount, + fake: 4, + }; + + await expect( + apiClient.getPastLogsV1(parameters as GetPastLogsV1Request), + ).rejects.toMatchObject({ + response: { + status: 400, + data: expect.arrayContaining([ + expect.objectContaining({ + path: expect.stringContaining("/body/fake"), + }), + ]), + }, + }); + }); + + test(`${testCase} - ${fRecord} - ${cOk}`, async () => { + const runTxRes = await apiClient.runTransactionV1({ + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.LedgerBlockAck, + timeoutMs: 5000, + }, + transactionConfig: { + from: testEthAccount1.address, + to: testEthAccount2.address, + value: 1, + gas: 10000000, + }, + web3SigningCredential: { + ethAccount: testEthAccount1.address, + secret: testEthAccount1.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + }); + + expect(runTxRes).toBeTruthy(); + expect(runTxRes.status).toBeNumber(); + expect(runTxRes.status).toEqual(200); + expect(runTxRes.data).toBeTruthy(); + expect((runTxRes.data as any).data.transactionReceipt).toBeTruthy(); + + const parameters = { + transactionHash: (runTxRes.data as any).data.transactionReceipt + .transactionHash, + }; + const res = await apiClient.getBesuRecordV1( + parameters as GetBesuRecordV1Request, + ); + expect(res.status).toEqual(200); + expect(res.data).toBeTruthy(); + }); + + test(`${testCase} - ${fRecord} - ${cWithoutParams}`, async () => { + try { + const parameters = {}; + const response = await apiClient.getBesuRecordV1( + parameters as GetBesuRecordV1Request, + ); + console.log( + "e.response.status should be 400 but actually is,", + response.status, + ); + } catch (e) { + expect(e.response.status).toEqual(400); + const fields = e.response.data.map((param: any) => + param.path.replace("/body/", ""), + ); + expect(fields.includes("transactionHash")).toBeTrue(); + } + + // since status code is actually 200 refactored approach does not work + + // const parameters = {}; // Empty parameters object + + // await expect(apiClient.getBesuRecordV1(parameters as GetBesuRecordV1Request)) + // .rejects.toMatchObject({ + // response: { + // status: 400, + // data: expect.arrayContaining([ + // expect.objectContaining({ path: expect.stringContaining("/body/transactionHash") }) + // ]) + // } + // }); + }); + + test(`${testCase} - ${fRecord} - ${cInvalidParams}`, async () => { + const parameters = { + transactionHash: "", + fake: 5, + }; + + await expect( + apiClient.getBesuRecordV1(parameters as GetBesuRecordV1Request), + ).rejects.toMatchObject({ + response: { + status: 400, + data: expect.arrayContaining([ + expect.objectContaining({ + path: expect.stringContaining("/body/fake"), + }), + ]), + }, + }); + }); + + afterAll(async () => { + await pruneDockerAllIfGithubAction({ logLevel }); + }); +}); diff --git a/packages/cactus-test-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-validator-besu/run-transaction-endpoint.test.ts b/packages/cactus-test-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-validator-besu/run-transaction-endpoint.test.ts new file mode 100644 index 00000000000..fc7527c4926 --- /dev/null +++ b/packages/cactus-test-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-validator-besu/run-transaction-endpoint.test.ts @@ -0,0 +1,129 @@ +import "jest-extended"; +import { v4 as uuidv4 } from "uuid"; +import { createServer } from "http"; +import { AddressInfo } from "net"; + +import { + ApiServer, + AuthorizationProtocol, + ConfigService, +} from "@hyperledger/cactus-cmd-api-server"; + +import { + BesuTestLedger, + pruneDockerAllIfGithubAction, +} from "@hyperledger/cactus-test-tooling"; + +import { + BesuApiClientOptions, + BesuApiClient, + IPluginLedgerConnectorBesuOptions, + PluginLedgerConnectorBesu, + Web3SigningCredentialType, + ReceiptType, + RunTransactionRequest, +} from "@hyperledger/cactus-plugin-ledger-connector-besu"; + +import { PluginRegistry } from "@hyperledger/cactus-core"; + +const testCase = "Test runTransactionV1 endpoint"; +const fRun = "runTransactionV1"; +const cLedgerUnavailable = "backing ledger unavailable"; +const logLevel = "TRACE"; +let apiServer: ApiServer; +let api: BesuApiClient; +let besuTestLedger: BesuTestLedger; + +describe(testCase, () => { + beforeAll(async () => { + const pruning = pruneDockerAllIfGithubAction({ logLevel }); + await expect(pruning).resolves.toBeTruthy(); + + // Initialize Besu test ledger + const containerImageVersion = "2021-08-24--feat-1244"; + const containerImageName = "ghcr.io/hyperledger/cactus-besu-21-1-6-all-in-one"; + besuTestLedger = new BesuTestLedger({ containerImageName, containerImageVersion }); + await besuTestLedger.start(); + + // Setup HTTP server and ApiServer + const httpServer = createServer(); + await new Promise((resolve, reject) => { + httpServer.once("error", reject); + httpServer.once("listening", resolve); + httpServer.listen(0, "127.0.0.1"); + }); + const addressInfo = httpServer.address() as AddressInfo; + + const rpcApiHttpHost = await besuTestLedger.getRpcApiHttpHost(); + const rpcApiWsHost = await besuTestLedger.getRpcApiWsHost(); + + const pluginRegistry = new PluginRegistry(); + const options: IPluginLedgerConnectorBesuOptions = { + instanceId: uuidv4(), + rpcApiHttpHost, + rpcApiWsHost, + pluginRegistry, + logLevel, + }; + const pluginValidatorBesu = new PluginLedgerConnectorBesu(options); + pluginRegistry.add(pluginValidatorBesu); + + const configService = new ConfigService(); + const apiServerOptions = await configService.newExampleConfig(); + apiServerOptions.authorizationProtocol = AuthorizationProtocol.NONE; + apiServerOptions.configFile = ""; + apiServerOptions.apiCorsDomainCsv = "*"; + apiServerOptions.apiPort = addressInfo.port; + apiServerOptions.cockpitPort = 0; + apiServerOptions.apiTlsEnabled = false; + + const config = await configService.newExampleConfigConvict(apiServerOptions); + apiServer = new ApiServer({ + httpServerApi: httpServer, + config: config.getProperties(), + pluginRegistry, + }); + await apiServer.start(); + + const configuration = new BesuApiClientOptions({ basePath: `http://${addressInfo.address}:${addressInfo.port}` }); + api = new BesuApiClient(configuration); + }); + + afterAll(async () => { + await apiServer.shutdown(); + }); + + test(`${testCase} - ${fRun} - ${cLedgerUnavailable}`, async () => { + const testEthAccount1 = await besuTestLedger.createEthTestAccount(); + const testEthAccount2 = await besuTestLedger.createEthTestAccount(); + + const parameters = { + web3SigningCredential: { + ethAccount: testEthAccount1.address, + secret: testEthAccount1.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + transactionConfig: { + from: testEthAccount1.address, + to: testEthAccount2.address, + value: 10e7, + gas: 1000000, + }, + consistencyStrategy: { + blockConfirmations: 0, + receiptType: ReceiptType.NodeTxPoolAck, + timeoutMs: 5000, + }, + }; + + await besuTestLedger.stop(); + await besuTestLedger.destroy(); + + try { + const res = await api.runTransactionV1(parameters as RunTransactionRequest); + expect(res).toBeTruthy(); + } catch (error) { + expect(error.response?.status).toEqual(503); + } + }); +}); diff --git a/yarn.lock b/yarn.lock index a85c6ec45fc..966ba5b903e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10300,9 +10300,11 @@ __metadata: web3: "npm:1.6.1" web3-core: "npm:1.6.1" web3-eth: "npm:1.6.1" + web3-eth-accounts: "patch:web3-eth-accounts@npm%3A1.6.1#~/.yarn/patches/web3-eth-accounts-npm-1.6.1-c95f31ca81.patch" web3-eth-contract: "npm:1.6.1" web3-utils: "npm:1.6.1" web3js-quorum: "npm:22.4.0" + websocket-event-codes: "npm:1.1.0" languageName: unknown linkType: soft @@ -28621,7 +28623,7 @@ __metadata: languageName: node linkType: hard -"ethereumjs-util@npm:^7.0.10, ethereumjs-util@npm:^7.1.0, ethereumjs-util@npm:^7.1.1, ethereumjs-util@npm:^7.1.4": +"ethereumjs-util@npm:^7.0.10, ethereumjs-util@npm:^7.1.0, ethereumjs-util@npm:^7.1.4": version: 7.1.4 resolution: "ethereumjs-util@npm:7.1.4" dependencies: @@ -28634,7 +28636,7 @@ __metadata: languageName: node linkType: hard -"ethereumjs-util@npm:^7.0.2, ethereumjs-util@npm:^7.1.2, ethereumjs-util@npm:^7.1.5": +"ethereumjs-util@npm:^7.0.2, ethereumjs-util@npm:^7.1.1, ethereumjs-util@npm:^7.1.2, ethereumjs-util@npm:^7.1.5": version: 7.1.5 resolution: "ethereumjs-util@npm:7.1.5" dependencies: @@ -53873,7 +53875,7 @@ __metadata: languageName: node linkType: hard -"web3-eth-accounts@npm:4.1.1, web3-eth-accounts@npm:^4.1.1": +"web3-eth-accounts@npm:4.1.1": version: 4.1.1 resolution: "web3-eth-accounts@npm:4.1.1" dependencies: @@ -53888,33 +53890,37 @@ __metadata: languageName: node linkType: hard -"web3-eth-accounts@npm:^4.0.3, web3-eth-accounts@npm:^4.0.5": - version: 4.0.5 - resolution: "web3-eth-accounts@npm:4.0.5" +"web3-eth-accounts@npm:^4.0.3, web3-eth-accounts@npm:^4.0.5, web3-eth-accounts@npm:^4.1.0, web3-eth-accounts@npm:^4.1.1": + version: 4.1.3 + resolution: "web3-eth-accounts@npm:4.1.3" dependencies: "@ethereumjs/rlp": "npm:^4.0.1" crc-32: "npm:^1.2.2" ethereum-cryptography: "npm:^2.0.0" - web3-errors: "npm:^1.1.1" - web3-types: "npm:^1.1.1" - web3-utils: "npm:^4.0.5" - web3-validator: "npm:^2.0.1" - checksum: 10/37e9e2d909544e3d9b8b9bf3168365cd068653b18880cb65835c9b2a3acefc885ecb18de52ead56f7a3663b33a6bb58d19e4e9fc21f2618cd9cb262a355a7dc1 + web3-errors: "npm:^1.2.0" + web3-types: "npm:^1.7.0" + web3-utils: "npm:^4.3.1" + web3-validator: "npm:^2.0.6" + checksum: 10/c5d4aa7b82517372666833e1752ffb2e768932833284de9388d710399dbdb609b129af76e4f658a0679ae18190b627e2b2331ad616ab3109c741dcd75a9e9524 languageName: node linkType: hard -"web3-eth-accounts@npm:^4.1.0": - version: 4.1.0 - resolution: "web3-eth-accounts@npm:4.1.0" +"web3-eth-accounts@patch:web3-eth-accounts@npm%3A1.6.1#~/.yarn/patches/web3-eth-accounts-npm-1.6.1-c95f31ca81.patch": + version: 1.6.1 + resolution: "web3-eth-accounts@patch:web3-eth-accounts@npm%3A1.6.1#~/.yarn/patches/web3-eth-accounts-npm-1.6.1-c95f31ca81.patch::version=1.6.1&hash=f41816" dependencies: - "@ethereumjs/rlp": "npm:^4.0.1" - crc-32: "npm:^1.2.2" - ethereum-cryptography: "npm:^2.0.0" - web3-errors: "npm:^1.1.3" - web3-types: "npm:^1.3.0" - web3-utils: "npm:^4.0.7" - web3-validator: "npm:^2.0.3" - checksum: 10/2048b3d1211593a44921a7cf200a2a407d82f832eae5751aa3b193012f4c084038814878c9423988636435f9b41967769a854ab643fb7d3cda8d898ad070be26 + "@ethereumjs/common": "npm:^2.5.0" + "@ethereumjs/tx": "npm:^3.3.2" + crypto-browserify: "npm:3.12.0" + eth-lib: "npm:0.2.8" + ethereumjs-util: "npm:^7.0.10" + scrypt-js: "npm:^3.0.1" + uuid: "npm:3.3.2" + web3-core: "npm:1.6.1" + web3-core-helpers: "npm:1.6.1" + web3-core-method: "npm:1.6.1" + web3-utils: "npm:1.6.1" + checksum: 10/5446cb368d4a6f72b85c7d0f7f1a894ae1090749d660ee0289d82b5e77f10ff885d36f2cc857ddb2c6e1a84b8081bddec3203ecc390bba43d39919e0e066502b languageName: node linkType: hard @@ -54942,6 +54948,13 @@ __metadata: languageName: node linkType: hard +"web3-types@npm:^1.7.0": + version: 1.7.0 + resolution: "web3-types@npm:1.7.0" + checksum: 10/fcd5d7a9a94579fcd01fa86dfa70e6afb269f66a7ce60e6786849e64ff6e4a107f1c25cb2784343a48952ac36d4bf3093a73b75de6ebcc971308e6b44abb211f + languageName: node + linkType: hard + "web3-utils@npm:1.10.0": version: 1.10.0 resolution: "web3-utils@npm:1.10.0" @@ -55060,6 +55073,19 @@ __metadata: languageName: node linkType: hard +"web3-utils@npm:^4.3.1": + version: 4.3.1 + resolution: "web3-utils@npm:4.3.1" + dependencies: + ethereum-cryptography: "npm:^2.0.0" + eventemitter3: "npm:^5.0.1" + web3-errors: "npm:^1.2.0" + web3-types: "npm:^1.7.0" + web3-validator: "npm:^2.0.6" + checksum: 10/88e39a6d43b756e965226b25ddc54855f26a7c13f6240b99fb521e1bae35a20a24f637f09fd0f4ef5d3f5a9e46b7f843bb5fd7ac2c9f99cfe18bb71817390f6e + languageName: node + linkType: hard + "web3-validator@npm:2.0.2": version: 2.0.2 resolution: "web3-validator@npm:2.0.2" @@ -55666,6 +55692,13 @@ __metadata: languageName: node linkType: hard +"websocket-event-codes@npm:1.1.0": + version: 1.1.0 + resolution: "websocket-event-codes@npm:1.1.0" + checksum: 10/97db632273f3de620856f984f7d17672c7c221ca80af3d2790681947f029bfc3128dd16c9ddc82e62251e0537eb0d4567ce4a5609776c09bf3996e1fc21a9332 + languageName: node + linkType: hard + "websocket-extensions@npm:>=0.1.1": version: 0.1.4 resolution: "websocket-extensions@npm:0.1.4"