From 7ed88c54fa7e83cd752bc61a31730d5568a0fd76 Mon Sep 17 00:00:00 2001 From: Michal Bajer Date: Thu, 17 Mar 2022 18:45:51 +0100 Subject: [PATCH] feat(quorum-connector): implement validator interface on go-quorum-connector Add three new endpoints to quorum ledger connector achieve legacy verifier compatibility. InvokeRawWeb3EthContractEndpoint can be used to form any call to deployed contract. InvokeRawWeb3EthMethodEndpoint can be used to call any web3.eth function. Both are marked as low-level functions, should be used only when there's no designated endpoint for given functionality yet. WatchBlocksV1Endpoint can be used to monitor new block headers / data from the ledger. Type of the output is determined from input option flag. Extend QuorumApiClient to support Verifier interface, that is: block monitoring, and sending sync/async requests. Sending requests is marked as deprecated, because user can use direct REST calls from generated ApiClient, nevertheless this API was requested by one of the teams. Added functional tests for two new, request based endpoints. Moved verifier-besu integration test to besu-test package. Added verifier-quorum integration test, it supplements direct endpoint tests and provides a reference for API usage. Added support for QuorumApiClient in Verifier. Closes: #1604 Signed-off-by: Michal Bajer --- .../Dockerfile | 2 +- .../README.md | 84 ++- .../package.json | 6 +- .../src/main/json/openapi.json | 456 ++++++++++++- .../api-client/quorum-api-client.ts | 253 +++++++ .../generated/openapi/typescript-axios/api.ts | 619 ++++++++++++++++- .../plugin-ledger-connector-quorum.ts | 182 ++++- .../src/main/typescript/public-api.ts | 12 + ...invoke-raw-web3eth-contract-v1-endpoint.ts | 109 +++ .../invoke-raw-web3eth-method-v1-endpoint.ts | 107 +++ .../web-services/watch-blocks-v1-endpoint.ts | 103 +++ .../openapi-validation-no-keychain.test.ts | 9 +- .../openapi/openapi-validation.test.ts | 9 +- ...ct-from-json-json-object-endpoints.test.ts | 9 +- ...loy-contract-from-json-json-object.test.ts | 9 +- .../v2.3.0-deploy-contract-from-json.test.ts | 9 +- ...oke-contract-json-object-endpoints.test.ts | 9 +- ...ct-from-json-json-object-endpoints.test.ts | 9 +- ...loy-contract-from-json-json-object.test.ts | 9 +- .../v21.4.1-deploy-contract-from-json.test.ts | 8 +- ...oke-contract-json-object-endpoints.test.ts | 9 +- .../v21.4.1-invoke-web3-contract-v1.test.ts | 203 ++++++ .../v21.4.1-invoke-web3-method-v1.test.ts | 157 +++++ packages/cactus-test-api-client/package.json | 2 - .../package.json | 1 + ...r-integration-with-besu-connector.test.ts} | 7 +- .../package.json | 1 + ...-integration-with-quorum-connector.test.ts | 631 ++++++++++++++++++ packages/cactus-verifier-client/package.json | 1 + .../typescript/get-validator-api-client.ts | 11 + yarn.lock | 35 +- 31 files changed, 2998 insertions(+), 73 deletions(-) create mode 100644 packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/api-client/quorum-api-client.ts create mode 100644 packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/invoke-raw-web3eth-contract-v1-endpoint.ts create mode 100644 packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/invoke-raw-web3eth-method-v1-endpoint.ts create mode 100644 packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/web-services/watch-blocks-v1-endpoint.ts create mode 100644 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 create mode 100644 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 rename packages/{cactus-test-api-client/src/test/typescript/integration/verifier-integration-with-openapi-connectors.test.ts => cactus-test-plugin-ledger-connector-besu/src/test/typescript/integration/api-client/verifier-integration-with-besu-connector.test.ts} (97%) create mode 100644 packages/cactus-test-plugin-ledger-connector-quorum/src/test/typescript/integration/api-client/verifier-integration-with-quorum-connector.test.ts diff --git a/packages/cactus-plugin-ledger-connector-quorum/Dockerfile b/packages/cactus-plugin-ledger-connector-quorum/Dockerfile index 6e57cabb00..65ef7de563 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 bce6236eed..bd3de5d01d 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 048bf10e07..ac1bb5ca8d 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 1b2fcd29e3..20c9afb029 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 @@ -147,8 +147,10 @@ "EthContractInvocationType": { "type": "string", "enum": [ - "SEND", - "CALL" + "send", + "call", + "encodeABI", + "estimateGas" ] }, "SolidityContractJsonArtifact": { @@ -507,7 +509,7 @@ } } }, - "DeployContractSolidityBytecodeJsonObjectV1Request": { + "DeployContractSolidityBytecodeJsonObjectV1Request": { "type": "object", "required": [ "web3SigningCredential", @@ -534,7 +536,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 +715,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 +738,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/EthContractInvocationType" + }, + "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 +1315,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 0000000000..2647c61c18 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-quorum/src/main/typescript/api-client/quorum-api-client.ts @@ -0,0 +1,253 @@ +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, + EthContractInvocationType, + 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: EthContractInvocationType; + 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(EthContractInvocationType).includes(method.command)) { + throw new Error( + `Unknown invocationType (${method.command}), must be specified in EthContractInvocationType`, + ); + } + 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 08ce366503..336bc6a494 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 @@ -226,8 +226,10 @@ export interface DeployContractSolidityBytecodeV1Response { */ export enum EthContractInvocationType { - Send = 'SEND', - Call = 'CALL' + Send = 'send', + Call = 'call', + EncodeAbi = 'encodeABI', + EstimateGas = 'estimateGas' } /** @@ -401,6 +403,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 {EthContractInvocationType} + * @memberof InvokeRawWeb3EthContractV1Request + */ + invocationType: EthContractInvocationType; + /** + * 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 +677,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 +1066,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 +1393,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 +1559,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 +1651,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 +1750,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 72e30bacfb..f2c81a3951 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"; @@ -50,23 +52,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 +103,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 +122,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 +156,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 +233,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 +301,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 +329,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 +350,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 +372,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 +710,80 @@ 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 { + return new Promise((resolve, rejects) => { + 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" + ) { + const web3Method = looseWeb3Eth[args.methodName]; + + const web3Response: Promise = args.params + ? web3Method(...args.params) + : web3Method(); + + web3Response + .then((result: any) => { + this.log.debug("invokeRawWeb3EthMethod response (OK):", result); + return resolve(result); + }) + .catch((err) => { + this.log.warn("invokeRawWeb3EthMethod response (ERROR):", err); + rejects(err); + }); + } else { + rejects(new Error(`No method "${args.methodName}" in web3.eth`)); + } + }); + } + + // 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(EthContractInvocationType).includes(args.invocationType) + ) { + throw new Error( + `Unknown invocationType (${args.invocationType}), must be specified in EthContractInvocationType`, + ); + } + + 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 8fe5b14280..5fbef1b732 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 0000000000..f4c3b96a47 --- /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 0000000000..8206b2945a --- /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 0000000000..c56f76ebbb --- /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 ef2e7f8a8b..ec230d14b3 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 eaadd41a22..90423c62eb 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 be6603ef90..f6df95596d 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 9e69dcbe5b..a6c3b1226f 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 29771a10ef..09122401d0 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 351e9859a9..b67e7e2a4a 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 30ab5bc092..0302fdd6be 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 31e6da46ed..d6617e3ae4 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 7ad0543b9f..69edb85e67 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 1a6869466d..a8db04228c 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 0000000000..daa87133a9 --- /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 { + EthContractInvocationType, + 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: EthContractInvocationType.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: EthContractInvocationType.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: EthContractInvocationType.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 EthContractInvocationType, + 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: EthContractInvocationType.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 0000000000..5b897746fa --- /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 4b6636c81d..a909120197 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 47bf5fecb1..63785c5980 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", "web3-eea": "0.11.0" 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 6b8a1a4d59..d59459f52a 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 09090924ae..be8bdae3f5 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 0000000000..3d3a5fe14b --- /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"; + +// @todo Replace with cactus ghcr when this image will be available +const containerImageName = "ghcr.io/outsh/cactus-quorum-multi-party-all-in-one"; +const containerImageVersion = "2022-03-10-b298004"; + +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 7ca3d29de2..b076aa1abc 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 9cf062d10a..ac346b4c3d 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 6c73807edd..35ab023344 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4630,6 +4630,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" @@ -12342,7 +12349,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== @@ -17689,6 +17696,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" @@ -18528,6 +18540,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" @@ -19851,6 +19872,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"