diff --git a/packages/cactus-plugin-ledger-connector-quorum/Dockerfile b/packages/cactus-plugin-ledger-connector-quorum/Dockerfile index 6e57cabb003..65ef7de5631 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/Dockerfile +++ b/packages/cactus-plugin-ledger-connector-quorum/Dockerfile @@ -1,4 +1,4 @@ -FROM cactus-api-server:latest +FROM ghcr.io/hyperledger/cactus-cmd-api-server:v1.0.0 ARG NPM_PKG_VERSION=latest diff --git a/packages/cactus-plugin-ledger-connector-quorum/README.md b/packages/cactus-plugin-ledger-connector-quorum/README.md index bce6236eed3..bd3de5d01d0 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/README.md +++ b/packages/cactus-plugin-ledger-connector-quorum/README.md @@ -24,14 +24,7 @@ your local machine for development and testing purposes. In the root of the project to install the dependencies execute the command: ```sh -npm run comfigure -``` - -### Compiling - -In the projects root folder, run this command to compile the plugin and create the dist directory: -```sh -npm run tsc +npm run configure ``` ## Usage @@ -47,12 +40,16 @@ To use this import public-api and create new **PluginLedgerConnectorQuorum**. You can make calls through the connector to the plugin API: ```typescript -async invokeContract(req: InvokeContractV1Request):Promise; +async invokeContract(req: InvokeContractJsonObjectV1Request):Promise; +async transact(req: RunTransactionRequest): Promise; async transactSigned(rawTransaction: string): Promise; +async transactGethKeychain(txIn: RunTransactionRequest): Promise; async transactPrivateKey(req: RunTransactionRequest): Promise; async transactCactusKeychainRef(req: RunTransactionRequest):Promise; -async deployContract(req: DeployContractSolidityBytecodeV1Request):Promise; -async signTransaction(req: SignTransactionRequest):Promise>; +async deployContract(req: DeployContractSolidityBytecodeV1Request :Promise; +async deployContractJsonObject(req: DeployContractSolidityBytecodeJsonObjectV1Request): Promise +async invokeRawWeb3EthMethod(req: InvokeRawWeb3EthMethodV1Request): Promise; +async invokeRawWeb3EthContract(req: InvokeRawWeb3EthContractV1Request): Promise; ``` Call example to deploy a contract: @@ -78,6 +75,59 @@ enum Web3SigningCredentialType { ``` > Extensive documentation and examples in the [readthedocs](https://readthedocs.org/projects/hyperledger-cactus/) (WIP) +## QuorumApiClient + +All connector API endpoints are defined in [open-api specification](./src/main/json/openapi.json). You can use [QuorumApiClient](./src/main/typescript/api-client) to call remote quorum connector functions. It also contain additional utility functions to ease integration. + +### REST Functions +See [DefaultApi](./src/main/typescript/generated/openapi/typescript-axios/api.ts) for up-to-date listing of supported endpoints. +- deployContractSolBytecodeJsonObjectV1 +- deployContractSolBytecodeV1 +- getPrometheusMetricsV1 +- invokeContractV1 +- invokeContractV1NoKeychain +- invokeRawWeb3EthContractV1 +- invokeRawWeb3EthMethodV1 +- runTransactionV1 + +### Asynchronous Functions (socket.io) +- watchBlocksV1 + +### Send Request Methods +Both methods are deprecated, async version returns immediately while sync respond with Promise of a call results. +- `sendAsyncRequest` +- `sendSyncRequest` + +#### Supported Requests +- `web3Eth`: Calls `invokeRawWeb3EthMethodV1` +- `web3EthContract`: Calls `invokeRawWeb3EthContractV1` + +#### Arguments +- The same for both async and sync methods. +- Arguments interpretation depends on `method.type` (i.e. request type) +``` typescript +// Contract definition for web3EthContract request, ignored otherwise +contract: { + abi?: AbiItem[], + address?: string +}, + +// Request definition +method: { + type: "web3Eth" | "web3EthContract", + command: string // web3 method + function?: string; // contract function + params?: any[]; // contract parameters +} + +// web3 method arguments +args: { + { + args?: any[] | Record; + } +}, +``` + ## Running the tests To check that all has been installed correctly and that the pugin has no errors, there are two options to run the tests: @@ -108,6 +158,8 @@ docker run \ --rm \ --publish 3000:3000 \ --publish 4000:4000 \ + --env AUTHORIZATION_PROTOCOL='NONE' \ + --env AUTHORIZATION_CONFIG_JSON='{}' \ --env PLUGINS='[{"packageName": "@hyperledger/cactus-plugin-ledger-connector-quorum", "type": "org.hyperledger.cactus.plugin_import_type.LOCAL", "action": "org.hyperledger.cactus.plugin_import_action.INSTALL", "options": {"rpcApiHttpHost": "http://localhost:8545", "instanceId": "some-unique-quorum-connector-instance-id"}}]' \ cplcb ``` @@ -119,14 +171,16 @@ docker run \ --publish 3000:3000 \ --publish 4000:4000 \ cplcb \ - ./node_modules/.bin/cactusapi \ + ./node_modules/@hyperledger/cactus-cmd-api-server/dist/lib/main/typescript/cmd/cactus-api.js \ + --authorization-protocol='NONE' \ + --authorization-config-json='{}' \ --plugins='[{"packageName": "@hyperledger/cactus-plugin-ledger-connector-quorum", "type": "org.hyperledger.cactus.plugin_import_type.LOCAL", "action": "org.hyperledger.cactus.plugin_import_action.INSTALL", "options": {"rpcApiHttpHost": "http://localhost:8545", "instanceId": "some-unique-quorum-connector-instance-id"}}]' ``` Launch container with **configuration file** mounted from host machine: ```sh -echo '[{"packageName": "@hyperledger/cactus-plugin-ledger-connector-quorum", "type": "org.hyperledger.cactus.plugin_import_type.LOCAL", "action": "org.hyperledger.cactus.plugin_import_action.INSTALL", "options": {"rpcApiHttpHost": "http://localhost:8545", "instanceId": "some-unique-quorum-connector-instance-id"}}]' > cactus.json +echo '{"authorizationProtocol":"NONE","authorizationConfigJson":{},"plugins":[{"packageName":"@hyperledger/cactus-plugin-ledger-connector-quorum","type":"org.hyperledger.cactus.plugin_import_type.LOCAL","action":"org.hyperledger.cactus.plugin_import_action.INSTALL","options":{"rpcApiHttpHost":"http://localhost:8545","instanceId":"some-unique-quorum-connector-instance-id"}}]}' > cactus.json docker run \ --rm \ @@ -134,7 +188,7 @@ docker run \ --publish 4000:4000 \ --mount type=bind,source="$(pwd)"/cactus.json,target=/cactus.json \ cplcb \ - ./node_modules/.bin/cactusapi \ + ./node_modules/@hyperledger/cactus-cmd-api-server/dist/lib/main/typescript/cmd/cactus-api.js \ --config-file=/cactus.json ``` @@ -263,5 +317,5 @@ Please review [CONTIRBUTING.md](../../CONTRIBUTING.md) to get started. This distribution is published under the Apache License Version 2.0 found in the [LICENSE](../../LICENSE) file. -## Acknowledgments +## Acknowledgments ``` \ No newline at end of file diff --git a/packages/cactus-plugin-ledger-connector-quorum/package.json b/packages/cactus-plugin-ledger-connector-quorum/package.json index 048bf10e079..ac1bb5ca8d4 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/package.json +++ b/packages/cactus-plugin-ledger-connector-quorum/package.json @@ -59,14 +59,18 @@ "axios": "0.21.4", "express": "4.17.1", "prom-client": "13.2.0", + "rxjs": "7.3.0", + "sanitize-html": "2.7.0", "typescript-optional": "2.0.1", "web3": "1.5.2", - "web3-eth-contract": "1.5.2" + "web3-eth-contract": "1.5.2", + "run-time-error": "1.4.0" }, "devDependencies": { "@hyperledger/cactus-plugin-keychain-memory": "1.0.0", "@hyperledger/cactus-test-tooling": "1.0.0", "@types/express": "4.17.13", + "@types/sanitize-html": "2.6.2", "web3-eth": "1.5.2" }, "engines": { diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/json/openapi.json b/packages/cactus-plugin-ledger-connector-quorum/src/main/json/openapi.json index 1b2fcd29e3e..f237394336c 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/main/json/openapi.json +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/json/openapi.json @@ -151,6 +151,15 @@ "CALL" ] }, + "EthContractInvocationWeb3Method": { + "type": "string", + "enum": [ + "send", + "call", + "encodeABI", + "estimateGas" + ] + }, "SolidityContractJsonArtifact": { "type": "object", "required": [ @@ -507,7 +516,7 @@ } } }, - "DeployContractSolidityBytecodeJsonObjectV1Request": { + "DeployContractSolidityBytecodeJsonObjectV1Request": { "type": "object", "required": [ "web3SigningCredential", @@ -534,7 +543,7 @@ "default": 60000, "nullable": false }, - "contractJSON": { + "contractJSON": { "$ref": "#/components/schemas/ContractJSON", "description": "For use when not using keychain, pass the contract in as this variable", "nullable": false @@ -713,7 +722,7 @@ "default": 60000, "nullable": false }, - "contractJSON": { + "contractJSON": { "$ref": "#/components/schemas/ContractJSON", "description": "For use when not using keychain, pass the contract in as this variable", "nullable": false @@ -736,9 +745,385 @@ } } }, + "InvokeRawWeb3EthMethodV1Request": { + "type": "object", + "required": ["methodName"], + "additionalProperties": false, + "properties": { + "methodName": { + "description": "The name of the web3.eth method to invoke", + "type": "string", + "nullable": false, + "minLength": 1, + "maxLength": 2048 + }, + "params": { + "description": "The list of arguments to pass to web3.eth method specified in methodName", + "type": "array", + "default": [], + "items": {} + } + } + }, + "InvokeRawWeb3EthMethodV1Response": { + "type": "object", + "required": [ + "status" + ], + "additionalProperties": false, + "properties": { + "status": { + "type": "number", + "nullable": false, + "description": "Status code of the operation" + }, + "data": { + "description": "Output of requested web3.eth method" + }, + "errorDetail": { + "type": "string", + "nullable": false, + "description": "Error details" + } + } + }, + "InvokeRawWeb3EthContractV1Request": { + "type": "object", + "required": [ + "abi", + "address", + "invocationType", + "contractMethod" + ], + "additionalProperties": false, + "properties": { + "abi": { + "description": "The application binary interface of the solidity contract", + "type": "array", + "items": {} + }, + "address": { + "description": "Deployed solidity contract address", + "type": "string" + }, + "invocationType": { + "description": "Contract invocation method to be performed (send, call, etc...)", + "$ref": "#/components/schemas/EthContractInvocationWeb3Method" + }, + "invocationParams": { + "description": "The list of arguments for contract invocation method (send, call, etc...)", + "type": "object", + "default": {} + }, + "contractMethod": { + "description": "Method of deployed solidity contract to execute", + "type": "string" + }, + "contractMethodArgs": { + "description": "The list of arguments for deployed solidity contract method", + "type": "array", + "default": [], + "items": {} + } + } + }, + "InvokeRawWeb3EthContractV1Response": { + "type": "object", + "required": [ + "status" + ], + "additionalProperties": false, + "properties": { + "status": { + "description": "Status code of the operation", + "type": "number" + }, + "data": { + "description": "Output of contract invocation method" + }, + "errorDetail": { + "description": "Error details", + "type": "string" + } + } + }, "PrometheusExporterMetricsResponse": { "type": "string", "nullable": false + }, + "WatchBlocksV1": { + "type": "string", + "enum": [ + "org.hyperledger.cactus.api.async.quorum.WatchBlocksV1.Subscribe", + "org.hyperledger.cactus.api.async.quorum.WatchBlocksV1.Next", + "org.hyperledger.cactus.api.async.quorum.WatchBlocksV1.Unsubscribe", + "org.hyperledger.cactus.api.async.quorum.WatchBlocksV1.Error", + "org.hyperledger.cactus.api.async.quorum.WatchBlocksV1.Complete" + ], + "x-enum-varnames": [ + "Subscribe", + "Next", + "Unsubscribe", + "Error", + "Complete" + ] + }, + "WatchBlocksV1Options": { + "type": "object", + "properties": { + "getBlockData": { + "type": "boolean" + } + } + }, + "Web3BlockHeader": { + "type": "object", + "required": [ + "number", + "hash", + "parentHash", + "nonce", + "sha3Uncles", + "logsBloom", + "transactionRoot", + "stateRoot", + "receiptRoot", + "miner", + "extraData", + "gasLimit", + "gasUsed", + "timestamp" + ], + "properties": { + "number": { + "type": "number" + }, + "hash": { + "type": "string" + }, + "parentHash": { + "type": "string" + }, + "nonce": { + "type": "string" + }, + "sha3Uncles": { + "type": "string" + }, + "logsBloom": { + "type": "string" + }, + "transactionsRoot": { + "type": "string" + }, + "stateRoot": { + "type": "string" + }, + "receiptsRoot": { + "type": "string" + }, + "difficulty": { + "type": "string" + }, + "mixHash": { + "type": "string" + }, + "miner": { + "type": "string" + }, + "extraData": { + "type": "string" + }, + "gasLimit": { + "type": "integer" + }, + "gasUsed": { + "type": "integer" + }, + "timestamp": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + } + } + }, + "Web3Transaction": { + "type": "object", + "required": [ + "hash", + "nonce", + "blockHash", + "blockNumber", + "transactionIndex", + "from", + "to", + "value", + "gasPrice", + "gas", + "input" + ], + "properties": { + "hash": { + "type": "string" + }, + "nonce": { + "type": "number" + }, + "blockHash": { + "type": "string", + "nullable": true + }, + "blockNumber": { + "type": "number", + "nullable": true + }, + "transactionIndex": { + "type": "number", + "nullable": true + }, + "from": { + "type": "string" + }, + "to": { + "type": "string", + "nullable": true + }, + "value": { + "type": "string" + }, + "gasPrice": { + "type": "string" + }, + "gas": { + "type": "number" + }, + "input": { + "type": "string" + }, + "v": { + "type": "string" + }, + "r": { + "type": "string" + }, + "s": { + "type": "string" + } + } + }, + "WatchBlocksV1BlockData": { + "type": "object", + "required": [ + "number", + "hash", + "parentHash", + "nonce", + "sha3Uncles", + "logsBloom", + "transactionRoot", + "stateRoot", + "receiptRoot", + "miner", + "extraData", + "gasLimit", + "gasUsed", + "timestamp", + "size", + "totalDifficulty", + "uncles", + "transactions" + ], + "properties": { + "number": { + "type": "number" + }, + "hash": { + "type": "string" + }, + "parentHash": { + "type": "string" + }, + "nonce": { + "type": "string" + }, + "sha3Uncles": { + "type": "string" + }, + "logsBloom": { + "type": "string" + }, + "transactionsRoot": { + "type": "string" + }, + "stateRoot": { + "type": "string" + }, + "receiptsRoot": { + "type": "string" + }, + "difficulty": { + "type": "string" + }, + "mixHash": { + "type": "string" + }, + "miner": { + "type": "string" + }, + "extraData": { + "type": "string" + }, + "gasLimit": { + "type": "integer" + }, + "gasUsed": { + "type": "integer" + }, + "timestamp": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + }, + "size": { + "type": "number" + }, + "totalDifficulty": { + "type": "string" + }, + "uncles": { + "type": "array", + "items": { + "type": "string" + } + }, + "transactions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Web3Transaction" + } + } + } + }, + "WatchBlocksV1Progress": { + "type": "object", + "properties": { + "blockHeader": { + "$ref": "#/components/schemas/Web3BlockHeader" + }, + "blockData": { + "$ref": "#/components/schemas/WatchBlocksV1BlockData" + } + } } } }, @@ -937,6 +1322,74 @@ } } } + }, + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-quorum/invoke-raw-web3eth-method": { + "post": { + "x-hyperledger-cactus": { + "http": { + "verbLowerCase": "post", + "path": "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-quorum/invoke-raw-web3eth-method" + } + }, + "operationId": "invokeWeb3EthMethodV1", + "summary": "Invoke any method from web3.eth (low-level)", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvokeRawWeb3EthMethodV1Request" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/InvokeRawWeb3EthMethodV1Response" + } + } + } + } + } + } + }, + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-quorum/invoke-raw-web3eth-contract": { + "post": { + "x-hyperledger-cactus": { + "http": { + "verbLowerCase": "post", + "path": "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-quorum/invoke-raw-web3eth-contract" + } + }, + "operationId": "invokeRawWeb3EthContractV1", + "summary": "Low-level endpoint to invoke a method on deployed contract.", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvokeRawWeb3EthContractV1Request" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/InvokeRawWeb3EthContractV1Response" + } + } + } + } + } + } } } } \ No newline at end of file diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/api-client/quorum-api-client.ts b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/api-client/quorum-api-client.ts new file mode 100644 index 00000000000..ebd3ba80eaa --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/api-client/quorum-api-client.ts @@ -0,0 +1,255 @@ +import { Observable, ReplaySubject } from "rxjs"; +import { finalize } from "rxjs/operators"; +import { io } from "socket.io-client"; +import { Logger, Checks } from "@hyperledger/cactus-common"; +import { LogLevelDesc, LoggerProvider } from "@hyperledger/cactus-common"; +import { Constants, ISocketApiClient } from "@hyperledger/cactus-core-api"; +import { + DefaultApi, + EthContractInvocationWeb3Method, + InvokeRawWeb3EthContractV1Request, + WatchBlocksV1, + WatchBlocksV1Options, + WatchBlocksV1Progress, +} from "../generated/openapi/typescript-axios"; +import { Configuration } from "../generated/openapi/typescript-axios/configuration"; +import { AbiItem } from "web3-utils"; + +export class QuorumApiClientOptions extends Configuration { + readonly logLevel?: LogLevelDesc; + readonly wsApiHost?: string; + readonly wsApiPath?: string; +} + +// Command 'web3Eth' input method type +export type QuorumRequestInputWeb3EthMethod = { + type: "web3Eth"; + command: string; +}; + +// Command 'web3EthContract' input method type +export type QuorumRequestInputWeb3EthContractMethod = { + type: "web3EthContract"; + command: EthContractInvocationWeb3Method; + function: string; + params?: any[]; +}; + +// Common input types for sending requests +export type QuorumRequestInputContract = { + abi?: AbiItem[]; + address?: string; +}; +export type QuorumRequestInputMethod = + | QuorumRequestInputWeb3EthMethod + | QuorumRequestInputWeb3EthContractMethod; +export type QuorumRequestInputArgs = { + args?: any[] | Record; +}; + +export class QuorumApiClient + extends DefaultApi + implements ISocketApiClient { + public static readonly CLASS_NAME = "QuorumApiClient"; + + private readonly log: Logger; + private readonly wsApiHost: string; + private readonly wsApiPath: string; + + public get className(): string { + return QuorumApiClient.CLASS_NAME; + } + + constructor(public readonly options: QuorumApiClientOptions) { + super(options); + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + + const level = this.options.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + + this.wsApiHost = options.wsApiHost || options.basePath || location.host; + this.wsApiPath = options.wsApiPath || Constants.SocketIoConnectionPathV1; + this.log.debug(`Created ${this.className} OK.`); + this.log.debug(`wsApiHost=${this.wsApiHost}`); + this.log.debug(`wsApiPath=${this.wsApiPath}`); + this.log.debug(`basePath=${this.options.basePath}`); + } + + public watchBlocksV1( + options?: WatchBlocksV1Options, + ): Observable { + const socket = io(this.wsApiHost, { path: this.wsApiPath }); + const subject = new ReplaySubject(0); + + socket.on(WatchBlocksV1.Next, (data: WatchBlocksV1Progress) => { + this.log.debug("Received WatchBlocksV1.Next"); + subject.next(data); + }); + + socket.on(WatchBlocksV1.Error, (ex: string) => { + this.log.warn("Received WatchBlocksV1.Error:", ex); + subject.error(ex); + }); + + socket.on(WatchBlocksV1.Complete, () => { + this.log.debug("Received WatchBlocksV1.Complete"); + subject.complete(); + }); + + socket.on("connect", () => { + this.log.info("Connected OK, sending WatchBlocksV1.Subscribe request..."); + socket.emit(WatchBlocksV1.Subscribe, options); + }); + + socket.connect(); + + return subject.pipe( + finalize(() => { + this.log.info("FINALIZE - unsubscribing from the stream..."); + socket.emit(WatchBlocksV1.Unsubscribe); + socket.disconnect(); + }), + ); + } + + /** + * Immediately sends request to the validator, doesn't report any error or responses. + * @param contract - contract to execute on the ledger. + * @param method - function / method to be executed by validator. + * @param args - arguments. + * @note Internally, it's just a wrapper around sendSyncRequest, but handles the promise resolution seamlessly. + * @deprecated Use QuorumApiClient REST calls directly. + */ + public sendAsyncRequest( + contract: QuorumRequestInputContract, + method: QuorumRequestInputMethod, + args: QuorumRequestInputArgs, + ): void { + const callName = `${method.type} - ${method.command}`; + this.log.debug("sendAsyncRequest()", callName); + + this.sendSyncRequest(contract, method, args) + .then((value) => { + this.log.info(`sendAsyncRequest call resolved (${callName})`); + this.log.debug("sendAsyncRequest results:", JSON.stringify(value)); + }) + .catch((err) => { + this.log.warn(`sendAsyncRequest failed (${callName}). Error:`, err); + }); + } + + private sendWeb3EthRequest( + method: QuorumRequestInputWeb3EthMethod, + args?: any[], + ): Promise { + return new Promise((resolve, reject) => { + // Check parameters + Checks.nonBlankString(method.command, "Method command must not be empty"); + if (args && !Array.isArray(args)) { + throw new Error("web3Eth arguments (args) must be an array"); + } + + // Prepare input + const invokeArgs = { + methodName: method.command, + params: args, + }; + + // Call the endpoint + this.invokeWeb3EthMethodV1(invokeArgs) + .then((value) => { + this.log.debug("sendWeb3EthRequest() OK"); + resolve(value.data); + }) + .catch((err) => { + this.log.debug("sendWeb3EthRequest() Error:", err); + reject(err); + }); + }); + } + + private sendWeb3EthContractRequest( + contract: QuorumRequestInputContract, + method: QuorumRequestInputWeb3EthContractMethod, + args?: Record, + ): Promise { + return new Promise((resolve, reject) => { + // Check parameters + Checks.truthy(contract.abi, "Contract ABI must be defined"); + Checks.truthy(contract.address, "Contract address must be set"); + if ( + !Object.values(EthContractInvocationWeb3Method).includes(method.command) + ) { + throw new Error( + `Unknown invocationType (${method.command}), must be specified in EthContractInvocationWeb3Method`, + ); + } + Checks.nonBlankString( + method.function, + "contractMethod (method.function) must not be empty", + ); + if (method.params && !Array.isArray(method.params)) { + throw new Error( + "Contract method arguments (method.params) must be an array", + ); + } + + // Prepare input + const invokeArgs: InvokeRawWeb3EthContractV1Request = { + abi: contract.abi as AbiItem[], + address: contract.address as string, + invocationType: method.command, + invocationParams: args, + contractMethod: method.function, + contractMethodArgs: method.params, + }; + + // Call the endpoint + this.invokeRawWeb3EthContractV1(invokeArgs) + .then((value) => { + resolve(value.data); + }) + .catch((err) => { + reject(err); + }); + }); + } + + /** + * Sends request to be executed on the ledger, watches and reports any error and the response from a ledger. + * @param contract - contract to execute on the ledger. + * @param method - function / method specification to be executed by validator. + * @param args - arguments. + * @returns Promise that will resolve with response from the ledger, or reject when error occurred. + * @deprecated Use QuorumApiClient REST calls directly. + */ + public sendSyncRequest( + contract: QuorumRequestInputContract, + method: QuorumRequestInputMethod, + args: QuorumRequestInputArgs, + ): Promise { + this.log.debug("sendSyncRequest()"); + + switch (method.type) { + case "web3Eth": { + this.log.info("Send 'web3Eth' request command"); + return this.sendWeb3EthRequest(method, args.args as any); + } + case "web3EthContract": { + this.log.info("Send 'web3EthContract' request command"); + return this.sendWeb3EthContractRequest( + contract, + method, + args.args as any, + ); + } + default: + const value: never = method; + return Promise.reject( + `Not support request method on Quorum: ${JSON.stringify(value)}`, + ); + } + } +} diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/generated/openapi/typescript-axios/api.ts index 08ce3665037..9727493827c 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -230,6 +230,19 @@ export enum EthContractInvocationType { Call = 'CALL' } +/** + * + * @export + * @enum {string} + */ + +export enum EthContractInvocationWeb3Method { + Send = 'send', + Call = 'call', + EncodeAbi = 'encodeABI', + EstimateGas = 'estimateGas' +} + /** * * @export @@ -401,6 +414,118 @@ export interface InvokeContractV1Response { */ success: boolean; } +/** + * + * @export + * @interface InvokeRawWeb3EthContractV1Request + */ +export interface InvokeRawWeb3EthContractV1Request { + /** + * The application binary interface of the solidity contract + * @type {Array} + * @memberof InvokeRawWeb3EthContractV1Request + */ + abi: Array; + /** + * Deployed solidity contract address + * @type {string} + * @memberof InvokeRawWeb3EthContractV1Request + */ + address: string; + /** + * + * @type {EthContractInvocationWeb3Method} + * @memberof InvokeRawWeb3EthContractV1Request + */ + invocationType: EthContractInvocationWeb3Method; + /** + * The list of arguments for contract invocation method (send, call, etc...) + * @type {object} + * @memberof InvokeRawWeb3EthContractV1Request + */ + invocationParams?: object; + /** + * Method of deployed solidity contract to execute + * @type {string} + * @memberof InvokeRawWeb3EthContractV1Request + */ + contractMethod: string; + /** + * The list of arguments for deployed solidity contract method + * @type {Array} + * @memberof InvokeRawWeb3EthContractV1Request + */ + contractMethodArgs?: Array; +} +/** + * + * @export + * @interface InvokeRawWeb3EthContractV1Response + */ +export interface InvokeRawWeb3EthContractV1Response { + /** + * Status code of the operation + * @type {number} + * @memberof InvokeRawWeb3EthContractV1Response + */ + status: number; + /** + * Output of contract invocation method + * @type {any} + * @memberof InvokeRawWeb3EthContractV1Response + */ + data?: any | null; + /** + * Error details + * @type {string} + * @memberof InvokeRawWeb3EthContractV1Response + */ + errorDetail?: string; +} +/** + * + * @export + * @interface InvokeRawWeb3EthMethodV1Request + */ +export interface InvokeRawWeb3EthMethodV1Request { + /** + * The name of the web3.eth method to invoke + * @type {string} + * @memberof InvokeRawWeb3EthMethodV1Request + */ + methodName: string; + /** + * The list of arguments to pass to web3.eth method specified in methodName + * @type {Array} + * @memberof InvokeRawWeb3EthMethodV1Request + */ + params?: Array; +} +/** + * + * @export + * @interface InvokeRawWeb3EthMethodV1Response + */ +export interface InvokeRawWeb3EthMethodV1Response { + /** + * Status code of the operation + * @type {number} + * @memberof InvokeRawWeb3EthMethodV1Response + */ + status: number; + /** + * Output of requested web3.eth method + * @type {any} + * @memberof InvokeRawWeb3EthMethodV1Response + */ + data?: any | null; + /** + * Error details + * @type {string} + * @memberof InvokeRawWeb3EthMethodV1Response + */ + errorDetail?: string; +} /** * * @export @@ -563,6 +688,282 @@ export interface SolidityContractJsonArtifact { */ gasEstimates?: object; } +/** + * + * @export + * @enum {string} + */ + +export enum WatchBlocksV1 { + Subscribe = 'org.hyperledger.cactus.api.async.quorum.WatchBlocksV1.Subscribe', + Next = 'org.hyperledger.cactus.api.async.quorum.WatchBlocksV1.Next', + Unsubscribe = 'org.hyperledger.cactus.api.async.quorum.WatchBlocksV1.Unsubscribe', + Error = 'org.hyperledger.cactus.api.async.quorum.WatchBlocksV1.Error', + Complete = 'org.hyperledger.cactus.api.async.quorum.WatchBlocksV1.Complete' +} + +/** + * + * @export + * @interface WatchBlocksV1BlockData + */ +export interface WatchBlocksV1BlockData { + /** + * + * @type {number} + * @memberof WatchBlocksV1BlockData + */ + number: number; + /** + * + * @type {string} + * @memberof WatchBlocksV1BlockData + */ + hash: string; + /** + * + * @type {string} + * @memberof WatchBlocksV1BlockData + */ + parentHash: string; + /** + * + * @type {string} + * @memberof WatchBlocksV1BlockData + */ + nonce: string; + /** + * + * @type {string} + * @memberof WatchBlocksV1BlockData + */ + sha3Uncles: string; + /** + * + * @type {string} + * @memberof WatchBlocksV1BlockData + */ + logsBloom: string; + /** + * + * @type {string} + * @memberof WatchBlocksV1BlockData + */ + transactionsRoot?: string; + /** + * + * @type {string} + * @memberof WatchBlocksV1BlockData + */ + stateRoot: string; + /** + * + * @type {string} + * @memberof WatchBlocksV1BlockData + */ + receiptsRoot?: string; + /** + * + * @type {string} + * @memberof WatchBlocksV1BlockData + */ + difficulty?: string; + /** + * + * @type {string} + * @memberof WatchBlocksV1BlockData + */ + mixHash?: string; + /** + * + * @type {string} + * @memberof WatchBlocksV1BlockData + */ + miner: string; + /** + * + * @type {string} + * @memberof WatchBlocksV1BlockData + */ + extraData: string; + /** + * + * @type {number} + * @memberof WatchBlocksV1BlockData + */ + gasLimit: number; + /** + * + * @type {number} + * @memberof WatchBlocksV1BlockData + */ + gasUsed: number; + /** + * + * @type {string | number} + * @memberof WatchBlocksV1BlockData + */ + timestamp: string | number; + /** + * + * @type {number} + * @memberof WatchBlocksV1BlockData + */ + size: number; + /** + * + * @type {string} + * @memberof WatchBlocksV1BlockData + */ + totalDifficulty: string; + /** + * + * @type {Array} + * @memberof WatchBlocksV1BlockData + */ + uncles: Array; + /** + * + * @type {Array} + * @memberof WatchBlocksV1BlockData + */ + transactions: Array; +} +/** + * + * @export + * @interface WatchBlocksV1Options + */ +export interface WatchBlocksV1Options { + /** + * + * @type {boolean} + * @memberof WatchBlocksV1Options + */ + getBlockData?: boolean; +} +/** + * + * @export + * @interface WatchBlocksV1Progress + */ +export interface WatchBlocksV1Progress { + /** + * + * @type {Web3BlockHeader} + * @memberof WatchBlocksV1Progress + */ + blockHeader?: Web3BlockHeader; + /** + * + * @type {WatchBlocksV1BlockData} + * @memberof WatchBlocksV1Progress + */ + blockData?: WatchBlocksV1BlockData; +} +/** + * + * @export + * @interface Web3BlockHeader + */ +export interface Web3BlockHeader { + /** + * + * @type {number} + * @memberof Web3BlockHeader + */ + number: number; + /** + * + * @type {string} + * @memberof Web3BlockHeader + */ + hash: string; + /** + * + * @type {string} + * @memberof Web3BlockHeader + */ + parentHash: string; + /** + * + * @type {string} + * @memberof Web3BlockHeader + */ + nonce: string; + /** + * + * @type {string} + * @memberof Web3BlockHeader + */ + sha3Uncles: string; + /** + * + * @type {string} + * @memberof Web3BlockHeader + */ + logsBloom: string; + /** + * + * @type {string} + * @memberof Web3BlockHeader + */ + transactionsRoot?: string; + /** + * + * @type {string} + * @memberof Web3BlockHeader + */ + stateRoot: string; + /** + * + * @type {string} + * @memberof Web3BlockHeader + */ + receiptsRoot?: string; + /** + * + * @type {string} + * @memberof Web3BlockHeader + */ + difficulty?: string; + /** + * + * @type {string} + * @memberof Web3BlockHeader + */ + mixHash?: string; + /** + * + * @type {string} + * @memberof Web3BlockHeader + */ + miner: string; + /** + * + * @type {string} + * @memberof Web3BlockHeader + */ + extraData: string; + /** + * + * @type {number} + * @memberof Web3BlockHeader + */ + gasLimit: number; + /** + * + * @type {number} + * @memberof Web3BlockHeader + */ + gasUsed: number; + /** + * + * @type {string | number} + * @memberof Web3BlockHeader + */ + timestamp: string | number; +} /** * @type Web3SigningCredential * @export @@ -676,6 +1077,97 @@ export enum Web3SigningCredentialType { None = 'NONE' } +/** + * + * @export + * @interface Web3Transaction + */ +export interface Web3Transaction { + /** + * + * @type {string} + * @memberof Web3Transaction + */ + hash: string; + /** + * + * @type {number} + * @memberof Web3Transaction + */ + nonce: number; + /** + * + * @type {string} + * @memberof Web3Transaction + */ + blockHash: string | null; + /** + * + * @type {number} + * @memberof Web3Transaction + */ + blockNumber: number | null; + /** + * + * @type {number} + * @memberof Web3Transaction + */ + transactionIndex: number | null; + /** + * + * @type {string} + * @memberof Web3Transaction + */ + from: string; + /** + * + * @type {string} + * @memberof Web3Transaction + */ + to: string | null; + /** + * + * @type {string} + * @memberof Web3Transaction + */ + value: string; + /** + * + * @type {string} + * @memberof Web3Transaction + */ + gasPrice: string; + /** + * + * @type {number} + * @memberof Web3Transaction + */ + gas: number; + /** + * + * @type {string} + * @memberof Web3Transaction + */ + input: string; + /** + * + * @type {string} + * @memberof Web3Transaction + */ + v?: string; + /** + * + * @type {string} + * @memberof Web3Transaction + */ + r?: string; + /** + * + * @type {string} + * @memberof Web3Transaction + */ + s?: string; +} /** * * @export @@ -912,6 +1404,74 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati options: localVarRequestOptions, }; }, + /** + * + * @summary Low-level endpoint to invoke a method on deployed contract. + * @param {InvokeRawWeb3EthContractV1Request} [invokeRawWeb3EthContractV1Request] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + invokeRawWeb3EthContractV1: async (invokeRawWeb3EthContractV1Request?: InvokeRawWeb3EthContractV1Request, options: any = {}): Promise => { + const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-quorum/invoke-raw-web3eth-contract`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(invokeRawWeb3EthContractV1Request, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Invoke any method from web3.eth (low-level) + * @param {InvokeRawWeb3EthMethodV1Request} [invokeRawWeb3EthMethodV1Request] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + invokeWeb3EthMethodV1: async (invokeRawWeb3EthMethodV1Request?: InvokeRawWeb3EthMethodV1Request, options: any = {}): Promise => { + const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-quorum/invoke-raw-web3eth-method`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(invokeRawWeb3EthMethodV1Request, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @summary Executes a transaction on a quorum ledger @@ -1010,6 +1570,28 @@ export const DefaultApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.invokeContractV1NoKeychain(invokeContractJsonObjectV1Request, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @summary Low-level endpoint to invoke a method on deployed contract. + * @param {InvokeRawWeb3EthContractV1Request} [invokeRawWeb3EthContractV1Request] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async invokeRawWeb3EthContractV1(invokeRawWeb3EthContractV1Request?: InvokeRawWeb3EthContractV1Request, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.invokeRawWeb3EthContractV1(invokeRawWeb3EthContractV1Request, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @summary Invoke any method from web3.eth (low-level) + * @param {InvokeRawWeb3EthMethodV1Request} [invokeRawWeb3EthMethodV1Request] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async invokeWeb3EthMethodV1(invokeRawWeb3EthMethodV1Request?: InvokeRawWeb3EthMethodV1Request, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.invokeWeb3EthMethodV1(invokeRawWeb3EthMethodV1Request, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @summary Executes a transaction on a quorum ledger @@ -1080,6 +1662,26 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa invokeContractV1NoKeychain(invokeContractJsonObjectV1Request?: InvokeContractJsonObjectV1Request, options?: any): AxiosPromise { return localVarFp.invokeContractV1NoKeychain(invokeContractJsonObjectV1Request, options).then((request) => request(axios, basePath)); }, + /** + * + * @summary Low-level endpoint to invoke a method on deployed contract. + * @param {InvokeRawWeb3EthContractV1Request} [invokeRawWeb3EthContractV1Request] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + invokeRawWeb3EthContractV1(invokeRawWeb3EthContractV1Request?: InvokeRawWeb3EthContractV1Request, options?: any): AxiosPromise { + return localVarFp.invokeRawWeb3EthContractV1(invokeRawWeb3EthContractV1Request, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Invoke any method from web3.eth (low-level) + * @param {InvokeRawWeb3EthMethodV1Request} [invokeRawWeb3EthMethodV1Request] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + invokeWeb3EthMethodV1(invokeRawWeb3EthMethodV1Request?: InvokeRawWeb3EthMethodV1Request, options?: any): AxiosPromise { + return localVarFp.invokeWeb3EthMethodV1(invokeRawWeb3EthMethodV1Request, options).then((request) => request(axios, basePath)); + }, /** * * @summary Executes a transaction on a quorum ledger @@ -1159,6 +1761,30 @@ export class DefaultApi extends BaseAPI { return DefaultApiFp(this.configuration).invokeContractV1NoKeychain(invokeContractJsonObjectV1Request, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @summary Low-level endpoint to invoke a method on deployed contract. + * @param {InvokeRawWeb3EthContractV1Request} [invokeRawWeb3EthContractV1Request] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public invokeRawWeb3EthContractV1(invokeRawWeb3EthContractV1Request?: InvokeRawWeb3EthContractV1Request, options?: any) { + return DefaultApiFp(this.configuration).invokeRawWeb3EthContractV1(invokeRawWeb3EthContractV1Request, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Invoke any method from web3.eth (low-level) + * @param {InvokeRawWeb3EthMethodV1Request} [invokeRawWeb3EthMethodV1Request] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof DefaultApi + */ + public invokeWeb3EthMethodV1(invokeRawWeb3EthMethodV1Request?: InvokeRawWeb3EthMethodV1Request, options?: any) { + return DefaultApiFp(this.configuration).invokeWeb3EthMethodV1(invokeRawWeb3EthMethodV1Request, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @summary Executes a transaction on a quorum ledger diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/plugin-ledger-connector-quorum.ts b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/plugin-ledger-connector-quorum.ts index 72e30bacfbb..650c0c78b0d 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/plugin-ledger-connector-quorum.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/plugin-ledger-connector-quorum.ts @@ -1,12 +1,14 @@ import { Server } from "http"; import { Server as SecureServer } from "https"; +import type { + Server as SocketIoServer, + Socket as SocketIoSocket, +} from "socket.io"; import { Express } from "express"; import Web3 from "web3"; -// The strange way of obtaining the contract class here is like this because -// web3-eth internally sub-classes the Contract class at runtime -// @see https://stackoverflow.com/a/63639280/698470 -const Contract = new Web3().eth.Contract; +import { AbiItem } from "web3-utils"; +import { Contract } from "web3-eth-contract"; import { ContractSendMethod } from "web3-eth-contract"; import { TransactionReceipt } from "web3-eth"; @@ -41,6 +43,7 @@ import { DeployContractSolidityBytecodeJsonObjectV1Request, DeployContractSolidityBytecodeV1Response, EthContractInvocationType, + EthContractInvocationWeb3Method, InvokeContractV1Request, InvokeContractJsonObjectV1Request, InvokeContractV1Response, @@ -50,23 +53,28 @@ import { Web3SigningCredentialCactusKeychainRef, Web3SigningCredentialPrivateKeyHex, Web3SigningCredentialType, + WatchBlocksV1, + WatchBlocksV1Options, + InvokeRawWeb3EthMethodV1Request, + InvokeRawWeb3EthContractV1Request, } from "./generated/openapi/typescript-axios/"; import { RunTransactionEndpoint } from "./web-services/run-transaction-endpoint"; import { InvokeContractEndpoint } from "./web-services/invoke-contract-endpoint"; import { InvokeContractJsonObjectEndpoint } from "./web-services/invoke-contract-endpoint-json-object"; -import { isWeb3SigningCredentialNone } from "./model-type-guards"; +import { WatchBlocksV1Endpoint } from "./web-services/watch-blocks-v1-endpoint"; +import { GetPrometheusExporterMetricsEndpointV1 } from "./web-services/get-prometheus-exporter-metrics-endpoint-v1"; +import { InvokeRawWeb3EthMethodEndpoint } from "./web-services/invoke-raw-web3eth-method-v1-endpoint"; +import { InvokeRawWeb3EthContractEndpoint } from "./web-services/invoke-raw-web3eth-contract-v1-endpoint"; +import { isWeb3SigningCredentialNone } from "./model-type-guards"; import { PrometheusExporter } from "./prometheus-exporter/prometheus-exporter"; -import { - GetPrometheusExporterMetricsEndpointV1, - IGetPrometheusExporterMetricsEndpointV1Options, -} from "./web-services/get-prometheus-exporter-metrics-endpoint-v1"; import { RuntimeError } from "run-time-error"; export interface IPluginLedgerConnectorQuorumOptions extends ICactusPluginOptions { rpcApiHttpHost: string; + rpcApiWsHost?: string; logLevel?: LogLevelDesc; prometheusExporter?: PrometheusExporter; pluginRegistry: PluginRegistry; @@ -96,6 +104,14 @@ export class PluginLedgerConnectorQuorum return PluginLedgerConnectorQuorum.CLASS_NAME; } + private getWeb3Provider() { + if (!this.options.rpcApiWsHost) { + return new Web3.providers.HttpProvider(this.options.rpcApiHttpHost); + } + + return new Web3.providers.WebsocketProvider(this.options.rpcApiWsHost); + } + constructor(public readonly options: IPluginLedgerConnectorQuorumOptions) { const fnTag = `${this.className}#constructor()`; Checks.truthy(options, `${fnTag} arg options`); @@ -107,10 +123,7 @@ export class PluginLedgerConnectorQuorum const label = this.className; this.log = LoggerProvider.getOrCreate({ level, label }); - const web3Provider = new Web3.providers.HttpProvider( - this.options.rpcApiHttpHost, - ); - this.web3 = new Web3(web3Provider); + this.web3 = new Web3(this.getWeb3Provider()); this.instanceId = options.instanceId; this.pluginRegistry = options.pluginRegistry as PluginRegistry; this.prometheusExporter = @@ -144,18 +157,42 @@ export class PluginLedgerConnectorQuorum public async shutdown(): Promise { this.log.info(`Shutting down ${this.className}...`); + const provider = this.web3.currentProvider; + if (provider && typeof provider == "object") { + if ("disconnect" in provider) { + provider.disconnect(1000, "shutdown"); + } + } } public async onPluginInit(): Promise { return; } - async registerWebServices(app: Express): Promise { + async registerWebServices( + app: Express, + wsApi: SocketIoServer, + ): Promise { + const { web3 } = this; + const { logLevel } = this.options; const webServices = await this.getOrCreateWebServices(); await Promise.all(webServices.map((ws) => ws.registerExpress(app))); + + wsApi.on("connection", (socket: SocketIoSocket) => { + this.log.debug(`New Socket connected. ID=${socket.id}`); + + socket.on(WatchBlocksV1.Subscribe, (options?: WatchBlocksV1Options) => { + new WatchBlocksV1Endpoint({ + web3, + socket, + logLevel, + options, + }).subscribe(); + }); + }); + return webServices; } - public async getOrCreateWebServices(): Promise { if (Array.isArray(this.endpoints)) { return this.endpoints; @@ -197,11 +234,24 @@ export class PluginLedgerConnectorQuorum endpoints.push(endpoint); } { - const opts: IGetPrometheusExporterMetricsEndpointV1Options = { + const endpoint = new GetPrometheusExporterMetricsEndpointV1({ connector: this, logLevel: this.options.logLevel, - }; - const endpoint = new GetPrometheusExporterMetricsEndpointV1(opts); + }); + endpoints.push(endpoint); + } + { + const endpoint = new InvokeRawWeb3EthMethodEndpoint({ + connector: this, + logLevel: this.options.logLevel, + }); + endpoints.push(endpoint); + } + { + const endpoint = new InvokeRawWeb3EthContractEndpoint({ + connector: this, + logLevel: this.options.logLevel, + }); endpoints.push(endpoint); } this.endpoints = endpoints; @@ -252,12 +302,15 @@ export class PluginLedgerConnectorQuorum const { methods } = contract; - return Object.prototype.hasOwnProperty.call(methods, name); + return ( + Object.prototype.hasOwnProperty.call(methods, name) && + typeof methods[name] === "function" + ); } public async getContractInfoKeychain( req: InvokeContractV1Request, - ): Promise { + ): Promise { const fnTag = `${this.className}#invokeContract()`; const { contractName, keychainId } = req; @@ -277,7 +330,6 @@ export class PluginLedgerConnectorQuorum } const contractStr = await keychainPlugin.get(contractName); const contractJSON = JSON.parse(contractStr); - (req as any).contractJSON = contractJSON; // if not exists a contract deployed, we deploy it const networkId = await this.web3.eth.net.getId(); @@ -299,14 +351,17 @@ export class PluginLedgerConnectorQuorum contractJSON.networks = network; keychainPlugin.set(req.contractName, JSON.stringify(contractJSON)); } - (req as any).contractAddress = contractJSON.networks[networkId].address; - return this.invokeContract(req); + return this.invokeContract({ + ...req, + contractAddress: contractJSON.networks[networkId].address, + contractJSON: contractJSON, + }); } public async getContractInfo( req: InvokeContractJsonObjectV1Request, - ): Promise { + ): Promise { const fnTag = `${this.className}#invokeContractNoKeychain()`; const { contractJSON, contractAddress } = req; if (!contractJSON) { @@ -318,7 +373,9 @@ export class PluginLedgerConnectorQuorum return this.invokeContract(req); } - public async invokeContract(req: any): Promise { + public async invokeContract( + req: InvokeContractJsonObjectV1Request, + ): Promise { const fnTag = `${this.className}#invokeContract()`; const { contractAddress, contractJSON } = req; @@ -654,4 +711,67 @@ export class PluginLedgerConnectorQuorum } return this.runDeploy(req); } + + // Low level function to call any method from web3.eth + // Should be used only if given functionality is not already covered by another endpoint. + public async invokeRawWeb3EthMethod( + args: InvokeRawWeb3EthMethodV1Request, + ): Promise { + this.log.debug("invokeRawWeb3EthMethod input:", JSON.stringify(args)); + + Checks.nonBlankString( + args.methodName, + "web3.eth method string must not be empty", + ); + + const looseWeb3Eth = this.web3.eth as any; + if ( + !Object.prototype.hasOwnProperty.call(looseWeb3Eth, args.methodName) || + typeof looseWeb3Eth[args.methodName] !== "function" + ) { + throw new Error(`No method "${args.methodName}" in web3.eth`); + } + + const web3MethodArgs = args.params || []; + return looseWeb3Eth[args.methodName](...web3MethodArgs); + } + + // Low level function to invoke contract + // Should be used only if given functionality is not already covered by another endpoint. + public async invokeRawWeb3EthContract( + args: InvokeRawWeb3EthContractV1Request, + ): Promise { + this.log.debug("invokeRawWeb3EthContract input:", JSON.stringify(args)); + + const contractMethodArgs = args.contractMethodArgs || []; + + if ( + !Object.values(EthContractInvocationWeb3Method).includes( + args.invocationType, + ) + ) { + throw new Error( + `Unknown invocationType (${args.invocationType}), must be specified in EthContractInvocationWeb3Method`, + ); + } + + const contract = new this.web3.eth.Contract( + args.abi as AbiItem[], + args.address, + ); + + const isSafeToCall = await this.isSafeToCallContractMethod( + contract, + args.contractMethod, + ); + if (!isSafeToCall) { + throw new RuntimeError( + `Invalid method name provided in request. ${args.contractMethod} does not exist on the Web3 contract object's "methods" property.`, + ); + } + + return contract.methods[args.contractMethod](...contractMethodArgs)[ + args.invocationType + ](args.invocationParams); + } } diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/public-api.ts b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/public-api.ts index 8fe5b14280a..5fbef1b7328 100755 --- a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/public-api.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/public-api.ts @@ -12,6 +12,18 @@ export { PluginFactoryLedgerConnector } from "./plugin-factory-ledger-connector" import { IPluginFactoryOptions } from "@hyperledger/cactus-core-api"; import { PluginFactoryLedgerConnector } from "./plugin-factory-ledger-connector"; +export { + QuorumApiClient, + QuorumApiClientOptions, + QuorumRequestInputWeb3EthMethod, + QuorumRequestInputWeb3EthContractMethod, + QuorumRequestInputContract, + QuorumRequestInputMethod, + QuorumRequestInputArgs, +} from "./api-client/quorum-api-client"; + +export * from "./generated/openapi/typescript-axios/api"; + export async function createPluginFactory( pluginFactoryOptions: IPluginFactoryOptions, ): Promise { diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/invoke-raw-web3eth-contract-v1-endpoint.ts b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/invoke-raw-web3eth-contract-v1-endpoint.ts new file mode 100644 index 00000000000..f4c3b96a47a --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/invoke-raw-web3eth-contract-v1-endpoint.ts @@ -0,0 +1,109 @@ +import { Express, Request, Response } from "express"; +import { + Logger, + Checks, + LogLevelDesc, + LoggerProvider, + IAsyncProvider, +} from "@hyperledger/cactus-common"; +import { + IEndpointAuthzOptions, + IExpressRequestHandler, + IWebServiceEndpoint, +} from "@hyperledger/cactus-core-api"; +import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; +import { PluginLedgerConnectorQuorum } from "../plugin-ledger-connector-quorum"; +import OAS from "../../json/openapi.json"; +import sanitizeHtml from "sanitize-html"; +import { InvokeRawWeb3EthContractV1Response } from "../generated/openapi/typescript-axios"; + +export interface IInvokeRawWeb3EthContractEndpointOptions { + logLevel?: LogLevelDesc; + connector: PluginLedgerConnectorQuorum; +} + +export class InvokeRawWeb3EthContractEndpoint implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "InvokeRawWeb3EthContractEndpoint"; + + private readonly log: Logger; + + public get className(): string { + return InvokeRawWeb3EthContractEndpoint.CLASS_NAME; + } + + constructor( + public readonly options: IInvokeRawWeb3EthContractEndpointOptions, + ) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + Checks.truthy(options.connector, `${fnTag} arg options.connector`); + + const level = this.options.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + public get oasPath(): typeof OAS.paths["/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-quorum/invoke-raw-web3eth-contract"] { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-quorum/invoke-raw-web3eth-contract" + ]; + } + + public getPath(): string { + return this.oasPath.post["x-hyperledger-cactus"].http.path; + } + + public getVerbLowerCase(): string { + return this.oasPath.post["x-hyperledger-cactus"].http.verbLowerCase; + } + + public getOperationId(): string { + return this.oasPath.post.operationId; + } + + getAuthorizationOptionsProvider(): IAsyncProvider { + // TODO: make this an injectable dependency in the constructor + return { + get: async () => ({ + isProtected: true, + requiredRoles: [], + }), + }; + } + + public async registerExpress( + expressApp: Express, + ): Promise { + await registerWebServiceEndpoint(expressApp, this); + return this; + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public async handleRequest(req: Request, res: Response): Promise { + const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; + this.log.debug(reqTag); + + try { + const methodResponse = await this.options.connector.invokeRawWeb3EthContract( + req.body, + ); + const response: InvokeRawWeb3EthContractV1Response = { + status: 200, + data: methodResponse, + }; + res.json(response); + } catch (ex: any) { + this.log.warn(`Error while serving ${reqTag}`, ex); + res.json({ + status: 504, + errorDetail: sanitizeHtml(ex, { + allowedTags: [], + allowedAttributes: {}, + }), + }); + } + } +} diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/invoke-raw-web3eth-method-v1-endpoint.ts b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/invoke-raw-web3eth-method-v1-endpoint.ts new file mode 100644 index 00000000000..8206b2945ae --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/invoke-raw-web3eth-method-v1-endpoint.ts @@ -0,0 +1,107 @@ +import { Express, Request, Response } from "express"; +import { + Logger, + Checks, + LogLevelDesc, + LoggerProvider, + IAsyncProvider, +} from "@hyperledger/cactus-common"; +import { + IEndpointAuthzOptions, + IExpressRequestHandler, + IWebServiceEndpoint, +} from "@hyperledger/cactus-core-api"; +import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; +import { PluginLedgerConnectorQuorum } from "../plugin-ledger-connector-quorum"; +import OAS from "../../json/openapi.json"; +import sanitizeHtml from "sanitize-html"; +import { InvokeRawWeb3EthMethodV1Response } from "../public-api"; + +export interface IInvokeRawWeb3EthMethodEndpointOptions { + logLevel?: LogLevelDesc; + connector: PluginLedgerConnectorQuorum; +} + +export class InvokeRawWeb3EthMethodEndpoint implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "InvokeRawWeb3EthMethodEndpoint"; + + private readonly log: Logger; + + public get className(): string { + return InvokeRawWeb3EthMethodEndpoint.CLASS_NAME; + } + + constructor(public readonly options: IInvokeRawWeb3EthMethodEndpointOptions) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(options, `${fnTag} arg options`); + Checks.truthy(options.connector, `${fnTag} arg options.connector`); + + const level = this.options.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + public get oasPath(): typeof OAS.paths["/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-quorum/invoke-raw-web3eth-method"] { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-quorum/invoke-raw-web3eth-method" + ]; + } + + public getPath(): string { + return this.oasPath.post["x-hyperledger-cactus"].http.path; + } + + public getVerbLowerCase(): string { + return this.oasPath.post["x-hyperledger-cactus"].http.verbLowerCase; + } + + public getOperationId(): string { + return this.oasPath.post.operationId; + } + + getAuthorizationOptionsProvider(): IAsyncProvider { + // TODO: make this an injectable dependency in the constructor + return { + get: async () => ({ + isProtected: true, + requiredRoles: [], + }), + }; + } + + public async registerExpress( + expressApp: Express, + ): Promise { + await registerWebServiceEndpoint(expressApp, this); + return this; + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public async handleRequest(req: Request, res: Response): Promise { + const reqTag = `${this.getVerbLowerCase()} - ${this.getPath()}`; + this.log.debug(reqTag); + + try { + const methodResponse = await this.options.connector.invokeRawWeb3EthMethod( + req.body, + ); + const response: InvokeRawWeb3EthMethodV1Response = { + status: 200, + data: methodResponse, + }; + res.json(response); + } catch (ex: any) { + this.log.warn(`Error while serving ${reqTag}`, ex); + res.json({ + status: 504, + errorDetail: sanitizeHtml(ex, { + allowedTags: [], + allowedAttributes: {}, + }), + }); + } + } +} diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/watch-blocks-v1-endpoint.ts b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/watch-blocks-v1-endpoint.ts new file mode 100644 index 00000000000..c56f76ebbb5 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/watch-blocks-v1-endpoint.ts @@ -0,0 +1,103 @@ +import { + Logger, + LogLevelDesc, + LoggerProvider, + Checks, +} from "@hyperledger/cactus-common"; +import { + WatchBlocksV1Options, + WatchBlocksV1Progress, + WatchBlocksV1, + WatchBlocksV1BlockData, +} from "../generated/openapi/typescript-axios"; +import { Socket as SocketIoSocket } from "socket.io"; +import Web3 from "web3"; + +export interface IWatchBlocksV1EndpointConfiguration { + logLevel?: LogLevelDesc; + socket: SocketIoSocket; + web3: Web3; + options?: WatchBlocksV1Options; +} + +export class WatchBlocksV1Endpoint { + public static readonly CLASS_NAME = "WatchBlocksV1Endpoint"; + + private readonly log: Logger; + private readonly socket: SocketIoSocket< + Record void>, + Record void> + >; + private readonly web3: Web3; + private readonly isGetBlockData: boolean; + + public get className(): string { + return WatchBlocksV1Endpoint.CLASS_NAME; + } + + constructor(public readonly config: IWatchBlocksV1EndpointConfiguration) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(config, `${fnTag} arg options`); + Checks.truthy(config.web3, `${fnTag} arg options.web3`); + Checks.truthy(config.socket, `${fnTag} arg options.socket`); + + this.web3 = config.web3; + this.socket = config.socket; + this.isGetBlockData = config.options?.getBlockData == true; + + const level = this.config.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + public async subscribe(): Promise { + const { socket, log, web3, isGetBlockData } = this; + log.debug(`${WatchBlocksV1.Subscribe} => ${socket.id}`); + + const sub = web3.eth.subscribe( + "newBlockHeaders", + async (ex, blockHeader) => { + log.debug("newBlockHeaders: Error=%o BlockHeader=%o", ex, blockHeader); + + if (ex) { + socket.emit(WatchBlocksV1.Error, ex.message); + sub.unsubscribe(); + } else if (blockHeader) { + let next: WatchBlocksV1Progress; + + if (isGetBlockData) { + const web3BlockData = await web3.eth.getBlock( + blockHeader.hash, + true, + ); + + next = { + // difficulty and totalDifficulty returned from the ledger are string, forcing typecast + blockData: (web3BlockData as unknown) as WatchBlocksV1BlockData, + }; + } else { + next = { blockHeader }; + } + + socket.emit(WatchBlocksV1.Next, next); + } + }, + ); + + log.debug("Subscribing to Web3 new block headers event..."); + + socket.on("disconnect", async (reason: string) => { + log.debug("WebSocket:disconnect reason=%o", reason); + sub.unsubscribe((ex: Error, success: boolean) => { + log.debug("Web3 unsubscribe success=%o, ex=%", success, ex); + }); + }); + + socket.on(WatchBlocksV1.Unsubscribe, () => { + log.debug(`${WatchBlocksV1.Unsubscribe}: unsubscribing Web3...`); + sub.unsubscribe((ex: Error, success: boolean) => { + log.debug("Web3 unsubscribe error=%o, success=%", ex, success); + }); + }); + } +} diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/openapi/openapi-validation-no-keychain.test.ts b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/openapi/openapi-validation-no-keychain.test.ts index ef2e7f8a8b7..ec230d14b3e 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/openapi/openapi-validation-no-keychain.test.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/openapi/openapi-validation-no-keychain.test.ts @@ -30,8 +30,9 @@ import express from "express"; import bodyParser from "body-parser"; import http from "http"; import { AddressInfo } from "net"; -import { Configuration } from "@hyperledger/cactus-core-api"; +import { Configuration, Constants } from "@hyperledger/cactus-core-api"; import { PluginRegistry } from "@hyperledger/cactus-core"; +import { Server as SocketIoServer } from "socket.io"; import { installOpenapiValidationMiddleware } from "@hyperledger/cactus-core"; import OAS from "../../../../../../main/json/openapi.json"; @@ -97,6 +98,10 @@ test(testCase, async (t: Test) => { const apiConfig = new Configuration({ basePath: apiHost }); const apiClient = new QuorumApi(apiConfig); + const wsApi = new SocketIoServer(server, { + path: Constants.SocketIoConnectionPathV1, + }); + await installOpenapiValidationMiddleware({ logLevel, app: expressApp, @@ -104,7 +109,7 @@ test(testCase, async (t: Test) => { }); await connector.getOrCreateWebServices(); - await connector.registerWebServices(expressApp); + await connector.registerWebServices(expressApp, wsApi); const fDeploy = "deployContractSolBytecodeJsonObjectV1"; const fInvoke = "invokeContractV1NoKeychain"; diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/openapi/openapi-validation.test.ts b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/openapi/openapi-validation.test.ts index eaadd41a22a..90423c62eb4 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/openapi/openapi-validation.test.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/openapi/openapi-validation.test.ts @@ -27,7 +27,8 @@ import { AddressInfo } from "net"; import express from "express"; import bodyParser from "body-parser"; import http from "http"; -import { Configuration } from "@hyperledger/cactus-core-api"; +import { Configuration, Constants } from "@hyperledger/cactus-core-api"; +import { Server as SocketIoServer } from "socket.io"; import { installOpenapiValidationMiddleware } from "@hyperledger/cactus-core"; import OAS from "../../../../../../main/json/openapi.json"; @@ -116,6 +117,10 @@ test(testCase, async (t: Test) => { const apiConfig = new Configuration({ basePath: apiHost }); const apiClient = new QuorumApi(apiConfig); + const wsApi = new SocketIoServer(server, { + path: Constants.SocketIoConnectionPathV1, + }); + await installOpenapiValidationMiddleware({ logLevel, app: expressApp, @@ -123,7 +128,7 @@ test(testCase, async (t: Test) => { }); await connector.getOrCreateWebServices(); - await connector.registerWebServices(expressApp); + await connector.registerWebServices(expressApp, wsApi); const fDeploy = "apiV1QuorumDeployContractSolidityBytecode"; const fInvoke = "apiV1QuorumInvokeContract"; diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v2.3.0-deploy-contract-from-json-json-object-endpoints.test.ts b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v2.3.0-deploy-contract-from-json-json-object-endpoints.test.ts index be6603ef906..f6df95596dd 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v2.3.0-deploy-contract-from-json-json-object-endpoints.test.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v2.3.0-deploy-contract-from-json-json-object-endpoints.test.ts @@ -32,7 +32,8 @@ import express from "express"; import bodyParser from "body-parser"; import http from "http"; import { AddressInfo } from "net"; -import { Configuration } from "@hyperledger/cactus-core-api"; +import { Configuration, Constants } from "@hyperledger/cactus-core-api"; +import { Server as SocketIoServer } from "socket.io"; const logLevel: LogLevelDesc = "INFO"; @@ -101,8 +102,12 @@ test(testCase, async (t: Test) => { const apiConfig = new Configuration({ basePath: apiHost }); const apiClient = new QuorumApi(apiConfig); + const wsApi = new SocketIoServer(server, { + path: Constants.SocketIoConnectionPathV1, + }); + await connector.getOrCreateWebServices(); - await connector.registerWebServices(expressApp); + await connector.registerWebServices(expressApp, wsApi); await connector.transact({ web3SigningCredential: { diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v2.3.0-deploy-contract-from-json-json-object.test.ts b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v2.3.0-deploy-contract-from-json-json-object.test.ts index 9e69dcbe5b2..a6c3b1226f2 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v2.3.0-deploy-contract-from-json-json-object.test.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v2.3.0-deploy-contract-from-json-json-object.test.ts @@ -32,7 +32,8 @@ import express from "express"; import bodyParser from "body-parser"; import http from "http"; import { AddressInfo } from "net"; -import { Configuration } from "@hyperledger/cactus-core-api"; +import { Configuration, Constants } from "@hyperledger/cactus-core-api"; +import { Server as SocketIoServer } from "socket.io"; const logLevel: LogLevelDesc = "INFO"; @@ -101,8 +102,12 @@ test(testCase, async (t: Test) => { const apiConfig = new Configuration({ basePath: apiHost }); const apiClient = new QuorumApi(apiConfig); + const wsApi = new SocketIoServer(server, { + path: Constants.SocketIoConnectionPathV1, + }); + await connector.getOrCreateWebServices(); - await connector.registerWebServices(expressApp); + await connector.registerWebServices(expressApp, wsApi); await connector.transact({ web3SigningCredential: { diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v2.3.0-deploy-contract-from-json.test.ts b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v2.3.0-deploy-contract-from-json.test.ts index 29771a10efa..09122401d06 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v2.3.0-deploy-contract-from-json.test.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v2.3.0-deploy-contract-from-json.test.ts @@ -35,7 +35,8 @@ import express from "express"; import bodyParser from "body-parser"; import http from "http"; import { AddressInfo } from "net"; -import { Configuration } from "@hyperledger/cactus-core-api"; +import { Configuration, Constants } from "@hyperledger/cactus-core-api"; +import { Server as SocketIoServer } from "socket.io"; const logLevel: LogLevelDesc = "INFO"; const contractName = "HelloWorld"; @@ -120,8 +121,12 @@ test(testCase, async (t: Test) => { const apiConfig = new Configuration({ basePath: apiHost }); const apiClient = new QuorumApi(apiConfig); + const wsApi = new SocketIoServer(server, { + path: Constants.SocketIoConnectionPathV1, + }); + await connector.getOrCreateWebServices(); - await connector.registerWebServices(expressApp); + await connector.registerWebServices(expressApp, wsApi); await connector.transact({ web3SigningCredential: { diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v2.3.0-invoke-contract-json-object-endpoints.test.ts b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v2.3.0-invoke-contract-json-object-endpoints.test.ts index 351e9859a9c..b67e7e2a4a3 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v2.3.0-invoke-contract-json-object-endpoints.test.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v2.3.0-invoke-contract-json-object-endpoints.test.ts @@ -28,7 +28,8 @@ import express from "express"; import bodyParser from "body-parser"; import http from "http"; import { AddressInfo } from "net"; -import { Configuration } from "@hyperledger/cactus-core-api"; +import { Configuration, Constants } from "@hyperledger/cactus-core-api"; +import { Server as SocketIoServer } from "socket.io"; const logLevel: LogLevelDesc = "INFO"; @@ -87,8 +88,12 @@ test("Quorum Ledger Connector Plugin", async (t: Test) => { const apiConfig = new Configuration({ basePath: apiHost }); const apiClient = new QuorumApi(apiConfig); + const wsApi = new SocketIoServer(server, { + path: Constants.SocketIoConnectionPathV1, + }); + await connector.getOrCreateWebServices(); - await connector.registerWebServices(expressApp); + await connector.registerWebServices(expressApp, wsApi); await connector.transact({ web3SigningCredential: { diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-deploy-contract-from-json-json-object-endpoints.test.ts b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-deploy-contract-from-json-json-object-endpoints.test.ts index 30ab5bc0921..0302fdd6be6 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-deploy-contract-from-json-json-object-endpoints.test.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-deploy-contract-from-json-json-object-endpoints.test.ts @@ -31,8 +31,9 @@ import express from "express"; import bodyParser from "body-parser"; import http from "http"; import { AddressInfo } from "net"; -import { Configuration } from "@hyperledger/cactus-core-api"; +import { Configuration, Constants } from "@hyperledger/cactus-core-api"; import { PluginRegistry } from "@hyperledger/cactus-core"; +import { Server as SocketIoServer } from "socket.io"; const logLevel: LogLevelDesc = "INFO"; @@ -98,8 +99,12 @@ test(testCase, async (t: Test) => { const apiConfig = new Configuration({ basePath: apiHost }); const apiClient = new QuorumApi(apiConfig); + const wsApi = new SocketIoServer(server, { + path: Constants.SocketIoConnectionPathV1, + }); + await connector.getOrCreateWebServices(); - await connector.registerWebServices(expressApp); + await connector.registerWebServices(expressApp, wsApi); await connector.transact({ web3SigningCredential: { diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-deploy-contract-from-json-json-object.test.ts b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-deploy-contract-from-json-json-object.test.ts index 31e6da46edf..d6617e3ae47 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-deploy-contract-from-json-json-object.test.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-deploy-contract-from-json-json-object.test.ts @@ -31,8 +31,9 @@ import express from "express"; import bodyParser from "body-parser"; import http from "http"; import { AddressInfo } from "net"; -import { Configuration } from "@hyperledger/cactus-core-api"; +import { Configuration, Constants } from "@hyperledger/cactus-core-api"; import { PluginRegistry } from "@hyperledger/cactus-core"; +import { Server as SocketIoServer } from "socket.io"; const logLevel: LogLevelDesc = "INFO"; @@ -98,8 +99,12 @@ test(testCase, async (t: Test) => { const apiConfig = new Configuration({ basePath: apiHost }); const apiClient = new QuorumApi(apiConfig); + const wsApi = new SocketIoServer(server, { + path: Constants.SocketIoConnectionPathV1, + }); + await connector.getOrCreateWebServices(); - await connector.registerWebServices(expressApp); + await connector.registerWebServices(expressApp, wsApi); await connector.transact({ web3SigningCredential: { diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-deploy-contract-from-json.test.ts b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-deploy-contract-from-json.test.ts index 7ad0543b9fc..69edb85e67a 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-deploy-contract-from-json.test.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-deploy-contract-from-json.test.ts @@ -33,7 +33,8 @@ import express from "express"; import bodyParser from "body-parser"; import http from "http"; import { AddressInfo } from "net"; -import { Configuration } from "@hyperledger/cactus-core-api"; +import { Configuration, Constants } from "@hyperledger/cactus-core-api"; +import { Server as SocketIoServer } from "socket.io"; const testCase = "Quorum Ledger Connector Plugin"; @@ -63,6 +64,9 @@ describe(testCase, () => { keychainEntryValue: string, keychainPlugin: PluginKeychainMemory, firstHighNetWorthAccount: string; + const wsApi = new SocketIoServer(server, { + path: Constants.SocketIoConnectionPathV1, + }); afterAll(async () => await Servers.shutdown(server)); beforeAll(async () => { @@ -133,7 +137,7 @@ describe(testCase, () => { ); // Instantiate connector with the keychain plugin that already has the // private key we want to use for one of our tests - await connector.registerWebServices(expressApp); + await connector.registerWebServices(expressApp, wsApi); await connector.transact({ web3SigningCredential: { diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-invoke-contract-json-object-endpoints.test.ts b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-invoke-contract-json-object-endpoints.test.ts index 1a6869466d1..a8db04228c8 100644 --- a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-invoke-contract-json-object-endpoints.test.ts +++ b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-invoke-contract-json-object-endpoints.test.ts @@ -23,7 +23,8 @@ import { IAccount, } from "@hyperledger/cactus-test-tooling"; import { PluginRegistry } from "@hyperledger/cactus-core"; -import { Configuration } from "@hyperledger/cactus-core-api"; +import { Configuration, Constants } from "@hyperledger/cactus-core-api"; +import { Server as SocketIoServer } from "socket.io"; import express from "express"; import bodyParser from "body-parser"; @@ -87,8 +88,12 @@ test("Quorum Ledger Connector Plugin", async (t: Test) => { const apiConfig = new Configuration({ basePath: apiHost }); const apiClient = new QuorumApi(apiConfig); + const wsApi = new SocketIoServer(server, { + path: Constants.SocketIoConnectionPathV1, + }); + await connector.getOrCreateWebServices(); - await connector.registerWebServices(expressApp); + await connector.registerWebServices(expressApp, wsApi); await connector.transact({ web3SigningCredential: { diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-invoke-web3-contract-v1.test.ts b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-invoke-web3-contract-v1.test.ts new file mode 100644 index 00000000000..165a4e16be6 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-invoke-web3-contract-v1.test.ts @@ -0,0 +1,203 @@ +/* + * Copyright 2020-2022 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +////////////////////////////////// +// Constants +////////////////////////////////// + +const testLogLevel = "info"; +const sutLogLevel = "info"; + +const containerImageVersion = "2021-05-03-quorum-v21.4.1"; + +import "jest-extended"; +import { v4 as uuidv4 } from "uuid"; +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { + EthContractInvocationWeb3Method, + InvokeRawWeb3EthContractV1Request, + PluginLedgerConnectorQuorum, + Web3SigningCredentialType, +} from "../../../../../main/typescript/index"; +import { + QuorumTestLedger, + IQuorumGenesisOptions, + IAccount, + pruneDockerAllIfGithubAction, +} from "@hyperledger/cactus-test-tooling"; +import { Logger, LoggerProvider } from "@hyperledger/cactus-common"; +import { AbiItem } from "web3-utils"; + +import HelloWorldContractJson from "../../../../solidity/hello-world-contract/HelloWorld.json"; +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; + +// Unit Test logger setup +const log: Logger = LoggerProvider.getOrCreate({ + label: "v21.4.1-invoke-web3-contract-v1.test", + level: testLogLevel, +}); +log.info("Test started"); + +describe("invokeRawWeb3EthContract Tests", () => { + let quorumTestLedger: QuorumTestLedger; + let connector: PluginLedgerConnectorQuorum; + let firstHighNetWorthAccount: string; + let contractAbi: AbiItem[]; + let contractAddress: string; + + ////////////////////////////////// + // Environment Setup + ////////////////////////////////// + + beforeAll(async () => { + log.info("Prune Docker..."); + await pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + + log.info("Start QuorumTestLedger..."); + log.debug("Quorum version:", containerImageVersion); + quorumTestLedger = new QuorumTestLedger({ + containerImageVersion, + }); + await quorumTestLedger.start(); + + log.info("Get highNetWorthAccounts..."); + const quorumGenesisOptions: IQuorumGenesisOptions = await quorumTestLedger.getGenesisJsObject(); + expect(quorumGenesisOptions).toBeTruthy(); + expect(quorumGenesisOptions.alloc).toBeTruthy(); + + const highNetWorthAccounts: string[] = Object.keys( + quorumGenesisOptions.alloc, + ).filter((address: string) => { + const anAccount: IAccount = quorumGenesisOptions.alloc[address]; + const theBalance = parseInt(anAccount.balance, 10); + return theBalance > 10e7; + }); + [firstHighNetWorthAccount] = highNetWorthAccounts; + + const rpcApiHttpHost = await quorumTestLedger.getRpcApiHttpHost(); + log.debug("rpcApiHttpHost:", rpcApiHttpHost); + + log.info("Create PluginKeychainMemory..."); + const keychainPlugin = new PluginKeychainMemory({ + instanceId: uuidv4(), + keychainId: uuidv4(), + logLevel: sutLogLevel, + }); + keychainPlugin.set( + HelloWorldContractJson.contractName, + JSON.stringify(HelloWorldContractJson), + ); + + log.info("Create PluginLedgerConnectorQuorum..."); + connector = new PluginLedgerConnectorQuorum({ + rpcApiHttpHost: rpcApiHttpHost, + logLevel: sutLogLevel, + instanceId: uuidv4(), + pluginRegistry: new PluginRegistry({ plugins: [keychainPlugin] }), + }); + + log.info("Deploy contract to interact with..."); + const deployOut = await connector.deployContract({ + contractName: HelloWorldContractJson.contractName, + keychainId: keychainPlugin.getKeychainId(), + web3SigningCredential: { + ethAccount: firstHighNetWorthAccount, + secret: "", + type: Web3SigningCredentialType.GethKeychainPassword, + }, + gas: 1000000, + }); + expect(deployOut).toBeTruthy(); + expect(deployOut.transactionReceipt).toBeTruthy(); + expect(deployOut.transactionReceipt.contractAddress).toBeTruthy(); + expect(deployOut.transactionReceipt.status).toBeTrue(); + + contractAbi = HelloWorldContractJson.abi as AbiItem[]; + contractAddress = deployOut.transactionReceipt.contractAddress as string; + }); + + afterAll(async () => { + log.info("Shutdown connector"); + await connector.shutdown(); + + log.info("Stop and destroy the test ledger..."); + await quorumTestLedger.stop(); + await quorumTestLedger.destroy(); + + log.info("Prune docker..."); + await pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + }); + + test("invokeRawWeb3EthContract send and call to valid contract works correctly", async () => { + const newName = "QuorumCactus"; + + // 1. Set new value (send) + const sendInvocationArgs = { + from: firstHighNetWorthAccount, + }; + + const sendInvokeArgs: InvokeRawWeb3EthContractV1Request = { + abi: contractAbi, + address: contractAddress, + invocationType: EthContractInvocationWeb3Method.Send, + invocationParams: sendInvocationArgs, + contractMethod: "setName", + contractMethodArgs: [newName], + }; + + const resultsSend = await connector.invokeRawWeb3EthContract( + sendInvokeArgs, + ); + expect(resultsSend).toBeTruthy(); + expect(resultsSend.status).toBeTrue(); + + // // 2. Get new, updated value (call) + const callInvokeArgs: InvokeRawWeb3EthContractV1Request = { + abi: contractAbi, + address: contractAddress, + invocationType: EthContractInvocationWeb3Method.Call, + contractMethod: "getName", + }; + + const resultsCall = await connector.invokeRawWeb3EthContract( + callInvokeArgs, + ); + expect(resultsCall).toBeTruthy(); + expect(resultsCall).toEqual(newName); + }); + + test("invokeRawWeb3EthContract throws error when called on wrong contract", async () => { + const callInvokeArgs: InvokeRawWeb3EthContractV1Request = { + abi: contractAbi, + address: "0x0321", + invocationType: EthContractInvocationWeb3Method.Call, + contractMethod: "getName", + }; + + await expect(connector.invokeRawWeb3EthContract(callInvokeArgs)).toReject(); + }); + + test("invokeRawWeb3EthContract throws error when requested wrong invocation method", async () => { + const callInvokeArgs: InvokeRawWeb3EthContractV1Request = { + abi: contractAbi, + address: contractAddress, + invocationType: "foo" as EthContractInvocationWeb3Method, + contractMethod: "getName", + }; + + await expect(connector.invokeRawWeb3EthContract(callInvokeArgs)).toReject(); + }); + + test("invokeRawWeb3EthContract throws error when called non existent contract method", async () => { + const callInvokeArgs: InvokeRawWeb3EthContractV1Request = { + abi: contractAbi, + address: contractAddress, + invocationType: EthContractInvocationWeb3Method.Call, + contractMethod: "nonExistingFoo", + }; + + await expect(connector.invokeRawWeb3EthContract(callInvokeArgs)).toReject(); + }); +}); diff --git a/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-invoke-web3-method-v1.test.ts b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-invoke-web3-method-v1.test.ts new file mode 100644 index 00000000000..5b897746fa8 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-quorum/src/test/typescript/integration/plugin-ledger-connector-quorum/deploy-contract/v21.4.1-invoke-web3-method-v1.test.ts @@ -0,0 +1,157 @@ +/* + * Copyright 2020-2022 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +////////////////////////////////// +// Constants +////////////////////////////////// + +const testLogLevel = "info"; +const sutLogLevel = "info"; + +const containerImageVersion = "2021-05-03-quorum-v21.4.1"; + +import "jest-extended"; +import { v4 as uuidv4 } from "uuid"; +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { PluginLedgerConnectorQuorum } from "../../../../../main/typescript/index"; +import { + QuorumTestLedger, + pruneDockerAllIfGithubAction, +} from "@hyperledger/cactus-test-tooling"; +import { Logger, LoggerProvider } from "@hyperledger/cactus-common"; +import Web3 from "web3"; + +// Unit Test logger setup +const log: Logger = LoggerProvider.getOrCreate({ + label: "v21.4.1-invoke-web3-method-v1.test", + level: testLogLevel, +}); +log.info("Test started"); + +describe("invokeRawWeb3EthMethod Tests", () => { + let quorumTestLedger: QuorumTestLedger; + let connector: PluginLedgerConnectorQuorum; + let web3: Web3; + + ////////////////////////////////// + // Environment Setup + ////////////////////////////////// + + beforeAll(async () => { + log.info("Prune Docker..."); + await pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + + log.info("Start QuorumTestLedger..."); + log.debug("Quorum version:", containerImageVersion); + quorumTestLedger = new QuorumTestLedger({ + containerImageVersion, + }); + await quorumTestLedger.start(); + + const rpcApiHttpHost = await quorumTestLedger.getRpcApiHttpHost(); + log.debug("rpcApiHttpHost:", rpcApiHttpHost); + + log.info("Create PluginLedgerConnectorQuorum..."); + connector = new PluginLedgerConnectorQuorum({ + rpcApiHttpHost: rpcApiHttpHost, + logLevel: sutLogLevel, + instanceId: uuidv4(), + pluginRegistry: new PluginRegistry(), + }); + + web3 = new Web3(rpcApiHttpHost); + }); + + afterAll(async () => { + log.info("Shutdown connector"); + await connector.shutdown(); + + log.info("Stop and destroy the test ledger..."); + await quorumTestLedger.stop(); + await quorumTestLedger.destroy(); + + log.info("Prune docker..."); + await pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + }); + + test("invokeRawWeb3EthMethod with 0-argument method works (getGasPrice)", async () => { + const connectorResponse = await connector.invokeRawWeb3EthMethod({ + methodName: "getGasPrice", + }); + expect(connectorResponse).toBeTruthy(); + expect(connectorResponse).toEqual("0"); // gas is free on quorum + }); + + test("invokeRawWeb3EthMethod with 1-argument method works (getBlock)", async () => { + const connectorResponse = await connector.invokeRawWeb3EthMethod({ + methodName: "getBlock", + params: ["earliest"], + }); + expect(connectorResponse).toBeTruthy(); + expect(connectorResponse.hash.length).toBeGreaterThan(5); + + // Compare with direct web3 response + const web3Response = await web3.eth.getBlock("earliest"); + expect(web3Response).toBeTruthy(); + expect(web3Response).toEqual(connectorResponse); + }); + + test("invokeRawWeb3EthMethod with 2-argument method works (getStorageAt)", async () => { + const genesisAccount = await quorumTestLedger.getGenesisAccount(); + log.debug("genesisAccount:", genesisAccount); + + const connectorResponse = await connector.invokeRawWeb3EthMethod({ + methodName: "getStorageAt", + params: [genesisAccount, 0], + }); + expect(connectorResponse).toBeTruthy(); + + // Compare with direct web3 response + const web3Response = await web3.eth.getStorageAt(genesisAccount, 0); + expect(web3Response).toBeTruthy(); + expect(web3Response).toEqual(connectorResponse); + }); + + test("invokeRawWeb3EthMethod with missing arg throws error (getBlock)", async () => { + try { + const connectorResponse = connector.invokeRawWeb3EthMethod({ + methodName: "getBlock", + }); + + await connectorResponse; + fail("Calling getBlock with missing argument should throw an error"); + } catch (err) { + expect(err).toBeTruthy(); + } + }); + + test("invokeRawWeb3EthMethod with invalid arg throws error (getBlock)", async () => { + try { + const connectorResponse = connector.invokeRawWeb3EthMethod({ + methodName: "getBlock", + params: ["foo"], + }); + + await connectorResponse; + fail("Calling getBlock with argument should throw an error"); + } catch (err) { + expect(err).toBeTruthy(); + } + }); + + test("invokeRawWeb3EthMethod with non existing method throws error", async () => { + try { + const connectorResponse = connector.invokeRawWeb3EthMethod({ + methodName: "foo", + params: ["foo"], + }); + + await connectorResponse; + fail("Calling non existing method should throw an error"); + } catch (err) { + expect(err).toBeTruthy(); + } + }); +}); diff --git a/packages/cactus-test-api-client/package.json b/packages/cactus-test-api-client/package.json index 4b6636c81d3..a9091201971 100644 --- a/packages/cactus-test-api-client/package.json +++ b/packages/cactus-test-api-client/package.json @@ -56,8 +56,6 @@ "@hyperledger/cactus-core": "1.0.0", "@hyperledger/cactus-core-api": "1.0.0", "@hyperledger/cactus-plugin-consortium-manual": "1.0.0", - "@hyperledger/cactus-plugin-ledger-connector-quorum": "1.0.0", - "@hyperledger/cactus-verifier-client": "1.0.0", "jose": "4.1.0", "web3": "1.5.2" }, diff --git a/packages/cactus-test-plugin-ledger-connector-besu/package.json b/packages/cactus-test-plugin-ledger-connector-besu/package.json index 870819d5eb2..d21bc877bdb 100644 --- a/packages/cactus-test-plugin-ledger-connector-besu/package.json +++ b/packages/cactus-test-plugin-ledger-connector-besu/package.json @@ -56,6 +56,7 @@ "@hyperledger/cactus-plugin-keychain-memory": "1.0.0", "@hyperledger/cactus-plugin-ledger-connector-besu": "1.0.0", "@hyperledger/cactus-test-tooling": "1.0.0", + "@hyperledger/cactus-verifier-client": "1.0.0", "key-encoder": "2.0.3", "web3": "1.5.2", "web3js-quorum": "21.7.0-rc1" diff --git a/packages/cactus-test-api-client/src/test/typescript/integration/verifier-integration-with-openapi-connectors.test.ts b/packages/cactus-test-plugin-ledger-connector-besu/src/test/typescript/integration/api-client/verifier-integration-with-besu-connector.test.ts similarity index 97% rename from packages/cactus-test-api-client/src/test/typescript/integration/verifier-integration-with-openapi-connectors.test.ts rename to packages/cactus-test-plugin-ledger-connector-besu/src/test/typescript/integration/api-client/verifier-integration-with-besu-connector.test.ts index 6b8a1a4d592..d59459f52ac 100644 --- a/packages/cactus-test-api-client/src/test/typescript/integration/verifier-integration-with-openapi-connectors.test.ts +++ b/packages/cactus-test-plugin-ledger-connector-besu/src/test/typescript/integration/api-client/verifier-integration-with-besu-connector.test.ts @@ -1,3 +1,8 @@ +/* + * Copyright 2022 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + // Besu setup code based on: // packages/cactus-plugin-ledger-connector-besu/src/test/typescript/integration/plugin-ledger-connector-besu/deploy-contract/v21-deploy-contract-from-json.test.ts @@ -53,7 +58,7 @@ const log: Logger = LoggerProvider.getOrCreate({ }); log.info("Test started"); -describe("Verifier integration with openapi connectors tests", () => { +describe("Verifier integration with besu connector tests", () => { let besuTestLedger: BesuTestLedger; let server: http.Server; let connector: PluginLedgerConnectorBesu; diff --git a/packages/cactus-test-plugin-ledger-connector-quorum/package.json b/packages/cactus-test-plugin-ledger-connector-quorum/package.json index 09090924ae7..be8bdae3f5d 100644 --- a/packages/cactus-test-plugin-ledger-connector-quorum/package.json +++ b/packages/cactus-test-plugin-ledger-connector-quorum/package.json @@ -56,6 +56,7 @@ "@hyperledger/cactus-core-api": "1.0.0", "@hyperledger/cactus-plugin-keychain-memory": "1.0.0", "@hyperledger/cactus-plugin-ledger-connector-quorum": "1.0.0", + "@hyperledger/cactus-verifier-client": "1.0.0", "web3": "1.5.2" }, "devDependencies": { diff --git a/packages/cactus-test-plugin-ledger-connector-quorum/src/test/typescript/integration/api-client/verifier-integration-with-quorum-connector.test.ts b/packages/cactus-test-plugin-ledger-connector-quorum/src/test/typescript/integration/api-client/verifier-integration-with-quorum-connector.test.ts new file mode 100644 index 00000000000..d0fae5441ab --- /dev/null +++ b/packages/cactus-test-plugin-ledger-connector-quorum/src/test/typescript/integration/api-client/verifier-integration-with-quorum-connector.test.ts @@ -0,0 +1,631 @@ +/* + * Copyright 2022 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +////////////////////////////////// +// Constants +////////////////////////////////// + +const testLogLevel = "info"; +const sutLogLevel = "info"; + +const containerImageName = + "ghcr.io/hyperledger/cactus-quorum-multi-party-all-in-one"; +const containerImageVersion = "2022-04-06-fd10e27"; + +import "jest-extended"; +import lodash from "lodash"; +import { v4 as uuidv4 } from "uuid"; +import Web3 from "web3"; +import { AbiItem } from "web3-utils"; +import { PluginRegistry } from "@hyperledger/cactus-core"; +import { + PluginLedgerConnectorQuorum, + QuorumApiClient, + WatchBlocksV1Progress, + Web3BlockHeader, + Web3SigningCredentialType, +} from "@hyperledger/cactus-plugin-ledger-connector-quorum"; +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; +import { Logger, LoggerProvider } from "@hyperledger/cactus-common"; +import { + ICactusPlugin, + IVerifierEventListener, + LedgerEvent, +} from "@hyperledger/cactus-core-api"; +import { AddressInfo } from "net"; +import { + ApiServer, + AuthorizationProtocol, + ConfigService, +} from "@hyperledger/cactus-cmd-api-server"; + +import { Verifier, VerifierFactory } from "@hyperledger/cactus-verifier-client"; +import { + pruneDockerAllIfGithubAction, + QuorumMultiPartyTestLedger, +} from "@hyperledger/cactus-test-tooling"; + +import HelloWorldContractJson from "../../../solidity/hello-world-contract/HelloWorld.json"; + +const log: Logger = LoggerProvider.getOrCreate({ + label: "verifier-integration-with-quorum-connector.test", + level: testLogLevel, +}); + +log.info("Test started"); + +describe("Verifier integration with quorum connector tests", () => { + let quorumTestLedger: QuorumMultiPartyTestLedger; + let apiServer: ApiServer; + let connector: PluginLedgerConnectorQuorum; + let web3: Web3; + let keychainPlugin: PluginKeychainMemory; + let connectionProfile: ReturnType< + typeof QuorumMultiPartyTestLedger.prototype.getKeys + > extends Promise + ? T + : never; + + const quorumValidatorId = "testQuorumId"; + let globalVerifierFactory: VerifierFactory; + + ////////////////////////////////// + // Environment Setup + ////////////////////////////////// + + beforeAll(async () => { + log.info("Prune Docker..."); + await pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + + // Start Ledger + log.info("Start QuorumMultiPartyTestLedger..."); + log.debug("QuorumMultiParty image:", containerImageName); + log.debug("QuorumMultiParty version:", containerImageVersion); + quorumTestLedger = new QuorumMultiPartyTestLedger({ + containerImageName, + containerImageVersion, + logLevel: sutLogLevel, + emitContainerLogs: false, + //useRunningLedger: true, + }); + await quorumTestLedger.start(); + + connectionProfile = await quorumTestLedger.getKeys(); + log.debug("connectionProfile:", connectionProfile); + + // Setup ApiServer plugins + const plugins: ICactusPlugin[] = []; + const pluginRegistry = new PluginRegistry({ plugins }); + + log.info("Create PluginKeychainMemory..."); + keychainPlugin = new PluginKeychainMemory({ + instanceId: uuidv4(), + keychainId: uuidv4(), + logLevel: sutLogLevel, + }); + keychainPlugin.set( + HelloWorldContractJson.contractName, + JSON.stringify(HelloWorldContractJson), + ); + plugins.push(keychainPlugin); + + log.info("Create PluginLedgerConnectorQuorum..."); + connector = new PluginLedgerConnectorQuorum({ + rpcApiHttpHost: connectionProfile.quorum.member1.url, + rpcApiWsHost: connectionProfile.quorum.member1.wsUrl, + logLevel: sutLogLevel, + instanceId: uuidv4(), + pluginRegistry: new PluginRegistry({ plugins: [keychainPlugin] }), + }); + plugins.push(connector); + + // Create web3 provider for test + web3 = new Web3(connectionProfile.quorum.member1.url); + + // Create Api Server + log.info("Create ApiServer..."); + const configService = new ConfigService(); + const cactusApiServerOptions = await configService.newExampleConfig(); + cactusApiServerOptions.authorizationProtocol = AuthorizationProtocol.NONE; + cactusApiServerOptions.configFile = ""; + cactusApiServerOptions.apiCorsDomainCsv = "*"; + cactusApiServerOptions.apiTlsEnabled = false; + cactusApiServerOptions.apiPort = 0; + const config = await configService.newExampleConfigConvict( + cactusApiServerOptions, + ); + + apiServer = new ApiServer({ + config: config.getProperties(), + pluginRegistry, + }); + + // Start ApiServer + const apiServerStartOut = await apiServer.start(); + log.debug(`apiServerStartOut:`, apiServerStartOut); + const httpServer = apiServer.getHttpServerApi(); + + const addressInfo = httpServer?.address() as AddressInfo; + const { address, port } = addressInfo; + const apiHost = `http://${address}:${port}`; + + // Create VerifierFactory + log.info("Create VerifierFactory with Quorum Validator..."); + globalVerifierFactory = new VerifierFactory( + [ + { + validatorID: quorumValidatorId, + validatorType: "QUORUM_2X", + basePath: apiHost, + logLevel: sutLogLevel, + }, + ], + sutLogLevel, + ); + }); + + afterAll(async () => { + log.info("Shutdown the server..."); + if (apiServer) { + await apiServer.shutdown(); + } + + log.info("Stop and destroy the test ledger..."); + if (quorumTestLedger) { + await quorumTestLedger.stop(); + await quorumTestLedger.destroy(); + } + + log.info("Prune docker..."); + await pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + }); + + ////////////////////////////////// + // Helper Functions + ////////////////////////////////// + + function monitorAndGetBlock( + options: Record = {}, + ): Promise> { + return new Promise>( + (resolve, reject) => { + const appId = "testMonitor"; + const sut = globalVerifierFactory.getVerifier(quorumValidatorId); + + const monitor: IVerifierEventListener = { + onEvent(ledgerEvent: LedgerEvent): void { + try { + log.info("Received event:", ledgerEvent); + + if (!ledgerEvent.data) { + throw Error("No block data"); + } + + log.info( + "Listener received ledgerEvent, block number", + ledgerEvent.data.blockHeader?.number, + ); + + sut.stopMonitor(appId); + resolve(ledgerEvent); + } catch (err) { + reject(err); + } + }, + onError(err: any): void { + log.error("Ledger monitoring error:", err); + reject(err); + }, + }; + + sut.startMonitor(appId, options, monitor); + }, + ); + } + + ////////////////////////////////// + // Tests + ////////////////////////////////// + + test("Verifier of QuorumApiClient is created by VerifierFactory", () => { + const sut = globalVerifierFactory.getVerifier(quorumValidatorId); + expect(sut.ledgerApi.className).toEqual("QuorumApiClient"); + }); + + describe("web3EthContract tests", () => { + let verifier: Verifier; + let contractCommon: { + abi: AbiItem[]; + address: string; + }; + + beforeAll(async () => { + // Setup verifier + verifier = globalVerifierFactory.getVerifier( + quorumValidatorId, + "QUORUM_2X", + ); + + // Deploy contract to interact with + const deployOut = await connector.deployContract({ + contractName: HelloWorldContractJson.contractName, + keychainId: keychainPlugin.getKeychainId(), + web3SigningCredential: { + ethAccount: connectionProfile.quorum.member2.accountAddress, + secret: connectionProfile.quorum.member2.privateKey, + type: Web3SigningCredentialType.PrivateKeyHex, + }, + gas: 1000000, + }); + expect(deployOut).toBeTruthy(); + expect(deployOut.transactionReceipt).toBeTruthy(); + expect(deployOut.transactionReceipt.contractAddress).toBeTruthy(); + expect(deployOut.transactionReceipt.status).toBeTrue(); + + contractCommon = { + abi: HelloWorldContractJson.abi as AbiItem[], + address: deployOut.transactionReceipt.contractAddress as string, + }; + }); + + test("Invalid web3EthContract calls are rejected by QuorumApiClient", async () => { + // Define correct input parameters + const correctContract: Record = lodash.clone( + contractCommon, + ); + const correctMethod: Record = { + type: "web3EthContract", + command: "call", + function: "getName", + params: [], + }; + const correctArgs: any = {}; + + // Sanity check if correct parameters work + const resultCorrect = await verifier.sendSyncRequest( + correctContract, + correctMethod, + correctArgs, + ); + expect(resultCorrect.status).toEqual(200); + + // Failing: Missing contract ABI + const missingABIContract = lodash.clone(correctContract); + delete missingABIContract.abi; + + expect( + verifier.sendSyncRequest( + missingABIContract, + correctMethod, + correctArgs, + ), + ).toReject(); + + // Failing: Missing contract address + const missingAddressContract = lodash.clone(correctContract); + delete missingAddressContract.address; + + expect( + verifier.sendSyncRequest( + missingAddressContract, + correctMethod, + correctArgs, + ), + ).toReject(); + + // Failing: Unknown invocation method + const unknownMethod = lodash.clone(correctMethod); + unknownMethod.command = "foo"; + expect( + verifier.sendSyncRequest(correctContract, unknownMethod, correctArgs), + ).toReject(); + + // Failing: Empty invocation method + const emptyMethod = lodash.clone(correctMethod); + emptyMethod.command = ""; + expect( + verifier.sendSyncRequest(correctContract, emptyMethod, correctArgs), + ).toReject(); + + // Failing: Empty contract method + const emptyContractFunction = lodash.clone(correctMethod); + emptyContractFunction.function = ""; + expect( + verifier.sendSyncRequest( + correctContract, + emptyContractFunction, + correctArgs, + ), + ).toReject(); + + // Failing: Wrong method params format + const numericParam = lodash.clone(correctMethod); + numericParam.params = 42; + expect( + verifier.sendSyncRequest(correctContract, numericParam, correctArgs), + ).toReject(); + + const objectParam = lodash.clone(correctMethod); + objectParam.params = { arg1: 42 }; + expect( + verifier.sendSyncRequest(correctContract, objectParam, correctArgs), + ).toReject(); + }); + + test("Send unsigned transaction and use call to check results works", async () => { + const newName = "QuorumCactus"; + + // 1. Set new value (send) + // Will use signing key of the node we're connected to (member1) + const methodSend = { + type: "web3EthContract", + command: "send", + function: "setName", + params: [newName], + }; + const argsSend = { + args: { + from: connectionProfile.quorum.member1.accountAddress, + }, + }; + + const resultsSend = await verifier.sendSyncRequest( + contractCommon, + methodSend, + argsSend, + ); + expect(resultsSend.status).toEqual(200); + expect(resultsSend.data.status).toBeTrue(); + + // 2. Get new, updated value (call) + const methodCall = { + type: "web3EthContract", + command: "call", + function: "getName", + params: [], + }; + const argsCall = {}; + + const resultCall = await verifier.sendSyncRequest( + contractCommon, + methodCall, + argsCall, + ); + expect(resultCall.status).toEqual(200); + expect(resultCall.data).toEqual(newName); + }); + + test("encodeABI of transactions gives same results as direct web3 call", async () => { + // Send encodeABI request to connector + const methodEncode = { + type: "web3EthContract", + command: "encodeABI", + function: "setName", + params: ["QuorumCactusEncode"], + }; + const argsEncode = { + args: { + from: connectionProfile.quorum.member1.accountAddress, + }, + }; + + const resultsEncode = await verifier.sendSyncRequest( + contractCommon, + methodEncode, + argsEncode, + ); + expect(resultsEncode.status).toEqual(200); + expect(resultsEncode.data.length).toBeGreaterThan(5); + + // Compare encoded data with direct web3 call + const web3Contract = new web3.eth.Contract( + contractCommon.abi, + contractCommon.address, + ); + const web3Encode = await web3Contract.methods + .setName(...methodEncode.params) + .encodeABI(argsEncode); + expect(resultsEncode.data).toEqual(web3Encode); + }); + + test("estimateGas of transactions gives same results as direct web3 call", async () => { + // Send estimateGas request to connector + const methodEstimateGas = { + type: "web3EthContract", + command: "estimateGas", + function: "setName", + params: ["QuorumCactusGas"], + }; + const argsEstimateGas = {}; + + const resultsEstimateGas = await verifier.sendSyncRequest( + contractCommon, + methodEstimateGas, + argsEstimateGas, + ); + expect(resultsEstimateGas.status).toEqual(200); + expect(resultsEstimateGas.data).toBeGreaterThan(0); + + // Compare gas estimate with direct web3 call + const web3Contract = new web3.eth.Contract( + contractCommon.abi, + contractCommon.address, + ); + const web3Encode = await web3Contract.methods + .setName(...methodEstimateGas.params) + .estimateGas(argsEstimateGas); + expect(resultsEstimateGas.data).toEqual(web3Encode); + }); + + test("Sending transaction with sendAsyncRequest works", async () => { + const newName = "QuorumCactusAsync"; + + // 1. Set new value with async call (send) + // Will use signing key of the node we're connected to (member1) + const methodSendAsync = { + type: "web3EthContract", + command: "send", + function: "setName", + params: [newName], + }; + const argsSendAsync = { + args: { + from: connectionProfile.quorum.member1.accountAddress, + }, + }; + + await verifier.sendAsyncRequest( + contractCommon, + methodSendAsync, + argsSendAsync, + ); + + // 2. Wait for transaction commit + // We assume transaction will be included in the next block + await monitorAndGetBlock(); + + // 3. Get new, updated value (call) + const methodCall = { + type: "web3EthContract", + command: "call", + function: "getName", + params: [], + }; + const argsCall = {}; + + const resultsCall = await verifier.sendSyncRequest( + contractCommon, + methodCall, + argsCall, + ); + expect(resultsCall.status).toEqual(200); + expect(resultsCall.data).toEqual(newName); + }); + }); + + test("Verifier of QuorumApiClient supports web3Eth function", async () => { + // web3Eth.getBalance + const contract = {}; + const method = { type: "web3Eth", command: "getBalance" }; + const args = { args: [connectionProfile.quorum.member2.accountAddress] }; + + const results = await globalVerifierFactory + .getVerifier(quorumValidatorId) + .sendSyncRequest(contract, method, args); + expect(results.status).toEqual(200); + expect(results.data.length).toBeGreaterThan(0); + }); + + test("Invalid web3Eth calls are rejected by QuorumApiClient", async () => { + // Define correct input parameters + const correctContract = {}; + const correctMethod: Record = { + type: "web3Eth", + command: "getBalance", + }; + const correctArgs: any = { + args: [connectionProfile.quorum.member2.accountAddress], + }; + const verifier = globalVerifierFactory.getVerifier(quorumValidatorId); + + // Sanity check if correct parameters work + const resultCorrect = await verifier.sendSyncRequest( + correctContract, + correctMethod, + correctArgs, + ); + expect(resultCorrect.status).toEqual(200); + + // Failing: Empty web3.eth method + const emptyMethod = lodash.clone(correctMethod); + emptyMethod.command = ""; + + expect( + verifier.sendSyncRequest(correctContract, emptyMethod, correctArgs), + ).toReject(); + + // Failing: Wrong args format + const numericArgsFormat = lodash.clone(correctArgs); + numericArgsFormat.args = 42; + + expect( + verifier.sendSyncRequest(correctContract, numericArgsFormat, correctArgs), + ).toReject(); + + const objectArgsFormat = lodash.clone(correctArgs); + objectArgsFormat.args = { arg1: 42 }; + + expect( + verifier.sendSyncRequest(correctContract, objectArgsFormat, correctArgs), + ).toReject(); + }); + + test("QuorumApiClient web3Eth throws error on unknown method", async () => { + const contract = {}; + const method = { type: "web3Eth", command: "foo" }; + const args = {}; + + const results = await globalVerifierFactory + .getVerifier(quorumValidatorId) + .sendSyncRequest(contract, method, args); + + expect(results).toBeTruthy(); + expect(results.status).toEqual(504); + expect(results.errorDetail).toBeTruthy(); + }); + + function assertBlockHeader(header: Web3BlockHeader) { + // Check if defined and with expected type + // Ignore nullable / undefine-able fields + expect(typeof header.parentHash).toEqual("string"); + expect(typeof header.sha3Uncles).toEqual("string"); + expect(typeof header.miner).toEqual("string"); + expect(typeof header.stateRoot).toEqual("string"); + expect(typeof header.logsBloom).toEqual("string"); + expect(typeof header.number).toEqual("number"); + expect(typeof header.gasLimit).toEqual("number"); + expect(typeof header.gasUsed).toEqual("number"); + expect(typeof header.extraData).toEqual("string"); + expect(typeof header.nonce).toEqual("string"); + expect(typeof header.hash).toEqual("string"); + expect(typeof header.difficulty).toEqual("string"); + } + + test("Monitor new blocks headers on Quorum", async () => { + const ledgerEvent = await monitorAndGetBlock(); + // assert well-formed output + expect(ledgerEvent.id).toEqual(""); + expect(ledgerEvent.verifierId).toEqual(quorumValidatorId); + expect(ledgerEvent.data).toBeTruthy(); + + // blockData should not be present if called with empty options + expect(ledgerEvent.data?.blockData).toBeUndefined(); + expect(ledgerEvent.data?.blockHeader).toBeTruthy(); + + // check some fields + assertBlockHeader(ledgerEvent.data?.blockHeader as Web3BlockHeader); + }); + + test("Monitor new blocks data on Quorum", async () => { + const ledgerEvent = await monitorAndGetBlock({ getBlockData: true }); + // assert well-formed output + expect(ledgerEvent.id).toEqual(""); + expect(ledgerEvent.verifierId).toEqual(quorumValidatorId); + expect(ledgerEvent.data).toBeTruthy(); + + // blockHeader should not be present if called with getBlockData option + expect(ledgerEvent.data?.blockHeader).toBeFalsy(); + expect(ledgerEvent.data?.blockData).toBeTruthy(); + + // check some fields + assertBlockHeader(ledgerEvent.data?.blockData as Web3BlockHeader); + expect(typeof ledgerEvent.data?.blockData?.size).toEqual("number"); + expect(typeof ledgerEvent.data?.blockData?.totalDifficulty).toEqual( + "string", + ); + expect(typeof ledgerEvent.data?.blockData?.uncles).toEqual("object"); + expect(typeof ledgerEvent.data?.blockData?.transactions).toEqual("object"); + }); +}); diff --git a/packages/cactus-verifier-client/package.json b/packages/cactus-verifier-client/package.json index 7ca3d29de21..b076aa1abc5 100644 --- a/packages/cactus-verifier-client/package.json +++ b/packages/cactus-verifier-client/package.json @@ -53,6 +53,7 @@ "@hyperledger/cactus-common": "1.0.0", "@hyperledger/cactus-core-api": "1.0.0", "@hyperledger/cactus-plugin-ledger-connector-besu": "1.0.0", + "@hyperledger/cactus-plugin-ledger-connector-quorum": "1.0.0", "jest-extended": "0.11.5", "rxjs": "7.3.0" } diff --git a/packages/cactus-verifier-client/src/main/typescript/get-validator-api-client.ts b/packages/cactus-verifier-client/src/main/typescript/get-validator-api-client.ts index 9cf062d10aa..ac346b4c3db 100644 --- a/packages/cactus-verifier-client/src/main/typescript/get-validator-api-client.ts +++ b/packages/cactus-verifier-client/src/main/typescript/get-validator-api-client.ts @@ -15,6 +15,11 @@ import { BesuApiClientOptions, } from "@hyperledger/cactus-plugin-ledger-connector-besu"; +import { + QuorumApiClient, + QuorumApiClientOptions, +} from "@hyperledger/cactus-plugin-ledger-connector-quorum"; + /** * Configuration of ApiClients currently supported by Verifier and VerifierFactory * Each entry key defines the name of the connection type that has to be specified in VerifierFactory config. @@ -34,6 +39,10 @@ export type ClientApiConfig = { in: BesuApiClientOptions; out: BesuApiClient; }; + QUORUM_2X: { + in: QuorumApiClientOptions; + out: QuorumApiClient; + }; }; /** @@ -55,6 +64,8 @@ export function getValidatorApiClient( case "BESU_1X": case "BESU_2X": return new BesuApiClient(options as BesuApiClientOptions); + case "QUORUM_2X": + return new QuorumApiClient(options as QuorumApiClientOptions); default: // Will not compile if any ClientApiConfig key was not handled by this switch const _: never = validatorType; diff --git a/yarn.lock b/yarn.lock index becd6dd165a..b0896214c99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4635,6 +4635,13 @@ resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065" integrity sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g== +"@types/sanitize-html@2.6.2": + version "2.6.2" + resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.6.2.tgz#9c47960841b9def1e4c9dfebaaab010a3f6e97b9" + integrity sha512-7Lu2zMQnmHHQGKXVvCOhSziQMpa+R2hMHFefzbYoYMHeaXR0uXqNeOc3JeQQQ8/6Xa2Br/P1IQTLzV09xxAiUQ== + dependencies: + htmlparser2 "^6.0.0" + "@types/scheduler@*": version "0.16.2" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" @@ -12347,7 +12354,7 @@ htmlparser2@^3.9.1: inherits "^2.0.1" readable-stream "^3.1.1" -htmlparser2@^6.1.0: +htmlparser2@^6.0.0, htmlparser2@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== @@ -17684,6 +17691,11 @@ parse-path@^4.0.0: qs "^6.9.4" query-string "^6.13.8" +parse-srcset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" + integrity sha1-8r0iH2zJcKk42IVWq8WJyqqiveE= + parse-url@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-6.0.0.tgz#f5dd262a7de9ec00914939220410b66cff09107d" @@ -18523,6 +18535,15 @@ postcss@^8.2.15, postcss@^8.3.5, postcss@^8.3.7: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.3.11: + version "8.4.12" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.12.tgz#1e7de78733b28970fa4743f7da6f3763648b1905" + integrity sha512-lg6eITwYe9v6Hr5CncVbK70SoioNQIq81nsaG86ev5hAidQvmOeETBqs7jm43K2F5/Ley3ytDtriImV6TpNiSg== + dependencies: + nanoid "^3.3.1" + picocolors "^1.0.0" + source-map-js "^1.0.2" + prebuild-install@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.0.1.tgz#c10075727c318efe72412f333e0ef625beaf3870" @@ -19846,6 +19867,18 @@ sanitize-filename@1.6.3: dependencies: truncate-utf8-bytes "^1.0.0" +sanitize-html@2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.7.0.tgz#e106205b468aca932e2f9baf241f24660d34e279" + integrity sha512-jfQelabOn5voO7FAfnQF7v+jsA6z9zC/O4ec0z3E35XPEtHYJT/OdUziVWlKW4irCr2kXaQAyXTXDHWAibg1tA== + dependencies: + deepmerge "^4.2.2" + escape-string-regexp "^4.0.0" + htmlparser2 "^6.0.0" + is-plain-object "^5.0.0" + parse-srcset "^1.0.2" + postcss "^8.3.11" + sass-loader@12.1.0: version "12.1.0" resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-12.1.0.tgz#b73324622231009da6fba61ab76013256380d201"