From 599205aaade80eac491038dc4badf60bf1667cf4 Mon Sep 17 00:00:00 2001 From: Michal Bajer Date: Fri, 27 May 2022 13:20:12 +0000 Subject: [PATCH 1/2] refactor(connector-go-ethereum-socketio): fix strict flag warnings cactus-plugin-ledger-connector-go-ethereum-socketio will compile with global strict flag. Related issue: #1671 Signed-off-by: Michal Bajer --- .../main/typescript/common/core/bin/www.ts | 44 ++++++++++--------- .../main/typescript/connector/PluginUtil.ts | 2 +- .../connector/ServerMonitorPlugin.ts | 33 +++++++------- .../main/typescript/connector/ServerPlugin.ts | 32 +++++++------- .../tsconfig.json | 1 - 5 files changed, 56 insertions(+), 56 deletions(-) diff --git a/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/src/main/typescript/common/core/bin/www.ts b/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/src/main/typescript/common/core/bin/www.ts index 52444e2a5be..08df7236dc6 100644 --- a/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/src/main/typescript/common/core/bin/www.ts +++ b/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/src/main/typescript/common/core/bin/www.ts @@ -69,7 +69,7 @@ server.on("listening", onListening); * Normalize a port into a number, string, or false. */ -function normalizePort(val) { +function normalizePort(val: string) { const port = parseInt(val, 10); if (isNaN(port)) { @@ -89,7 +89,7 @@ function normalizePort(val) { * Event listener for HTTPS server "error" event. */ -function onError(error) { +function onError(error: any) { if (error.syscall !== "listen") { throw error; } @@ -118,6 +118,12 @@ function onError(error) { function onListening() { const addr = server.address(); + + if (!addr) { + logger.error("Could not get running server address - exit."); + process.exit(1); + } + const bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port; debug("Listening on " + bind); } @@ -144,14 +150,14 @@ io.on("connection", function (client) { // Check for the existence of the specified function and call it if it exists. if (Splug.isExistFunction(func)) { // Can be called with Server plugin function name. - Splug[func](args) - .then((respObj) => { + (Splug as any)[func](args) + .then((respObj: unknown) => { logger.info("*** RESPONSE ***"); logger.info("Client ID :" + client.id); logger.info("Response :" + JSON.stringify(respObj)); client.emit("response", respObj); }) - .catch((errObj) => { + .catch((errObj: unknown) => { logger.error("*** ERROR ***"); logger.error("Client ID :" + client.id); logger.error("Detail :" + JSON.stringify(errObj)); @@ -172,11 +178,12 @@ io.on("connection", function (client) { // TODO: "request2" -> "request" client.on("request2", function (data) { const methodType = data.method.type; - // const args = data.args; - const args = {}; - args["contract"] = data.contract; - args["method"] = data.method; - args["args"] = data.args; + let args: Record = { + contract: data.contract, + method: data.method, + args: data.args, + }; + if (data.reqID !== undefined) { logger.info(`##add reqID: ${data.reqID}`); args["reqID"] = data.reqID; @@ -223,14 +230,14 @@ io.on("connection", function (client) { // Check for the existence of the specified function and call it if it exists. if (Splug.isExistFunction(func)) { // Can be called with Server plugin function name. - Splug[func](args) - .then((respObj) => { + (Splug as any)[func](args) + .then((respObj: unknown) => { logger.info("*** RESPONSE ***"); logger.info("Client ID :" + client.id); logger.info("Response :" + JSON.stringify(respObj)); client.emit("response", respObj); }) - .catch((errObj) => { + .catch((errObj: unknown) => { logger.error("*** ERROR ***"); logger.error("Client ID :" + client.id); logger.error("Detail :" + JSON.stringify(errObj)); @@ -262,19 +269,16 @@ io.on("connection", function (client) { * startMonitor: starting block generation event monitoring **/ client.on("startMonitor", function () { - // Callback to receive monitoring results - const cb = function (callbackData) { + Smonitor.startMonitor(client.id, (event) => { let emitType = ""; - if (callbackData.status == 200) { + if (event.status == 200) { emitType = "eventReceived"; logger.info("event data callbacked."); } else { emitType = "monitor_error"; } - client.emit(emitType, callbackData); - }; - - Smonitor.startMonitor(client.id, cb); + client.emit(emitType, event); + }); }); /** diff --git a/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/src/main/typescript/connector/PluginUtil.ts b/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/src/main/typescript/connector/PluginUtil.ts index e2a554be11d..f9b322ce669 100644 --- a/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/src/main/typescript/connector/PluginUtil.ts +++ b/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/src/main/typescript/connector/PluginUtil.ts @@ -23,7 +23,7 @@ * 3.78*10^14 * 3.78e14 */ -export function convNum(value, defaultValue) { +export function convNum(value: number | string, defaultValue: number | string) { let retValue = 0; let defValue = 0; diff --git a/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/src/main/typescript/connector/ServerMonitorPlugin.ts b/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/src/main/typescript/connector/ServerMonitorPlugin.ts index 51d383ae957..5684c539f97 100644 --- a/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/src/main/typescript/connector/ServerMonitorPlugin.ts +++ b/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/src/main/typescript/connector/ServerMonitorPlugin.ts @@ -25,21 +25,18 @@ import { ValidatorAuthentication } from "./ValidatorAuthentication"; const Web3 = require("web3"); import safeStringify from "fast-safe-stringify"; +export type MonitorCallback = (callback: { + status: number; + blockData?: string; + errorDetail?: string; +}) => void; + /* * ServerMonitorPlugin * Class definitions of server monitoring */ export class ServerMonitorPlugin { - _filterTable: object; - - /* - * constructors - */ - constructor() { - // Define dependent specific settings - // Initialize monitored filter - this._filterTable = {}; - } + _filterTable = new Map(); /* * startMonitor @@ -47,11 +44,11 @@ export class ServerMonitorPlugin { * @param {string} clientId: Client ID from which monitoring start request was made * @param {function} cb: A callback function that receives monitoring results at any time. */ - startMonitor(clientId, cb) { + startMonitor(clientId: string, cb: MonitorCallback) { logger.info("*** START MONITOR ***"); logger.info("Client ID :" + clientId); // Implement handling to receive events from an end-chain and return them in a callback function - let filter = this._filterTable[clientId]; + let filter = this._filterTable.get(clientId); if (!filter) { logger.info("create new web3 filter and start watching."); try { @@ -63,8 +60,8 @@ export class ServerMonitorPlugin { filter = web3.eth.filter("latest"); // filter should be managed by client ID // (You should never watch multiple urls from the same client) - this._filterTable[clientId] = filter; - filter.watch(function (error, blockHash) { + this._filterTable.set(clientId, filter); + filter.watch(function (error: any, blockHash: string) { if (!error) { console.log("##[HL-BC] Notify new block data(D2)"); const blockData = web3.eth.getBlock(blockHash, true); @@ -92,7 +89,7 @@ export class ServerMonitorPlugin { } else { const errObj = { status: 504, - errorDetail: error, + errorDetail: safeStringify(error), }; cb(errObj); } @@ -115,14 +112,14 @@ export class ServerMonitorPlugin { * monitoring stop * @param {string} clientId: Client ID from which monitoring stop request was made */ - stopMonitor(clientId) { + stopMonitor(clientId: string) { // Implement a process to end EC monitoring - const filter = this._filterTable[clientId]; + let filter = this._filterTable.get(clientId); if (filter) { // Stop the filter & Remove it from table logger.info("stop watching and remove filter."); filter.stopWatching(); - delete this._filterTable[clientId]; + this._filterTable.delete(clientId); } } } diff --git a/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/src/main/typescript/connector/ServerPlugin.ts b/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/src/main/typescript/connector/ServerPlugin.ts index bfe8b4c34c9..13daf07fc00 100644 --- a/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/src/main/typescript/connector/ServerPlugin.ts +++ b/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/src/main/typescript/connector/ServerPlugin.ts @@ -47,8 +47,8 @@ export class ServerPlugin { * Scope of this function is in this class * Functions that should not be called directly should be implemented outside this class like utilities. */ - isExistFunction(funcName) { - if (this[funcName] != undefined) { + isExistFunction(funcName: string) { + if ((this as any)[funcName]) { return true; } else { return false; @@ -67,11 +67,11 @@ export class ServerPlugin { * } * @return {Object} JSON object */ - getNumericBalance(args) { + getNumericBalance(args: any) { // * The Web3 API can be used synchronously, but each function is always an asynchronous specification because of the use of other APIs such as REST, return new Promise((resolve, reject) => { logger.info("getNumericBalance start"); - let retObj = {}; + let retObj: Record; const referedAddress = args.args.args[0]; const reqID = args["reqID"]; @@ -139,11 +139,11 @@ export class ServerPlugin { * } * @return {Object} JSON object */ - transferNumericAsset(args) { + transferNumericAsset(args: any) { return new Promise((resolve, reject) => { logger.info("transferNumericAsset start"); - let retObj = {}; + let retObj: Record; let sendArgs = {}; const sendFunction = "sendTransaction"; // const funcParam = args; @@ -231,11 +231,11 @@ export class ServerPlugin { * } * @return {Object} JSON object */ - getNonce(args) { + getNonce(args: any) { // * The Web3 API can be used synchronously, but each function is always an asynchronous specification because of the use of other APIs such as REST, return new Promise((resolve, reject) => { logger.info("getNonce start"); - let retObj = {}; + let retObj: Record; const targetAddress = args.args.args.args[0]; const reqID = args["reqID"]; @@ -316,11 +316,11 @@ export class ServerPlugin { * } * @return {Object} JSON object */ - toHex(args) { + toHex(args: any) { // * The Web3 API can be used synchronously, but each function is always an asynchronous specification because of the use of other APIs such as REST, return new Promise((resolve, reject) => { logger.info("toHex start"); - let retObj = {}; + let retObj: Record; const targetValue = args.args.args.args[0]; const reqID = args["reqID"]; @@ -393,11 +393,11 @@ export class ServerPlugin { * } * @return {Object} JSON object */ - sendRawTransaction(args) { + sendRawTransaction(args: any) { return new Promise((resolve, reject) => { logger.info("sendRawTransaction(start"); - let retObj = {}; + let retObj: Record; const sendArgs = {}; const sendFunction = "sendTransaction"; const funcParam = args.args.args[0]; @@ -451,11 +451,11 @@ export class ServerPlugin { * } * @return {Object} JSON object */ - web3Eth(args) { + web3Eth(args: any) { return new Promise((resolve, reject) => { logger.info("web3Eth start"); - let retObj = {}; + let retObj: Record; const sendFunction = args.method.command; const sendArgs = args.args.args[0]; const reqID = args["reqID"]; @@ -518,11 +518,11 @@ export class ServerPlugin { * } * @return {Object} JSON object */ - contract(args) { + contract(args: any) { return new Promise((resolve, reject) => { logger.info("contract start"); - let retObj = {}; + let retObj: Record; const sendCommand = args.method.command; const sendFunction = args.method.function; const sendArgs = args.args.args; diff --git a/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/tsconfig.json b/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/tsconfig.json index 7713871089a..90eae72ba88 100644 --- a/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/tsconfig.json +++ b/packages/cactus-plugin-ledger-connector-go-ethereum-socketio/tsconfig.json @@ -8,7 +8,6 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "tsBuildInfoFile": "../../.build-cache/cactus-plugin-ledger-connector-go-ethereum-socketio.tsbuildinfo", - "strict": false // TODO - True, fix warnings }, "include": [ "./src/main/typescript/common/core/*.ts", From da94cd6b4fc5a364761716374ec7f6e7021bc76b Mon Sep 17 00:00:00 2001 From: Michal Bajer Date: Wed, 29 Jun 2022 08:40:07 +0000 Subject: [PATCH 2/2] feat(connector-iroha): sending transactions signed on the client-side - Add new endpoint `generate-transaction`, to create unsigned transactions that can be signed on the client side. - Add a function to iroha-connector package to help signing iroha transactions on the client (BLP) side. - Extend transact endpoint to accept signed transaction as an argument as well. New transact interface is backward compatible, all current code should work without any change. - Add new test suite to check features implemented in this PR (i.e. signing on the client side). - Perform minor cleanup in the connector code, remove unused fields and includes, fix some type related warnings. - Perform openapi interface cleanup, format json correctly, mark baseConfig as required by transact (will fail otherwise), add error return type schemas, remove invoke-contract endpoint that was not implemented. Closes 2077 Signed-off-by: Michal Bajer --- .../package.json | 5 +- .../src/main/json/openapi.json | 200 +++++++-- .../generated/openapi/typescript-axios/api.ts | 190 +++++--- .../src/main/typescript/iroha-sign-utils.ts | 39 ++ .../plugin-ledger-connector-iroha.ts | 242 ++++++++--- .../src/main/typescript/public-api.ts | 3 + .../generate-transaction-endpoint.ts | 131 ++++++ ...nerate-and-send-signed-transaction.test.ts | 405 ++++++++++++++++++ 8 files changed, 1069 insertions(+), 146 deletions(-) create mode 100644 packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/iroha-sign-utils.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/web-services/generate-transaction-endpoint.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha/src/test/typescript/integration/generate-and-send-signed-transaction.test.ts diff --git a/packages/cactus-plugin-ledger-connector-iroha/package.json b/packages/cactus-plugin-ledger-connector-iroha/package.json index da98a6e4956..0d3a327346d 100644 --- a/packages/cactus-plugin-ledger-connector-iroha/package.json +++ b/packages/cactus-plugin-ledger-connector-iroha/package.json @@ -62,6 +62,8 @@ "express": "4.17.1", "grpc": "1.24.11", "iroha-helpers-ts": "0.9.25-ss", + "fast-safe-stringify": "2.1.1", + "sanitize-html": "2.7.0", "openapi-types": "7.0.1", "prom-client": "13.1.0", "typescript-optional": "2.0.1" @@ -69,7 +71,8 @@ "devDependencies": { "@hyperledger/cactus-plugin-keychain-memory": "1.0.0", "@hyperledger/cactus-test-tooling": "1.0.0", - "@types/express": "4.17.8" + "@types/express": "4.17.8", + "@types/sanitize-html": "2.6.2" }, "engines": { "node": ">=10", diff --git a/packages/cactus-plugin-ledger-connector-iroha/src/main/json/openapi.json b/packages/cactus-plugin-ledger-connector-iroha/src/main/json/openapi.json index 071da984b85..f62dbe672e7 100644 --- a/packages/cactus-plugin-ledger-connector-iroha/src/main/json/openapi.json +++ b/packages/cactus-plugin-ledger-connector-iroha/src/main/json/openapi.json @@ -136,7 +136,10 @@ }, "KeyPair": { "type": "object", - "required": ["publicKey", "privateKey"], + "required": [ + "publicKey", + "privateKey" + ], "properties": { "publicKey": { "description": "SHA-3 ed25519 public keys of length 64 are recommended.", @@ -154,7 +157,11 @@ }, "RunTransactionRequestV1": { "type": "object", - "required": ["commandName", "params"], + "required": [ + "commandName", + "baseConfig", + "params" + ], "additionalProperties": false, "properties": { "commandName": { @@ -172,6 +179,54 @@ } } }, + "RunTransactionSignedRequestV1": { + "type": "object", + "required": [ + "signedTransaction" + ], + "properties": { + "signedTransaction": { + "description": "Signed transaction binary data received from generate-transaction endpoint.", + "type": "string", + "format": "binary" + }, + "baseConfig": { + "$ref": "#/components/schemas/IrohaBaseConfig", + "nullable": false + } + } + }, + "GenerateTransactionRequestV1": { + "type": "object", + "required": [ + "commandName", + "commandParams", + "creatorAccountId" + ], + "additionalProperties": false, + "properties": { + "commandName": { + "description": "Iroha command name.", + "type": "IrohaCommand", + "nullable": false + }, + "commandParams": { + "description": "Parameters for iroha command specified in commandName", + "type": "object" + }, + "creatorAccountId": { + "description": "Sender account id", + "type": "string", + "nullable": false + }, + "quorum": { + "description": "Requested transaction quorum", + "type": "number", + "nullable": false, + "default": 1 + } + } + }, "IrohaBaseConfig": { "type": "object", "additionalProperties": true, @@ -211,26 +266,61 @@ }, "RunTransactionResponse": { "type": "object", - "required": ["transactionReceipt"], + "required": [ + "transactionReceipt" + ], "properties": { "transactionReceipt": {} } }, - "InvokeContractV1Request": { + "PrometheusExporterMetricsResponse": { + "type": "string", + "nullable": false + }, + "ErrorExceptionJsonResponseV1": { "type": "object", - "additionalProperties": false, + "required": [ + "message" + ], "properties": { - "contractName": {} + "message": { + "type": "string", + "nullable": false + }, + "name": { + "type": "string", + "nullable": false + }, + "error": { + "type": "string", + "nullable": false + }, + "stack": { + "type": "string", + "nullable": false + }, + "cause": { + "type": "string", + "nullable": false + } } }, - "InvokeContractV1Response": { + "ErrorExceptionResponseV1": { "type": "object", - "required": ["success"], - "properties": {} - }, - "PrometheusExporterMetricsResponse": { - "type": "string", - "nullable": false + "required": [ + "message", + "error" + ], + "properties": { + "message": { + "type": "string", + "nullable": false + }, + "error": { + "type": "string", + "nullable": false + } + } } } }, @@ -250,7 +340,14 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RunTransactionRequestV1" + "oneOf": [ + { + "$ref": "#/components/schemas/RunTransactionRequestV1" + }, + { + "$ref": "#/components/schemas/RunTransactionSignedRequestV1" + } + ] } } } @@ -265,45 +362,88 @@ } } } + }, + "405": { + "description": "Method Not Allowed error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExceptionJsonResponseV1" + } + } + } + }, + "400": { + "description": "Bad Request error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExceptionJsonResponseV1" + } + } + } + }, + "500": { + "description": "Internal Server Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExceptionJsonResponseV1" + } + } + } } } } }, - "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha/invoke-contract": { + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha/generate-transaction": { "post": { "x-hyperledger-cactus": { "http": { "verbLowerCase": "post", - "path": "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha/invoke-contract" + "path": "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha/generate-transaction" } }, - "operationId": "invokeContractV1", - "summary": "Invokes a contract on a Iroha ledger", + "operationId": "generateTransactionV1", + "summary": "Generate transaction that can be signed locally.", "parameters": [], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/InvokeContractV1Request" + "$ref": "#/components/schemas/GenerateTransactionRequestV1" } } } }, "responses": { - "501": { - "description": "Not implemented", + "200": { + "description": "OK", + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "Bad Request Error", "content": { "application/json": { "schema": { - "type": "object", - "properties": { - "message": { - "type": "string", - "nullable": false, - "minLength": 1, - "maxLength": 2048 - } - } + "$ref": "#/components/schemas/ErrorExceptionResponseV1" + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorExceptionResponseV1" } } } diff --git a/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/generated/openapi/typescript-axios/api.ts index 970b309c622..9b8418015ad 100644 --- a/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -24,28 +24,89 @@ import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } fr /** * * @export - * @interface InlineResponse501 + * @interface ErrorExceptionJsonResponseV1 */ -export interface InlineResponse501 { +export interface ErrorExceptionJsonResponseV1 { /** * * @type {string} - * @memberof InlineResponse501 + * @memberof ErrorExceptionJsonResponseV1 */ - message?: string; + message: string; + /** + * + * @type {string} + * @memberof ErrorExceptionJsonResponseV1 + */ + name?: string; + /** + * + * @type {string} + * @memberof ErrorExceptionJsonResponseV1 + */ + error?: string; + /** + * + * @type {string} + * @memberof ErrorExceptionJsonResponseV1 + */ + stack?: string; + /** + * + * @type {string} + * @memberof ErrorExceptionJsonResponseV1 + */ + cause?: string; } /** * * @export - * @interface InvokeContractV1Request + * @interface ErrorExceptionResponseV1 */ -export interface InvokeContractV1Request { +export interface ErrorExceptionResponseV1 { /** * - * @type {any} - * @memberof InvokeContractV1Request + * @type {string} + * @memberof ErrorExceptionResponseV1 + */ + message: string; + /** + * + * @type {string} + * @memberof ErrorExceptionResponseV1 */ - contractName?: any | null; + error: string; +} +/** + * + * @export + * @interface GenerateTransactionRequestV1 + */ +export interface GenerateTransactionRequestV1 { + /** + * Iroha command name. + * @type {IrohaCommand} + * @memberof GenerateTransactionRequestV1 + */ + commandName: IrohaCommand; + /** + * Parameters for iroha command specified in commandName + * @type {object} + * @memberof GenerateTransactionRequestV1 + */ + commandParams: object; + /** + * Sender account id + * @type {string} + * @memberof GenerateTransactionRequestV1 + */ + creatorAccountId: string; + /** + * Requested transaction quorum + * @type {number} + * @memberof GenerateTransactionRequestV1 + */ + quorum?: number; } /** * @@ -292,7 +353,7 @@ export interface RunTransactionRequestV1 { * @type {IrohaBaseConfig} * @memberof RunTransactionRequestV1 */ - baseConfig?: IrohaBaseConfig; + baseConfig: IrohaBaseConfig; /** * The list of arguments to pass in to the transaction request. * @type {Array} @@ -313,6 +374,25 @@ export interface RunTransactionResponse { */ transactionReceipt: any | null; } +/** + * + * @export + * @interface RunTransactionSignedRequestV1 + */ +export interface RunTransactionSignedRequestV1 { + /** + * Signed transaction binary data received from generate-transaction endpoint. + * @type {any} + * @memberof RunTransactionSignedRequestV1 + */ + signedTransaction: any; + /** + * + * @type {IrohaBaseConfig} + * @memberof RunTransactionSignedRequestV1 + */ + baseConfig?: IrohaBaseConfig; +} /** * DefaultApi - axios parameter creator @@ -322,12 +402,13 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati return { /** * - * @summary Get the Prometheus Metrics + * @summary Generate transaction that can be signed locally. + * @param {GenerateTransactionRequestV1} [generateTransactionRequestV1] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getPrometheusMetricsV1: async (options: any = {}): Promise => { - const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha/get-prometheus-exporter-metrics`; + generateTransactionV1: async (generateTransactionRequestV1?: GenerateTransactionRequestV1, options: any = {}): Promise => { + const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha/generate-transaction`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -335,15 +416,18 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati baseOptions = configuration.baseOptions; } - const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + 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(generateTransactionRequestV1, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), @@ -352,13 +436,12 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati }, /** * - * @summary Invokes a contract on a Iroha ledger - * @param {InvokeContractV1Request} [invokeContractV1Request] + * @summary Get the Prometheus Metrics * @param {*} [options] Override http request option. * @throws {RequiredError} */ - invokeContractV1: async (invokeContractV1Request?: InvokeContractV1Request, options: any = {}): Promise => { - const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha/invoke-contract`; + getPrometheusMetricsV1: async (options: any = {}): Promise => { + const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha/get-prometheus-exporter-metrics`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); let baseOptions; @@ -366,18 +449,15 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati baseOptions = configuration.baseOptions; } - const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarRequestOptions = { method: 'GET', ...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(invokeContractV1Request, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), @@ -387,11 +467,11 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati /** * * @summary Executes a transaction on a Iroha ledger - * @param {RunTransactionRequestV1} [runTransactionRequestV1] + * @param {RunTransactionRequestV1 | RunTransactionSignedRequestV1} [runTransactionRequestV1RunTransactionSignedRequestV1] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - runTransactionV1: async (runTransactionRequestV1?: RunTransactionRequestV1, options: any = {}): Promise => { + runTransactionV1: async (runTransactionRequestV1RunTransactionSignedRequestV1?: RunTransactionRequestV1 | RunTransactionSignedRequestV1, options: any = {}): Promise => { const localVarPath = `/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha/run-transaction`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -411,7 +491,7 @@ export const DefaultApiAxiosParamCreator = function (configuration?: Configurati setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(runTransactionRequestV1, localVarRequestOptions, configuration) + localVarRequestOptions.data = serializeDataIfNeeded(runTransactionRequestV1RunTransactionSignedRequestV1, localVarRequestOptions, configuration) return { url: toPathString(localVarUrlObj), @@ -430,34 +510,34 @@ export const DefaultApiFp = function(configuration?: Configuration) { return { /** * - * @summary Get the Prometheus Metrics + * @summary Generate transaction that can be signed locally. + * @param {GenerateTransactionRequestV1} [generateTransactionRequestV1] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async getPrometheusMetricsV1(options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getPrometheusMetricsV1(options); + async generateTransactionV1(generateTransactionRequestV1?: GenerateTransactionRequestV1, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.generateTransactionV1(generateTransactionRequestV1, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * - * @summary Invokes a contract on a Iroha ledger - * @param {InvokeContractV1Request} [invokeContractV1Request] + * @summary Get the Prometheus Metrics * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async invokeContractV1(invokeContractV1Request?: InvokeContractV1Request, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.invokeContractV1(invokeContractV1Request, options); + async getPrometheusMetricsV1(options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getPrometheusMetricsV1(options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** * * @summary Executes a transaction on a Iroha ledger - * @param {RunTransactionRequestV1} [runTransactionRequestV1] + * @param {RunTransactionRequestV1 | RunTransactionSignedRequestV1} [runTransactionRequestV1RunTransactionSignedRequestV1] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async runTransactionV1(runTransactionRequestV1?: RunTransactionRequestV1, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.runTransactionV1(runTransactionRequestV1, options); + async runTransactionV1(runTransactionRequestV1RunTransactionSignedRequestV1?: RunTransactionRequestV1 | RunTransactionSignedRequestV1, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.runTransactionV1(runTransactionRequestV1RunTransactionSignedRequestV1, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, } @@ -472,32 +552,32 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa return { /** * - * @summary Get the Prometheus Metrics + * @summary Generate transaction that can be signed locally. + * @param {GenerateTransactionRequestV1} [generateTransactionRequestV1] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - getPrometheusMetricsV1(options?: any): AxiosPromise { - return localVarFp.getPrometheusMetricsV1(options).then((request) => request(axios, basePath)); + generateTransactionV1(generateTransactionRequestV1?: GenerateTransactionRequestV1, options?: any): AxiosPromise { + return localVarFp.generateTransactionV1(generateTransactionRequestV1, options).then((request) => request(axios, basePath)); }, /** * - * @summary Invokes a contract on a Iroha ledger - * @param {InvokeContractV1Request} [invokeContractV1Request] + * @summary Get the Prometheus Metrics * @param {*} [options] Override http request option. * @throws {RequiredError} */ - invokeContractV1(invokeContractV1Request?: InvokeContractV1Request, options?: any): AxiosPromise { - return localVarFp.invokeContractV1(invokeContractV1Request, options).then((request) => request(axios, basePath)); + getPrometheusMetricsV1(options?: any): AxiosPromise { + return localVarFp.getPrometheusMetricsV1(options).then((request) => request(axios, basePath)); }, /** * * @summary Executes a transaction on a Iroha ledger - * @param {RunTransactionRequestV1} [runTransactionRequestV1] + * @param {RunTransactionRequestV1 | RunTransactionSignedRequestV1} [runTransactionRequestV1RunTransactionSignedRequestV1] * @param {*} [options] Override http request option. * @throws {RequiredError} */ - runTransactionV1(runTransactionRequestV1?: RunTransactionRequestV1, options?: any): AxiosPromise { - return localVarFp.runTransactionV1(runTransactionRequestV1, options).then((request) => request(axios, basePath)); + runTransactionV1(runTransactionRequestV1RunTransactionSignedRequestV1?: RunTransactionRequestV1 | RunTransactionSignedRequestV1, options?: any): AxiosPromise { + return localVarFp.runTransactionV1(runTransactionRequestV1RunTransactionSignedRequestV1, options).then((request) => request(axios, basePath)); }, }; }; @@ -511,37 +591,37 @@ export const DefaultApiFactory = function (configuration?: Configuration, basePa export class DefaultApi extends BaseAPI { /** * - * @summary Get the Prometheus Metrics + * @summary Generate transaction that can be signed locally. + * @param {GenerateTransactionRequestV1} [generateTransactionRequestV1] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ - public getPrometheusMetricsV1(options?: any) { - return DefaultApiFp(this.configuration).getPrometheusMetricsV1(options).then((request) => request(this.axios, this.basePath)); + public generateTransactionV1(generateTransactionRequestV1?: GenerateTransactionRequestV1, options?: any) { + return DefaultApiFp(this.configuration).generateTransactionV1(generateTransactionRequestV1, options).then((request) => request(this.axios, this.basePath)); } /** * - * @summary Invokes a contract on a Iroha ledger - * @param {InvokeContractV1Request} [invokeContractV1Request] + * @summary Get the Prometheus Metrics * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ - public invokeContractV1(invokeContractV1Request?: InvokeContractV1Request, options?: any) { - return DefaultApiFp(this.configuration).invokeContractV1(invokeContractV1Request, options).then((request) => request(this.axios, this.basePath)); + public getPrometheusMetricsV1(options?: any) { + return DefaultApiFp(this.configuration).getPrometheusMetricsV1(options).then((request) => request(this.axios, this.basePath)); } /** * * @summary Executes a transaction on a Iroha ledger - * @param {RunTransactionRequestV1} [runTransactionRequestV1] + * @param {RunTransactionRequestV1 | RunTransactionSignedRequestV1} [runTransactionRequestV1RunTransactionSignedRequestV1] * @param {*} [options] Override http request option. * @throws {RequiredError} * @memberof DefaultApi */ - public runTransactionV1(runTransactionRequestV1?: RunTransactionRequestV1, options?: any) { - return DefaultApiFp(this.configuration).runTransactionV1(runTransactionRequestV1, options).then((request) => request(this.axios, this.basePath)); + public runTransactionV1(runTransactionRequestV1RunTransactionSignedRequestV1?: RunTransactionRequestV1 | RunTransactionSignedRequestV1, options?: any) { + return DefaultApiFp(this.configuration).runTransactionV1(runTransactionRequestV1RunTransactionSignedRequestV1, options).then((request) => request(this.axios, this.basePath)); } } diff --git a/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/iroha-sign-utils.ts b/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/iroha-sign-utils.ts new file mode 100644 index 00000000000..8be6e8ba712 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/iroha-sign-utils.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2020-2022 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Signing utility functions for HL Iroha ledger. + */ + +import txHelper from "iroha-helpers-ts/lib/txHelper"; +import { Transaction } from "iroha-helpers-ts/lib/proto/transaction_pb"; + +/** + * Sign transaction binary received from `generateTransactionV1()` call. + * Can be signed by multiple Signatories (with multiple keys) or a single key. + * + * @param serializedTx Serialized transaction data. + * To convert binary response from connector into `Uint8Array` required by this function you can use: + * ``` + * const serializedTx = Uint8Array.from(Object.values(genTxResponse.data)); + * ``` + * @param privateKeys One or multiple keys to sign the transaction with. + * @returns Signed transaction data (`Uint8Array`) + */ +export function signIrohaTransaction( + serializedTx: Uint8Array, + privateKeys: string | string[], +): Uint8Array { + const unsignedTx = Transaction.deserializeBinary(serializedTx); + + if (typeof privateKeys === "string") { + privateKeys = [privateKeys]; + } + + const signedTx = privateKeys.reduce( + (tx, key) => txHelper.sign(tx, key), + unsignedTx, + ); + + return signedTx.serializeBinary(); +} diff --git a/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/plugin-ledger-connector-iroha.ts b/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/plugin-ledger-connector-iroha.ts index df19e76c16e..b1f32fab3e3 100644 --- a/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/plugin-ledger-connector-iroha.ts +++ b/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/plugin-ledger-connector-iroha.ts @@ -1,10 +1,10 @@ -import { Server } from "http"; import * as grpc from "grpc"; -import { Server as SecureServer } from "https"; import { CommandService_v1Client as CommandService } from "iroha-helpers-ts/lib/proto/endpoint_grpc_pb"; import { QueryService_v1Client as QueryService } from "iroha-helpers-ts/lib/proto/endpoint_grpc_pb"; +import { Transaction } from "iroha-helpers-ts/lib/proto/transaction_pb"; import commands from "iroha-helpers-ts/lib/commands/index"; import queries from "iroha-helpers-ts/lib/queries"; +import { TxBuilder } from "iroha-helpers-ts/lib/chain"; import type { Express } from "express"; import { GrantablePermission, @@ -39,10 +39,13 @@ import { IrohaCommand, IrohaQuery, RunTransactionRequestV1, + RunTransactionSignedRequestV1, + GenerateTransactionRequestV1, RunTransactionResponse, } from "./generated/openapi/typescript-axios"; import { RunTransactionEndpoint } from "./web-services/run-transaction-endpoint"; +import { GenerateTransactionEndpoint } from "./web-services/generate-transaction-endpoint"; import { PrometheusExporter } from "./prometheus-exporter/prometheus-exporter"; import { GetPrometheusExporterMetricsEndpointV1, @@ -64,7 +67,7 @@ export class PluginLedgerConnectorIroha IPluginLedgerConnector< never, never, - RunTransactionRequestV1, + RunTransactionSignedRequestV1 | RunTransactionRequestV1, RunTransactionResponse >, ICactusPlugin, @@ -72,10 +75,8 @@ export class PluginLedgerConnectorIroha private readonly instanceId: string; public prometheusExporter: PrometheusExporter; private readonly log: Logger; - private readonly pluginRegistry: PluginRegistry; private endpoints: IWebServiceEndpoint[] | undefined; - private httpServer: Server | SecureServer | null = null; public static readonly CLASS_NAME = "PluginLedgerConnectorIroha"; @@ -90,7 +91,6 @@ export class PluginLedgerConnectorIroha options.rpcToriiPortHost, `${fnTag} options.rpcToriiPortHost`, ); - Checks.truthy(options.pluginRegistry, `${fnTag} options.pluginRegistry`); Checks.truthy(options.instanceId, `${fnTag} options.instanceId`); const level = this.options.logLevel || "INFO"; @@ -98,7 +98,6 @@ export class PluginLedgerConnectorIroha this.log = LoggerProvider.getOrCreate({ level, label }); this.instanceId = options.instanceId; - this.pluginRegistry = options.pluginRegistry; this.prometheusExporter = options.prometheusExporter || new PrometheusExporter({ pollingIntervalInMin: 1 }); @@ -166,6 +165,14 @@ export class PluginLedgerConnectorIroha const endpoint = new GetPrometheusExporterMetricsEndpointV1(opts); endpoints.push(endpoint); } + { + const endpoint = new GenerateTransactionEndpoint({ + connector: this, + logLevel: this.options.logLevel, + }); + endpoints.push(endpoint); + } + this.endpoints = endpoints; return endpoints; } @@ -185,38 +192,31 @@ export class PluginLedgerConnectorIroha return consensusHasTransactionFinality(currentConsensusAlgorithmFamily); } - public async transact( + /** + * Create and run Iroha transaction based on input arguments. + * Transaction is signed with a private key supplied in the input argument. + * + * @param req `RunTransactionSignedRequestV1` + * @param commandService Iroha SDK `CommandService_v1Client` instance + * @param queryService Iroha SDK `QueryService_v1Client` instance + * @returns `Promise` + */ + private async transactRequest( req: RunTransactionRequestV1, + commandService: CommandService, + queryService: QueryService, ): Promise { const { baseConfig } = req; if ( !baseConfig || !baseConfig.privKey || !baseConfig.creatorAccountId || - !baseConfig.irohaHost || - !baseConfig.irohaPort || !baseConfig.quorum || !baseConfig.timeoutLimit ) { - this.log.debug( - "Certain field within the Iroha basic configuration is missing!", - ); throw new RuntimeError("Some fields in baseConfig is undefined"); } - const irohaHostPort = `${baseConfig.irohaHost}:${baseConfig.irohaPort}`; - let grpcCredentials; - if (baseConfig.tls) { - throw new RuntimeError("TLS option is not supported"); - } else { - grpcCredentials = grpc.credentials.createInsecure(); - } - const commandService = new CommandService( - irohaHostPort, - //TODO:do something in the production environment - grpcCredentials, - ); - const queryService = new QueryService(irohaHostPort, grpcCredentials); const commandOptions = { privateKeys: baseConfig.privKey, //need an array of keys for command creatorAccountId: baseConfig.creatorAccountId, @@ -224,6 +224,7 @@ export class PluginLedgerConnectorIroha commandService: commandService, timeoutLimit: baseConfig.timeoutLimit, }; + const queryOptions = { privateKey: baseConfig.privKey[0], //only need 1 key for query creatorAccountId: baseConfig.creatorAccountId as string, @@ -241,7 +242,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaCommand.SetAccountDetail: { @@ -253,7 +254,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaCommand.CompareAndSetAccountDetail: { @@ -269,7 +270,7 @@ export class PluginLedgerConnectorIroha ); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaCommand.CreateAsset: { @@ -282,7 +283,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaCommand.CreateDomain: { @@ -293,7 +294,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaCommand.SetAccountQuorum: { @@ -304,7 +305,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaCommand.AddAssetQuantity: { @@ -315,7 +316,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaCommand.SubtractAssetQuantity: { @@ -329,7 +330,7 @@ export class PluginLedgerConnectorIroha ); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaCommand.TransferAsset: { @@ -343,7 +344,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaQuery.GetSignatories: { @@ -353,7 +354,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: queryRes }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaQuery.GetAccount: { @@ -363,7 +364,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: queryRes }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaQuery.GetAccountDetail: { @@ -378,7 +379,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: queryRes }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaQuery.GetAssetInfo: { @@ -388,7 +389,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: queryRes }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaQuery.GetAccountAssets: { @@ -400,7 +401,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: queryRes }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaCommand.AddSignatory: { @@ -411,7 +412,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaCommand.RemoveSignatory: { @@ -422,7 +423,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaQuery.GetRoles: { @@ -430,7 +431,7 @@ export class PluginLedgerConnectorIroha const response = await queries.getRoles(queryOptions); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaCommand.CreateRole: { @@ -441,7 +442,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaCommand.AppendRole: { @@ -452,7 +453,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaCommand.DetachRole: { @@ -463,7 +464,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaQuery.GetRolePermissions: { @@ -473,7 +474,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaCommand.GrantPermission: { @@ -485,7 +486,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaCommand.RevokePermission: { @@ -497,7 +498,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaCommand.SetSettingValue: { @@ -510,7 +511,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaQuery.GetPendingTransactions: { @@ -521,7 +522,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaQuery.GetAccountTransactions: { @@ -533,7 +534,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaQuery.GetAccountAssetTransactions: { @@ -549,7 +550,7 @@ export class PluginLedgerConnectorIroha ); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaQuery.GetBlock: { @@ -559,7 +560,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaCommand.CallEngine: { @@ -572,7 +573,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaQuery.GetEngineReceipts: { @@ -582,7 +583,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaQuery.FetchCommits: { @@ -590,7 +591,7 @@ export class PluginLedgerConnectorIroha const response = await queries.fetchCommits(queryOptions); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaCommand.AddPeer: { @@ -601,7 +602,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaCommand.RemovePeer: { @@ -611,7 +612,7 @@ export class PluginLedgerConnectorIroha }); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } case IrohaQuery.GetPeers: { @@ -619,7 +620,7 @@ export class PluginLedgerConnectorIroha const response = await queries.getPeers(queryOptions); return { transactionReceipt: response }; } catch (err) { - throw new RuntimeError(err); + throw new RuntimeError(err as any); } } default: { @@ -629,4 +630,125 @@ export class PluginLedgerConnectorIroha } } } + + /** + * Run Iroha transaction based on already signed transaction received from the client. + * + * @param req RunTransactionSignedRequestV1 + * @param commandService Iroha SDK `CommandService_v1Client` instance + * @returns `Promise` + */ + private async transactSigned( + req: RunTransactionSignedRequestV1, + commandService: CommandService, + ): Promise { + if (!req.baseConfig || !req.baseConfig.timeoutLimit) { + throw new RuntimeError("baseConfig.timeoutLimit is undefined"); + } + + try { + const transactionBinary = Uint8Array.from( + Object.values(req.signedTransaction), + ); + const signedTransaction = Transaction.deserializeBinary( + transactionBinary, + ); + this.log.debug("Received signed transaction:", signedTransaction); + + const sendResponse = await new TxBuilder(signedTransaction).send( + commandService, + req.baseConfig.timeoutLimit, + ); + + return { transactionReceipt: sendResponse }; + } catch (error) { + throw new RuntimeError(error as any); + } + } + + /** + * Entry point for transact endpoint. + * Validate common `baseConfig` arguments and perapre command and query services. + * Call different transaction logic depending on input arguments. + * + * @note TLS connections are not supported yet. + * @param req `RunTransactionSignedRequestV1 | RunTransactionRequestV1` + * @returns `Promise` + */ + public async transact( + req: RunTransactionSignedRequestV1 | RunTransactionRequestV1, + ): Promise { + const { baseConfig } = req; + if (!baseConfig || !baseConfig.irohaHost || !baseConfig.irohaPort) { + throw new RuntimeError("Missing Iroha URL information."); + } + const irohaHostPort = `${baseConfig.irohaHost}:${baseConfig.irohaPort}`; + + let grpcCredentials; + if (baseConfig.tls) { + throw new RuntimeError("TLS option is not supported"); + } else { + grpcCredentials = grpc.credentials.createInsecure(); + } + + const commandService = new CommandService( + irohaHostPort, + //TODO:do something in the production environment + grpcCredentials, + ); + const queryService = new QueryService(irohaHostPort, grpcCredentials); + + if ("signedTransaction" in req) { + return this.transactSigned(req, commandService); + } else { + return this.transactRequest(req, commandService, queryService); + } + } + + /** + * Check if given Iroha command is supported and can be safely called on the `TxBuilder`. + * Command must be listend in OpenAPI interface and be present on the builder object. + * @param builder `TxBuilder` that will be used to call the command on. + * @param command Iroha command name in string format. + * @returns `true` if command is safe, `false` otherwise. + */ + private isSafeIrohaCommand(builder: TxBuilder, command: string): boolean { + // Check if command is listen in the OpenAPI interface + if (!Object.values(IrohaCommand).includes(command as IrohaCommand)) { + this.log.debug("Command not listed in OpenAPI interface"); + return false; + } + + // Check if function is present in builder object + return ( + command in builder && typeof (builder as any)[command] === "function" + ); + } + + /** + * Entry point for generate unsigned transaction endpoint. + * Transaction must be deserialized and signed on the client side. + * It can be then send to transact endpoint for futher processing. + * @param req `GenerateTransactionRequestV1` + * @returns `Uint8Array` of serialized transaction. + */ + public generateTransaction(req: GenerateTransactionRequestV1): Uint8Array { + req.quorum = req.quorum ?? 1; + const builder = new TxBuilder(); + + if (!this.isSafeIrohaCommand(builder, req.commandName)) { + throw new RuntimeError( + `Bad Request: Not supported Iroha command '${req.commandName}' - aborted.`, + ); + } + + try { + return (builder as any) + [req.commandName](req.commandParams) + .addMeta(req.creatorAccountId, req.quorum) + .tx.serializeBinary(); + } catch (error) { + throw new RuntimeError(error as any); + } + } } diff --git a/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/public-api.ts b/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/public-api.ts index a36b7654813..58695cc1720 100755 --- a/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/public-api.ts +++ b/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/public-api.ts @@ -3,6 +3,9 @@ export { IPluginLedgerConnectorIrohaOptions, PluginLedgerConnectorIroha, } from "./plugin-ledger-connector-iroha"; + +export { signIrohaTransaction } from "./iroha-sign-utils"; + export { PluginFactoryLedgerConnector } from "./plugin-factory-ledger-connector"; import { IPluginFactoryOptions } from "@hyperledger/cactus-core-api"; diff --git a/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/web-services/generate-transaction-endpoint.ts b/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/web-services/generate-transaction-endpoint.ts new file mode 100644 index 00000000000..9f17ea03205 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/web-services/generate-transaction-endpoint.ts @@ -0,0 +1,131 @@ +import type { Express, Request, Response } from "express"; +import safeStringify from "fast-safe-stringify"; +import sanitizeHtml from "sanitize-html"; + +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 { PluginLedgerConnectorIroha } from "../plugin-ledger-connector-iroha"; + +import OAS from "../../json/openapi.json"; + +export interface IGenerateTransactionEndpointOptions { + logLevel?: LogLevelDesc; + connector: PluginLedgerConnectorIroha; +} + +export class GenerateTransactionEndpoint implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "GenerateTransactionEndpoint"; + + private readonly log: Logger; + + public get className(): string { + return GenerateTransactionEndpoint.CLASS_NAME; + } + + constructor(public readonly options: IGenerateTransactionEndpointOptions) { + 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 getOasPath() { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-iroha/generate-transaction" + ]; + } + + public getPath(): string { + const apiPath = this.getOasPath(); + return apiPath.post["x-hyperledger-cactus"].http.path; + } + + public getVerbLowerCase(): string { + const apiPath = this.getOasPath(); + return apiPath.post["x-hyperledger-cactus"].http.verbLowerCase; + } + + public getOperationId(): string { + return this.getOasPath().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 { + res.send(this.options.connector.generateTransaction(req.body)); + } catch (error) { + this.log.error(`Crash while serving ${reqTag}:`, error); + + if (error instanceof Error) { + let status = 500; + let message = "Internal Server Error"; + + if (error.message.includes("Bad Request")) { + status = 400; + message = "Bad Request Error"; + } + + this.log.info(`${message} [${status}]`); + res.status(status).json({ + message, + error: sanitizeHtml(error.stack || error.message, { + allowedTags: [], + allowedAttributes: {}, + }), + }); + } else { + this.log.warn("Unexpected exception that is not instance of Error!"); + res.status(500).json({ + message: "Unexpected Error", + error: sanitizeHtml(safeStringify(error), { + allowedTags: [], + allowedAttributes: {}, + }), + }); + } + } + } +} + +/** + * TODO + * Review main plugin (not done yet) + */ diff --git a/packages/cactus-plugin-ledger-connector-iroha/src/test/typescript/integration/generate-and-send-signed-transaction.test.ts b/packages/cactus-plugin-ledger-connector-iroha/src/test/typescript/integration/generate-and-send-signed-transaction.test.ts new file mode 100644 index 00000000000..556875e2840 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha/src/test/typescript/integration/generate-and-send-signed-transaction.test.ts @@ -0,0 +1,405 @@ +/** + * Test sending transactions signed on the BLP (client) side. + * No private keys are shared with the connector in this scenario. + * Test suite contains simple sanity check of regular transaction call as well. + */ + +////////////////////////////////// +// Constants +////////////////////////////////// + +// Ledger settings +const ledgerImageName = "ghcr.io/hyperledger/cactus-iroha-all-in-one"; +const ledgerImageVersion = "2021-08-16--1183"; +const postgresImageName = "postgres"; +const postgresImageVersion = "9.5-alpine"; + +// Log settings +const testLogLevel: LogLevelDesc = "info"; // default: info +const sutLogLevel: LogLevelDesc = "info"; // default: info + +import { + PostgresTestContainer, + IrohaTestLedger, + pruneDockerAllIfGithubAction, +} from "@hyperledger/cactus-test-tooling"; + +import { + LogLevelDesc, + LoggerProvider, + Logger, + Servers, +} from "@hyperledger/cactus-common"; + +import { PluginRegistry } from "@hyperledger/cactus-core"; + +import { PluginImportType, Configuration } from "@hyperledger/cactus-core-api"; + +import http from "http"; +import express from "express"; +import bodyParser from "body-parser"; +import { AddressInfo } from "net"; +import { v4 as uuidv4 } from "uuid"; +import { v4 as internalIpV4 } from "internal-ip"; +import "jest-extended"; + +import cryptoHelper from "iroha-helpers-ts/lib/cryptoHelper"; + +import { + PluginLedgerConnectorIroha, + DefaultApi as IrohaApi, + PluginFactoryLedgerConnector, + signIrohaTransaction, +} from "../../../main/typescript/public-api"; + +import { + IrohaCommand, + IrohaQuery, +} from "../../../main/typescript/generated/openapi/typescript-axios"; + +// Logger setup +const log: Logger = LoggerProvider.getOrCreate({ + label: "generate-and-send-signed-transaction.test", + level: testLogLevel, +}); + +/** + * Main test suite + */ +describe("Generate and send signed transaction tests", () => { + let postgresContainer: PostgresTestContainer; + let adminKeyPair: { + publicKey: string; + privateKey: string; + }; + let irohaLedger: IrohaTestLedger; + let irohaConnector: PluginLedgerConnectorIroha; + let irohaLedgerHost: string; + let irohaLedgerPort: number; + let irohaAdminID: string; + let connectorServer: http.Server; + let apiClient: IrohaApi; + + ////////////////////////////////// + // Environment Setup + ////////////////////////////////// + + beforeAll(async () => { + log.info("Prune Docker..."); + await pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + + log.info("Run PostgresTestContainer..."); + postgresContainer = new PostgresTestContainer({ + imageName: postgresImageName, + imageVersion: postgresImageVersion, + logLevel: testLogLevel, + }); + await postgresContainer.start(); + const postgresHost = (await internalIpV4()) as string; + const postgresPort = await postgresContainer.getPostgresPort(); + expect(postgresHost).toBeTruthy(); + expect(postgresPort).toBeTruthy(); + log.info(`Postgres running at ${postgresHost}:${postgresPort}`); + + log.info("Generate key pairs..."); + adminKeyPair = cryptoHelper.generateKeyPair(); + const nodeKeyPair = cryptoHelper.generateKeyPair(); + + log.info("Run IrohaTestLedger..."); + irohaLedger = new IrohaTestLedger({ + imageName: ledgerImageName, + imageVersion: ledgerImageVersion, + adminPriv: adminKeyPair.privateKey, + adminPub: adminKeyPair.publicKey, + nodePriv: nodeKeyPair.privateKey, + nodePub: nodeKeyPair.publicKey, + postgresHost: postgresHost, + postgresPort: postgresPort, + logLevel: testLogLevel, + }); + await irohaLedger.start(); + irohaLedgerHost = (await internalIpV4()) as string; + expect(irohaLedgerHost).toBeTruthy(); + irohaLedgerPort = await irohaLedger.getRpcToriiPort(); + expect(irohaLedgerPort).toBeTruthy(); + const rpcToriiPortHost = await irohaLedger.getRpcToriiPortHost(); + expect(rpcToriiPortHost).toBeTruthy(); + const admin = irohaLedger.getDefaultAdminAccount(); + expect(admin).toBeTruthy(); + const domain = irohaLedger.getDefaultDomain(); + expect(domain).toBeTruthy(); + irohaAdminID = `${admin}@${domain}`; + log.info( + "IrohaTestLedger RPC host:", + rpcToriiPortHost, + "irohaAdminID:", + irohaAdminID, + ); + + log.info("Create iroha connector..."); + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + connectorServer = http.createServer(expressApp); + const listenOptions = { + hostname: "localhost", + port: 0, + server: connectorServer, + }; + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + const { address, port } = addressInfo; + + const factory = new PluginFactoryLedgerConnector({ + pluginImportType: PluginImportType.Local, + }); + + irohaConnector = await factory.create({ + rpcToriiPortHost, + instanceId: uuidv4(), + pluginRegistry: new PluginRegistry(), + logLevel: sutLogLevel, + }); + await irohaConnector.getOrCreateWebServices(); + await irohaConnector.registerWebServices(expressApp); + log.info(`Iroha connector is running at ${address}:${port}`); + + log.info("Create iroha ApiClient..."); + const apiHost = `http://${address}:${port}`; + const apiConfig = new Configuration({ basePath: apiHost }); + apiClient = new IrohaApi(apiConfig); + }); + + afterAll(async () => { + log.info("FINISHING THE TESTS"); + + if (connectorServer) { + log.info("Stop the iroha connector..."); + await Servers.shutdown(connectorServer); + } + + if (irohaLedger) { + log.info("Stop iroha ledger..."); + await irohaLedger.stop(); + } + + if (postgresContainer) { + log.info("Stop iroha postgres container..."); + await postgresContainer.stop(); + } + + log.info("Prune Docker..."); + await pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + }); + + ////////////////////////////////// + // Test Helpers + ////////////////////////////////// + + /** + * Read iroha account details and check if valid response was received. + * @param accountID account to fetch. + * @returns getAccount command response. + */ + async function getAccountInfo(accountID: string) { + log.debug("Get account info with ID", accountID); + + const getAccReq = { + commandName: IrohaQuery.GetAccount, + baseConfig: { + irohaHost: irohaLedgerHost, + irohaPort: irohaLedgerPort, + creatorAccountId: irohaAdminID, + privKey: [adminKeyPair.privateKey], + quorum: 1, + timeoutLimit: 5000, + tls: false, + }, + params: [accountID], + }; + + const getAccResponse = await apiClient.runTransactionV1(getAccReq); + expect(getAccResponse).toBeTruthy(); + expect(getAccResponse.data).toBeTruthy(); + expect(getAccResponse.status).toEqual(200); + + return getAccResponse; + } + + ////////////////////////////////// + // Tests + ////////////////////////////////// + + /** + * Check if creating an account with regular transaction works well. + * This test sends private key to the connector to sign the transaction, + * and then reads new account details to confirm it was created. + */ + test("Sanity check if regular create account transaction works", async () => { + const username = "usersanity" + uuidv4().substring(0, 5); + const defaultDomain = irohaLedger.getDefaultDomain(); + const userKeyPair = cryptoHelper.generateKeyPair(); + const userID = `${username}@${defaultDomain}`; + + // 1. Create + log.debug("Create user with ID", userID); + const createAccReq = { + commandName: IrohaCommand.CreateAccount, + baseConfig: { + irohaHost: irohaLedgerHost, + irohaPort: irohaLedgerPort, + creatorAccountId: irohaAdminID, + privKey: [adminKeyPair.privateKey], + quorum: 1, + timeoutLimit: 5000, + tls: false, + }, + params: [username, defaultDomain, userKeyPair.publicKey], + }; + const createAccResponse = await apiClient.runTransactionV1(createAccReq); + expect(createAccResponse).toBeTruthy(); + expect(createAccResponse.data).toBeTruthy(); + expect(createAccResponse.status).toEqual(200); + expect(createAccResponse.data.transactionReceipt.status).toEqual( + "COMMITTED", + ); + + // 2. Confirm + const getAccResponse = await getAccountInfo(userID); + expect(getAccResponse.data.transactionReceipt).toEqual({ + accountId: userID, + domainId: defaultDomain, + quorum: 1, + jsonData: "{}", + }); + }); + + /** + * Create new account without sharing the private key with the connector. + * This test will first generate the unsigned transaction, use util function from connector package + * to sign the transaction, and finally send the signed transaction. + * Private key is not shared with the connector. + * New account details are read to confirm it was created and committed correctly. + */ + test("Sign transaction on the client (BLP) side", async () => { + const username = "user" + uuidv4().substring(0, 5); + const defaultDomain = irohaLedger.getDefaultDomain(); + const userKeyPair = cryptoHelper.generateKeyPair(); + const userID = `${username}@${defaultDomain}`; + + // Generate transaction + log.info("Call generateTransactionV1 to get unsigned transaction."); + const genTxResponse = await apiClient.generateTransactionV1({ + commandName: IrohaCommand.CreateAccount, + commandParams: { + accountName: username, + domainId: defaultDomain, + publicKey: userKeyPair.publicKey, + }, + creatorAccountId: irohaAdminID, + quorum: 1, + }); + expect(genTxResponse).toBeTruthy(); + expect(genTxResponse.data).toBeTruthy(); + expect(genTxResponse.status).toEqual(200); + const unsignedTransaction = Uint8Array.from( + Object.values(genTxResponse.data), + ); + expect(unsignedTransaction).toBeTruthy(); + log.info("Received unsigned transcation"); + log.debug("unsignedTransaction:", unsignedTransaction); + + // Sign + const signedTransaction = signIrohaTransaction( + unsignedTransaction, + adminKeyPair.privateKey, + ); + expect(signedTransaction).toBeTruthy(); + log.info("Transaction signed with local private key"); + log.debug("signedTx:", signedTransaction); + + // Send + const sendTransactionResponse = await apiClient.runTransactionV1({ + signedTransaction, + baseConfig: { + irohaHost: irohaLedgerHost, + irohaPort: irohaLedgerPort, + timeoutLimit: 5000, + tls: false, + }, + }); + expect(sendTransactionResponse).toBeTruthy(); + expect(sendTransactionResponse.status).toEqual(200); + expect(sendTransactionResponse.data).toBeTruthy(); + expect(sendTransactionResponse.data.transactionReceipt).toBeTruthy(); + expect(sendTransactionResponse.data.transactionReceipt.txHash).toBeTruthy(); + expect(sendTransactionResponse.data.transactionReceipt.status).toEqual( + "COMMITTED", + ); + + // 3. Confirm + const getAccResponse = await getAccountInfo(userID); + expect(getAccResponse.data.transactionReceipt).toEqual({ + accountId: userID, + domainId: defaultDomain, + quorum: 1, + jsonData: "{}", + }); + }); + + /** + * Check if exceptions thrown by generateTransactionV1 are properly serialized + * and sanitized, so that response message doesn't contain any malicious data. + */ + test("generateTransactionV1 error responses check", async () => { + const username = "user" + uuidv4().substring(0, 5); + const defaultDomain = irohaLedger.getDefaultDomain(); + const userKeyPair = cryptoHelper.generateKeyPair(); + + // Bad Request Error + try { + log.info( + "Call generateTransactionV1 with invalid command name - should fail.", + ); + await apiClient.generateTransactionV1({ + commandName: "MaliciousError " as any, + commandParams: { + accountName: username, + domainId: defaultDomain, + publicKey: userKeyPair.publicKey, + }, + creatorAccountId: irohaAdminID, + quorum: 1, + }); + expect(true).toBeFalse(); // Should always fail + } catch (error: any) { + const errorResponse = error.response; + expect(errorResponse).toBeTruthy(); + expect(errorResponse.status).toEqual(400); + expect(errorResponse.data).toBeTruthy(); + expect(errorResponse.data.message).toBeTruthy(); + expect(errorResponse.data.error).toBeTruthy(); + // HTML should be escaped from the response message + expect(errorResponse.data.message).not.toContain("