From 24fb91dd440b8931919a01baccb1cc0ecbb3e9c7 Mon Sep 17 00:00:00 2001 From: stepniowskip Date: Tue, 22 Mar 2022 11:59:17 +0100 Subject: [PATCH] feat: monitoring, sync and async requests Add monitoring and sending async/sync requests to ledger via SocketIO. Closes: #1941 Signed-off-by: stepniowskip --- .../ledger-connector/i-socket-api-client.ts | 12 +- .../package.json | 1 + .../src/main/json/openapi.json | 79 +++ .../typescript/api-client/iroha-api-client.ts | 246 ++++++++ .../plugin-ledger-connector-iroha.ts | 524 +++--------------- .../src/main/typescript/public-api.ts | 5 + .../src/main/typescript/transaction.ts | 491 ++++++++++++++++ .../web-services/socket-session-endpoint.ts | 203 +++++++ .../iroha-iroha-transfer-example.test.ts | 22 +- .../openapi/openapi-validation.test.ts | 11 +- .../run-transaction-endpoint-v1.test.ts | 12 +- .../typescript/integration/socket-api.test.ts | 444 +++++++++++++++ .../typescript/iroha/iroha-test-ledger.ts | 13 + .../typescript/get-validator-api-client.ts | 11 + tools/docker/iroha-testnet/docker-compose.yml | 3 +- 15 files changed, 1609 insertions(+), 468 deletions(-) create mode 100644 packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/api-client/iroha-api-client.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/transaction.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/web-services/socket-session-endpoint.ts create mode 100644 packages/cactus-plugin-ledger-connector-iroha/src/test/typescript/integration/socket-api.test.ts diff --git a/packages/cactus-core-api/src/main/typescript/plugin/ledger-connector/i-socket-api-client.ts b/packages/cactus-core-api/src/main/typescript/plugin/ledger-connector/i-socket-api-client.ts index 48d30acf73..00253932d6 100644 --- a/packages/cactus-core-api/src/main/typescript/plugin/ledger-connector/i-socket-api-client.ts +++ b/packages/cactus-core-api/src/main/typescript/plugin/ledger-connector/i-socket-api-client.ts @@ -8,15 +8,19 @@ import type { Observable } from "rxjs"; */ export interface ISocketApiClient { sendAsyncRequest?( - contract: Record, - method: Record, args: any, + method?: Record, + methodName?: string, + baseConfig?: any, + contract?: Record, ): void; sendSyncRequest?( - contract: Record, - method: Record, args: any, + method?: Record, + methodName?: string, + baseConfig?: any, + contract?: Record, ): Promise; watchBlocksV1?( diff --git a/packages/cactus-plugin-ledger-connector-iroha/package.json b/packages/cactus-plugin-ledger-connector-iroha/package.json index da98a6e495..6b206dde8c 100644 --- a/packages/cactus-plugin-ledger-connector-iroha/package.json +++ b/packages/cactus-plugin-ledger-connector-iroha/package.json @@ -64,6 +64,7 @@ "iroha-helpers-ts": "0.9.25-ss", "openapi-types": "7.0.1", "prom-client": "13.1.0", + "rxjs": "7.3.0", "typescript-optional": "2.0.1" }, "devDependencies": { 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 071da984b8..8d8fc97fb5 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 @@ -206,6 +206,9 @@ "type": "boolean", "nullable": false, "description": "Can only be set to false for an insecure grpc connection." + }, + "monitorMode": { + "type": "boolean" } } }, @@ -231,6 +234,82 @@ "PrometheusExporterMetricsResponse": { "type": "string", "nullable": false + }, + "IrohaSocketSession": { + "type": "string", + "enum": [ + "org.hyperledger.cactus.api.async.iroha.SocketSession.Subscribe", + "org.hyperledger.cactus.api.async.iroha.SocketSession.Next", + "org.hyperledger.cactus.api.async.iroha.SocketSession.Unsubscribe", + "org.hyperledger.cactus.api.async.iroha.SocketSession.Error", + "org.hyperledger.cactus.api.async.iroha.SocketSession.Complete", + "org.hyperledger.cactus.api.async.iroha.SocketSession.SendAsyncRequest", + "org.hyperledger.cactus.api.async.iroha.SocketSession.SendSyncRequest" + ], + "x-enum-varnames": [ + "Subscribe", + "Next", + "Unsubscribe", + "Error", + "Complete", + "SendAsyncRequest", + "SendSyncRequest" + ] + }, + "IrohaBlockResponse": { + "type": "object", + "required": [ + "payload", + "signaturesList" + ], + "properties": { + "payload": { + "type": "object", + "required": [ + "txNumber", + "transactionsList", + "height", + "prevBlockHash", + "createdTime", + "rejectedTransactionsHashesList" + ], + "properties": { + "transactionsList": { + "type": "array", + "items": {} + }, + "txNumber": { + "type": "number" + }, + "height": { + "type": "number" + }, + "prevBlockHash": { + "type": "string" + }, + "createdTime": { + "type": "number" + }, + "rejectedTransactionsHashesList": { + "type": "array", + "items": {} + } + } + }, + "signaturesList":{ + "type": "array", + "items": {} + } + } + }, + "IrohaBlockProgress": { + "type": "object", + "required": ["transactionReceipt"], + "properties": { + "transactionReceipt": { + "$ref": "#/components/schemas/IrohaBlockResponse" + } + } } } }, diff --git a/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/api-client/iroha-api-client.ts b/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/api-client/iroha-api-client.ts new file mode 100644 index 0000000000..6ee5d58ff3 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/api-client/iroha-api-client.ts @@ -0,0 +1,246 @@ +import { Observable, ReplaySubject, share } from "rxjs"; +import { finalize } from "rxjs/operators"; +import { Socket, 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, + IrohaSocketSession, + IrohaBlockProgress, + IrohaBaseConfig, +} from "../generated/openapi/typescript-axios"; +import { Configuration } from "../generated/openapi/typescript-axios/configuration"; +import { RuntimeError } from "run-time-error"; + +export class IrohaApiClientOptions extends Configuration { + readonly logLevel?: LogLevelDesc; + readonly wsApiHost?: string; + readonly wsApiPath?: string; +} + +export class IrohaApiClient + extends DefaultApi + implements ISocketApiClient { + public static readonly CLASS_NAME = "IrohaApiClient"; + private readonly log: Logger; + private readonly wsApiHost: string; + private readonly wsApiPath: string; + public get className(): string { + return IrohaApiClient.CLASS_NAME; + } + + constructor(public readonly options: IrohaApiClientOptions) { + 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}`); + + Checks.nonBlankString( + this.wsApiHost, + `${this.className}::constructor() wsApiHost`, + ); + Checks.nonBlankString( + this.wsApiPath, + `${this.className}::constructor() wsApiPath`, + ); + } + + /** + * Start monitoring for new blocks on the Iroha ledger. + * @param monitorOptions - Options to be passed to validator `startMonitoring` procedure. + * @returns RxJs Observable, `next` - new block, `error` - any error from the validator. + */ + public watchBlocksV1( + monitorOptions?: Record, + ): Observable { + const socket: Socket = io(this.wsApiHost, { path: this.wsApiPath }); + const subject = new ReplaySubject(0); + this.log.debug(monitorOptions); + socket.on(IrohaSocketSession.Next, (data: IrohaBlockProgress) => { + subject.next(data); + }); + + socket.on("connect", () => { + this.log.debug("connected OK..."); + socket.emit(IrohaSocketSession.Subscribe, monitorOptions); + }); + + socket.connect(); + + socket.on("connect_error", (err: Error) => { + this.log.error("Error (connect_error): ", err); + socket.disconnect(); + subject.error(err); + }); + + socket.on("connect_timeout", (err: Record) => { + this.log.error("Error (connect_timeout): ", err); + socket.disconnect(); + subject.error(err); + }); + + return subject.pipe( + finalize(() => { + this.log.info("FINALIZE - unsubscribing from the stream..."); + socket.emit(IrohaSocketSession.Unsubscribe); + socket.disconnect(); + }), + share(), + ); + } + + /** + * Immediately sends request to the validator, doesn't report any error or responses. + * @param args - arguments. + * @param baseConfig - baseConfig needed to properly connect to ledger + * @param methodName - function / method to be executed by validator. + */ + public sendAsyncRequest( + args: any, + baseConfig?: IrohaBaseConfig, + methodName?: string, + ): void { + this.log.debug(`inside: sendAsyncRequest()`); + this.log.debug(`baseConfig=${baseConfig}`); + this.log.debug(`methodName=${methodName}`); + this.log.debug(`args=${args}`); + + if (baseConfig === undefined || baseConfig === {}) { + throw new RuntimeError("baseConfig object must exist and not be empty"); + } + + if ( + baseConfig.privKey === undefined || + baseConfig.creatorAccountId === undefined || + baseConfig.irohaHost === undefined || + baseConfig.irohaPort === undefined || + baseConfig.quorum === undefined || + baseConfig.timeoutLimit === undefined + ) { + throw new RuntimeError("Some fields in baseConfig are undefined"); + } + + if (methodName === undefined || methodName === "") { + throw new RuntimeError("methodName parameter must be specified"); + } + + const socket: Socket = io(this.wsApiHost, { path: this.wsApiPath }); + const asyncRequestData = { + baseConfig: baseConfig, + methodName: methodName, + args: args, + }; + + this.log.debug("requestData:", asyncRequestData); + + try { + socket.emit(IrohaSocketSession.SendAsyncRequest, asyncRequestData); + } catch (err) { + this.log.error("Exception in: sendAsyncRequest(): ", err); + throw err; + } + } + + /** + * Sends request to be executed on the ledger, watches and reports any error and the response from a ledger. + * @param args - arguments. + * @param baseConfig - baseConfig needed to properly connect to ledger + * @param methodName - function / method to be executed by validator. + * @returns Promise that will resolve with response from the ledger, or reject when error occurred. + */ + public sendSyncRequest( + args: any, + baseConfig?: IrohaBaseConfig, + methodName?: string, + ): Promise { + this.log.debug(`inside: sendSyncRequest()`); + this.log.debug(`baseConfig=${baseConfig}`); + this.log.debug(`method=${methodName}`); + this.log.debug(`args=${args}`); + + if (baseConfig === undefined || baseConfig === {}) { + throw new RuntimeError("baseConfig object must exist and not be empty"); + } + + if ( + baseConfig.privKey === undefined || + baseConfig.creatorAccountId === undefined || + baseConfig.irohaHost === undefined || + baseConfig.irohaPort === undefined || + baseConfig.quorum === undefined || + baseConfig.timeoutLimit === undefined + ) { + throw new RuntimeError("Some fields in baseConfig are undefined"); + } + + if (methodName === undefined || methodName === "") { + throw new RuntimeError("methodName parameter must be specified"); + } + + const socket: Socket = io(this.wsApiHost, { path: this.wsApiPath }); + + let responseFlag = false; + + return new Promise((resolve, reject) => { + try { + socket.on("connect_error", (err: Error) => { + this.log.error("Error (connect_error): ", err); + socket.disconnect(); + reject(err); + }); + + socket.on("connect_timeout", (err: Record) => { + this.log.error("Error (connect_timeout): ", err); + socket.disconnect(); + reject(err); + }); + + socket.on("response", (result: any) => { + this.log.debug("#[recv]response, res:", result); + responseFlag = true; + const resultObj = { + status: result.status, + data: result.txHash, + }; + this.log.debug("resultObj =", resultObj); + resolve(resultObj); + }); + + const syncRequestData = { + baseConfig: baseConfig, + methodName: methodName, + args: args, + }; + + this.log.debug("requestData:", syncRequestData); + + try { + socket.emit(IrohaSocketSession.SendSyncRequest, syncRequestData); + } catch (err) { + this.log.error("Exception in: sendAsyncRequest(): ", err); + throw err; + } + + setTimeout(() => { + if (responseFlag === false) { + resolve({ status: 504 }); + } + }, baseConfig.timeoutLimit); + } catch (err) { + this.log.error("Exception in: sendSyncRequest(): ", err); + reject(err); + } + }); + } +} 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 df19e76c16..a0fa53d61c 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,15 +1,11 @@ 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 commands from "iroha-helpers-ts/lib/commands/index"; -import queries from "iroha-helpers-ts/lib/queries"; +import type { Server as SocketIoServer } from "socket.io"; +import type { Socket as SocketIoSocket } from "socket.io"; + import type { Express } from "express"; -import { - GrantablePermission, - GrantablePermissionMap, -} from "iroha-helpers-ts/lib/proto/primitive_pb"; + +import { Transaction } from "./transaction"; import OAS from "../json/openapi.json"; @@ -32,18 +28,19 @@ import { Logger, LoggerProvider, LogLevelDesc, - Http405NotAllowedError, } from "@hyperledger/cactus-common"; + import { RuntimeError } from "run-time-error"; + import { - IrohaCommand, - IrohaQuery, RunTransactionRequestV1, RunTransactionResponse, + IrohaSocketSession, } from "./generated/openapi/typescript-axios"; import { RunTransactionEndpoint } from "./web-services/run-transaction-endpoint"; import { PrometheusExporter } from "./prometheus-exporter/prometheus-exporter"; +import { SocketSessionEndpoint } from "./web-services/socket-session-endpoint"; import { GetPrometheusExporterMetricsEndpointV1, IGetPrometheusExporterMetricsEndpointV1Options, @@ -53,10 +50,12 @@ export const E_KEYCHAIN_NOT_FOUND = "cactus.connector.iroha.keychain_not_found"; export interface IPluginLedgerConnectorIrohaOptions extends ICactusPluginOptions { - rpcToriiPortHost: string; + rpcToriiPortHost: string; //http host:port + rpcApiWsHost: string; pluginRegistry: PluginRegistry; prometheusExporter?: PrometheusExporter; logLevel?: LogLevelDesc; + instanceId: string; } export class PluginLedgerConnectorIroha @@ -72,12 +71,12 @@ export class PluginLedgerConnectorIroha private readonly instanceId: string; public prometheusExporter: PrometheusExporter; private readonly log: Logger; - private readonly pluginRegistry: PluginRegistry; - private endpoints: IWebServiceEndpoint[] | undefined; + private readonly pluginRegistry: PluginRegistry; private httpServer: Server | SecureServer | null = null; public static readonly CLASS_NAME = "PluginLedgerConnectorIroha"; + private socketSessionDictionary: { [char: string]: SocketSessionEndpoint }; public get className(): string { return PluginLedgerConnectorIroha.CLASS_NAME; @@ -86,6 +85,7 @@ export class PluginLedgerConnectorIroha constructor(public readonly options: IPluginLedgerConnectorIrohaOptions) { const fnTag = `${this.className}#constructor()`; Checks.truthy(options, `${fnTag} arg options`); + Checks.truthy(options.rpcApiWsHost, `${fnTag} options.rpcApiWsHost`); Checks.truthy( options.rpcToriiPortHost, `${fnTag} options.rpcToriiPortHost`, @@ -106,7 +106,7 @@ export class PluginLedgerConnectorIroha this.prometheusExporter, `${fnTag} options.prometheusExporter`, ); - + this.socketSessionDictionary = {}; this.prometheusExporter.startMetricsCollection(); } @@ -140,9 +140,57 @@ export class PluginLedgerConnectorIroha this.log.info(`Shutting down ${this.className}...`); } - async registerWebServices(app: Express): Promise { + async registerWebServices( + app: Express, + wsApi: SocketIoServer, + ): Promise { + const { logLevel } = this.options; const webServices = await this.getOrCreateWebServices(); await Promise.all(webServices.map((ws) => ws.registerExpress(app))); + + wsApi.on("connection", (socket: SocketIoSocket) => { + let socketEndpoint: SocketSessionEndpoint; + + if (socket.id in this.socketSessionDictionary) { + this.log.debug(`Connected to existing socket session ID=${socket.id}`); + socketEndpoint = this.socketSessionDictionary[socket.id]; + } else { + this.log.debug(`New Socket connected. ID=${socket.id}`); + socketEndpoint = new SocketSessionEndpoint({ socket, logLevel }); + this.socketSessionDictionary[socket.id] = socketEndpoint; + } + + let monitorFlag: boolean; + + socket.on(IrohaSocketSession.Subscribe, (monitorOptions) => { + this.log.debug(`Caught event: Subscribe`); + monitorFlag = true; + socketEndpoint.startMonitor(monitorOptions); + }); + + socket.on(IrohaSocketSession.Unsubscribe, () => { + this.log.debug(`Caught event: Unsubscribe`); + socketEndpoint.stopMonitor(); + }); + + socket.on(IrohaSocketSession.SendAsyncRequest, (asyncRequestData) => { + this.log.debug(`Caught event: SendAsyncRequest`); + socketEndpoint.sendAsyncRequest(asyncRequestData); + }); + + socket.on(IrohaSocketSession.SendSyncRequest, (syncRequestData) => { + this.log.debug(`Caught event: SendSyncRequest`); + socketEndpoint.sendSyncRequest(syncRequestData); + }); + + socket.on("disconnect", async (reason: string) => { + this.log.info(`Session: ${socket.id} disconnected. Reason: ${reason}`); + if (monitorFlag) { + socketEndpoint.stopMonitor(); + monitorFlag = false; + } + }); + }); return webServices; } @@ -188,445 +236,7 @@ export class PluginLedgerConnectorIroha public async transact( req: RunTransactionRequestV1, ): 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, - quorum: baseConfig.quorum, - commandService: commandService, - timeoutLimit: baseConfig.timeoutLimit, - }; - const queryOptions = { - privateKey: baseConfig.privKey[0], //only need 1 key for query - creatorAccountId: baseConfig.creatorAccountId as string, - queryService: queryService, - timeoutLimit: baseConfig.timeoutLimit, - }; - - switch (req.commandName) { - case IrohaCommand.CreateAccount: { - try { - const response = await commands.createAccount(commandOptions, { - accountName: req.params[0], - domainId: req.params[1], - publicKey: req.params[2], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaCommand.SetAccountDetail: { - try { - const response = await commands.setAccountDetail(commandOptions, { - accountId: req.params[0], - key: req.params[1], - value: req.params[2], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaCommand.CompareAndSetAccountDetail: { - try { - const response = await commands.compareAndSetAccountDetail( - commandOptions, - { - accountId: req.params[0], - key: req.params[1], - value: req.params[2], - oldValue: req.params[3], - }, - ); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaCommand.CreateAsset: { - try { - const response = await commands // (coolcoin#test; precision:3) - .createAsset(commandOptions, { - assetName: req.params[0], - domainId: req.params[1], - precision: req.params[2], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaCommand.CreateDomain: { - try { - const response = await commands.createDomain(commandOptions, { - domainId: req.params[0], - defaultRole: req.params[1], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaCommand.SetAccountQuorum: { - try { - const response = await commands.setAccountQuorum(commandOptions, { - accountId: req.params[0], - quorum: req.params[1], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaCommand.AddAssetQuantity: { - try { - const response = await commands.addAssetQuantity(commandOptions, { - assetId: req.params[0], - amount: req.params[1], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaCommand.SubtractAssetQuantity: { - try { - const response = await commands.subtractAssetQuantity( - commandOptions, - { - assetId: req.params[0], - amount: req.params[1], - }, - ); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaCommand.TransferAsset: { - try { - const response = await commands.transferAsset(commandOptions, { - srcAccountId: req.params[0], - destAccountId: req.params[1], - assetId: req.params[2], - description: req.params[3], - amount: req.params[4], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaQuery.GetSignatories: { - try { - const queryRes = await queries.getSignatories(queryOptions, { - accountId: req.params[0], - }); - return { transactionReceipt: queryRes }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaQuery.GetAccount: { - try { - const queryRes = await queries.getAccount(queryOptions, { - accountId: req.params[0], - }); - return { transactionReceipt: queryRes }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaQuery.GetAccountDetail: { - try { - const queryRes = await queries.getAccountDetail(queryOptions, { - accountId: req.params[0], - key: req.params[1], - writer: req.params[2], - pageSize: req.params[3], - paginationKey: req.params[4], - paginationWriter: req.params[5], - }); - return { transactionReceipt: queryRes }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaQuery.GetAssetInfo: { - try { - const queryRes = await queries.getAssetInfo(queryOptions, { - assetId: req.params[0], - }); - return { transactionReceipt: queryRes }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaQuery.GetAccountAssets: { - try { - const queryRes = await queries.getAccountAssets(queryOptions, { - accountId: req.params[0], - pageSize: req.params[1], - firstAssetId: req.params[2], - }); - return { transactionReceipt: queryRes }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaCommand.AddSignatory: { - try { - const response = await commands.addSignatory(commandOptions, { - accountId: req.params[0], - publicKey: req.params[1], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaCommand.RemoveSignatory: { - try { - const response = await commands.removeSignatory(commandOptions, { - accountId: req.params[0], - publicKey: req.params[1], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaQuery.GetRoles: { - try { - const response = await queries.getRoles(queryOptions); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaCommand.CreateRole: { - try { - const response = await commands.createRole(commandOptions, { - roleName: req.params[0], - permissionsList: req.params[1], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaCommand.AppendRole: { - try { - const response = await commands.appendRole(commandOptions, { - accountId: req.params[0], - roleName: req.params[1], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaCommand.DetachRole: { - try { - const response = await commands.detachRole(commandOptions, { - accountId: req.params[0], - roleName: req.params[1], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaQuery.GetRolePermissions: { - try { - const response = await queries.getRolePermissions(queryOptions, { - roleId: req.params[0], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaCommand.GrantPermission: { - try { - type permission = keyof GrantablePermissionMap; - const response = await commands.grantPermission(commandOptions, { - accountId: req.params[0], - permission: GrantablePermission[req.params[1] as permission], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaCommand.RevokePermission: { - try { - type permission = keyof GrantablePermissionMap; - const response = await commands.revokePermission(commandOptions, { - accountId: req.params[0], - permission: GrantablePermission[req.params[1] as permission], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaCommand.SetSettingValue: { - throw new Http405NotAllowedError("SetSettingValue is not supported."); - } - case IrohaQuery.GetTransactions: { - try { - const response = await queries.getTransactions(queryOptions, { - txHashesList: req.params[0], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaQuery.GetPendingTransactions: { - try { - const response = await queries.getPendingTransactions(queryOptions, { - pageSize: req.params[0], - firstTxHash: req.params[1], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaQuery.GetAccountTransactions: { - try { - const response = await queries.getAccountTransactions(queryOptions, { - accountId: req.params[0], - pageSize: req.params[1], - firstTxHash: req.params[2], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaQuery.GetAccountAssetTransactions: { - try { - const response = await queries.getAccountAssetTransactions( - queryOptions, - { - accountId: req.params[0], - assetId: req.params[1], - pageSize: req.params[2], - firstTxHash: req.params[3], - }, - ); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaQuery.GetBlock: { - try { - const response = await queries.getBlock(queryOptions, { - height: req.params[0], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaCommand.CallEngine: { - try { - const response = await commands.callEngine(commandOptions, { - type: req.params[0], - caller: req.params[1], - callee: req.params[2], - input: req.params[3], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaQuery.GetEngineReceipts: { - try { - const response = await queries.getEngineReceipts(queryOptions, { - txHash: req.params[0], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaQuery.FetchCommits: { - try { - const response = await queries.fetchCommits(queryOptions); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaCommand.AddPeer: { - try { - const response = await commands.addPeer(commandOptions, { - address: req.params[0], - peerKey: req.params[1], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaCommand.RemovePeer: { - try { - const response = await commands.removePeer(commandOptions, { - publicKey: req.params[0], - }); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - case IrohaQuery.GetPeers: { - try { - const response = await queries.getPeers(queryOptions); - return { transactionReceipt: response }; - } catch (err) { - throw new RuntimeError(err); - } - } - default: { - throw new RuntimeError( - "command or query does not exist, or is not supported in current version", - ); - } - } + const transaction = new Transaction(this.options.logLevel); + return await transaction.transact(req); } } 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 a36b765481..cc1facc1c2 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 @@ -8,6 +8,11 @@ export { PluginFactoryLedgerConnector } from "./plugin-factory-ledger-connector" import { IPluginFactoryOptions } from "@hyperledger/cactus-core-api"; import { PluginFactoryLedgerConnector } from "./plugin-factory-ledger-connector"; +export { + IrohaApiClient, + IrohaApiClientOptions, +} from "./api-client/iroha-api-client"; + export * from "./generated/openapi/typescript-axios/api"; export async function createPluginFactory( diff --git a/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/transaction.ts b/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/transaction.ts new file mode 100644 index 0000000000..3f48602ba8 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/transaction.ts @@ -0,0 +1,491 @@ +import { Logger } from "@hyperledger/cactus-common"; +import { + LoggerProvider, + LogLevelDesc, + Http405NotAllowedError, +} from "@hyperledger/cactus-common"; +import { + IrohaCommand, + IrohaQuery, + RunTransactionRequestV1, + RunTransactionResponse, +} from "./generated/openapi/typescript-axios"; + +import { RuntimeError } from "run-time-error"; +import * as grpc from "grpc"; + +import { + GrantablePermission, + GrantablePermissionMap, +} from "iroha-helpers-ts/lib/proto/primitive_pb"; + +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 commands from "iroha-helpers-ts/lib/commands/index"; +import queries from "iroha-helpers-ts/lib/queries"; + +export class Transaction { + private readonly log: Logger; + public static readonly CLASS_NAME = "Transaction"; + + public get className(): string { + return Transaction.CLASS_NAME; + } + + constructor(logLevel?: LogLevelDesc) { + const level = logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + public async transact( + req: RunTransactionRequestV1, + ): 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 are 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, + quorum: baseConfig.quorum, + commandService: commandService, + timeoutLimit: baseConfig.timeoutLimit, + }; + const queryOptions = { + privateKey: baseConfig.privKey[0], //only need 1 key for query + creatorAccountId: baseConfig.creatorAccountId as string, + queryService: queryService, + timeoutLimit: baseConfig.timeoutLimit, + }; + + switch (req.commandName) { + case IrohaCommand.CreateAccount: { + try { + const response = await commands.createAccount(commandOptions, { + accountName: req.params[0], + domainId: req.params[1], + publicKey: req.params[2], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaCommand.SetAccountDetail: { + try { + const response = await commands.setAccountDetail(commandOptions, { + accountId: req.params[0], + key: req.params[1], + value: req.params[2], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaCommand.CompareAndSetAccountDetail: { + try { + const response = await commands.compareAndSetAccountDetail( + commandOptions, + { + accountId: req.params[0], + key: req.params[1], + value: req.params[2], + oldValue: req.params[3], + }, + ); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaCommand.CreateAsset: { + try { + const response = await commands // (coolcoin#test; precision:3) + .createAsset(commandOptions, { + assetName: req.params[0], + domainId: req.params[1], + precision: req.params[2], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaCommand.CreateDomain: { + try { + const response = await commands.createDomain(commandOptions, { + domainId: req.params[0], + defaultRole: req.params[1], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaCommand.SetAccountQuorum: { + try { + const response = await commands.setAccountQuorum(commandOptions, { + accountId: req.params[0], + quorum: req.params[1], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaCommand.AddAssetQuantity: { + try { + const response = await commands.addAssetQuantity(commandOptions, { + assetId: req.params[0], + amount: req.params[1], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaCommand.SubtractAssetQuantity: { + try { + const response = await commands.subtractAssetQuantity( + commandOptions, + { + assetId: req.params[0], + amount: req.params[1], + }, + ); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaCommand.TransferAsset: { + try { + const response = await commands.transferAsset(commandOptions, { + srcAccountId: req.params[0], + destAccountId: req.params[1], + assetId: req.params[2], + description: req.params[3], + amount: req.params[4], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaQuery.GetSignatories: { + try { + const queryRes = await queries.getSignatories(queryOptions, { + accountId: req.params[0], + }); + return { transactionReceipt: queryRes }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaQuery.GetAccount: { + try { + const queryRes = await queries.getAccount(queryOptions, { + accountId: req.params[0], + }); + return { transactionReceipt: queryRes }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaQuery.GetAccountDetail: { + try { + const queryRes = await queries.getAccountDetail(queryOptions, { + accountId: req.params[0], + key: req.params[1], + writer: req.params[2], + pageSize: req.params[3], + paginationKey: req.params[4], + paginationWriter: req.params[5], + }); + return { transactionReceipt: queryRes }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaQuery.GetAssetInfo: { + try { + const queryRes = await queries.getAssetInfo(queryOptions, { + assetId: req.params[0], + }); + return { transactionReceipt: queryRes }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaQuery.GetAccountAssets: { + try { + const queryRes = await queries.getAccountAssets(queryOptions, { + accountId: req.params[0], + pageSize: req.params[1], + firstAssetId: req.params[2], + }); + return { transactionReceipt: queryRes }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaCommand.AddSignatory: { + try { + const response = await commands.addSignatory(commandOptions, { + accountId: req.params[0], + publicKey: req.params[1], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaCommand.RemoveSignatory: { + try { + const response = await commands.removeSignatory(commandOptions, { + accountId: req.params[0], + publicKey: req.params[1], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaQuery.GetRoles: { + try { + const response = await queries.getRoles(queryOptions); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaCommand.CreateRole: { + try { + const response = await commands.createRole(commandOptions, { + roleName: req.params[0], + permissionsList: req.params[1], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaCommand.AppendRole: { + try { + const response = await commands.appendRole(commandOptions, { + accountId: req.params[0], + roleName: req.params[1], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaCommand.DetachRole: { + try { + const response = await commands.detachRole(commandOptions, { + accountId: req.params[0], + roleName: req.params[1], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaQuery.GetRolePermissions: { + try { + const response = await queries.getRolePermissions(queryOptions, { + roleId: req.params[0], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaCommand.GrantPermission: { + try { + type permission = keyof GrantablePermissionMap; + const response = await commands.grantPermission(commandOptions, { + accountId: req.params[0], + permission: GrantablePermission[req.params[1] as permission], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaCommand.RevokePermission: { + try { + type permission = keyof GrantablePermissionMap; + const response = await commands.revokePermission(commandOptions, { + accountId: req.params[0], + permission: GrantablePermission[req.params[1] as permission], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaCommand.SetSettingValue: { + throw new Http405NotAllowedError("SetSettingValue is not supported."); + } + case IrohaQuery.GetTransactions: { + try { + const response = await queries.getTransactions(queryOptions, { + txHashesList: req.params[0], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaQuery.GetPendingTransactions: { + try { + const response = await queries.getPendingTransactions(queryOptions, { + pageSize: req.params[0], + firstTxHash: req.params[1], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaQuery.GetAccountTransactions: { + try { + const response = await queries.getAccountTransactions(queryOptions, { + accountId: req.params[0], + pageSize: req.params[1], + firstTxHash: req.params[2], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaQuery.GetAccountAssetTransactions: { + try { + const response = await queries.getAccountAssetTransactions( + queryOptions, + { + accountId: req.params[0], + assetId: req.params[1], + pageSize: req.params[2], + firstTxHash: req.params[3], + }, + ); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaQuery.GetBlock: { + try { + const response = await queries.getBlock(queryOptions, { + height: req.params[0], + }); + return { transactionReceipt: response }; + } catch (err: any) { + if ("monitorMode" in baseConfig && baseConfig.monitorMode === true) { + return { transactionReceipt: err }; + } else { + this.log.error(err); + throw new RuntimeError(err); + } + } + } + case IrohaCommand.CallEngine: { + try { + const response = await commands.callEngine(commandOptions, { + type: req.params[0], + caller: req.params[1], + callee: req.params[2], + input: req.params[3], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaQuery.GetEngineReceipts: { + try { + const response = await queries.getEngineReceipts(queryOptions, { + txHash: req.params[0], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaQuery.FetchCommits: { + try { + const response = await queries.fetchCommits(queryOptions); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaCommand.AddPeer: { + try { + const response = await commands.addPeer(commandOptions, { + address: req.params[0], + peerKey: req.params[1], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaCommand.RemovePeer: { + try { + const response = await commands.removePeer(commandOptions, { + publicKey: req.params[0], + }); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + case IrohaQuery.GetPeers: { + try { + const response = await queries.getPeers(queryOptions); + return { transactionReceipt: response }; + } catch (err: any) { + throw new RuntimeError(err); + } + } + default: { + throw new RuntimeError( + "command or query does not exist, or is not supported in current version", + ); + } + } + } +} diff --git a/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/web-services/socket-session-endpoint.ts b/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/web-services/socket-session-endpoint.ts new file mode 100644 index 0000000000..9cdb245a95 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha/src/main/typescript/web-services/socket-session-endpoint.ts @@ -0,0 +1,203 @@ +import { Socket as SocketIoSocket } from "socket.io"; + +import { Logger, Checks } from "@hyperledger/cactus-common"; +import { LogLevelDesc, LoggerProvider } from "@hyperledger/cactus-common"; +import { + RunTransactionRequestV1, + RunTransactionResponse, + IrohaBlockProgress, + IrohaCommand, + IrohaBaseConfig, +} from "../generated/openapi/typescript-axios"; +import { + IrohaSocketSession, + IrohaQuery, +} from "../generated/openapi/typescript-axios"; + +import { Transaction } from "../transaction"; +export interface IWatchBlocksV1EndpointOptions { + logLevel?: LogLevelDesc; + socket: SocketIoSocket; +} + +export class SocketSessionEndpoint { + public static readonly CLASS_NAME = "SocketSessionEndpoint"; + + private readonly log: Logger; + private readonly socket: SocketIoSocket; + private transaction: Transaction; + private currentBlockHeight: number; + private monitorMode: boolean; + private monitoringInterval: any; + + public get className(): string { + return SocketSessionEndpoint.CLASS_NAME; + } + + constructor(public readonly options: IWatchBlocksV1EndpointOptions) { + const fnTag = `${this.className}#constructor()`; + + Checks.truthy(options, `${fnTag} arg options`); + Checks.truthy(options.socket, `${fnTag} arg options.socket`); + + this.socket = options.socket; + + const level = this.options.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + this.transaction = new Transaction(level); + this.currentBlockHeight = 1; + this.monitorMode = false; + } + + private createRequestBody( + config: IrohaBaseConfig, + methodName: string, + params: Array, + ): RunTransactionRequestV1 { + if (this.monitorMode === true) { + config.monitorMode = this.monitorMode; + } + const requestBody = { + commandName: methodName, + params: params, + baseConfig: config, + }; + return requestBody; + } + + private async getInitialMaxBlockHeight(requestData: any): Promise { + this.log.debug("Checking max block height..."); + const methodName: string = IrohaQuery.GetBlock; + + let args: Array; + let requestBody: RunTransactionRequestV1; + let response: RunTransactionResponse; + let str_response: string; + + while (true) { + args = [this.currentBlockHeight]; + + requestBody = this.createRequestBody(requestData, methodName, args); + this.log.debug(`Iroha requestBody: ${requestBody}`); + + response = await this.transaction.transact(requestBody); + str_response = String(response.transactionReceipt); + + if (str_response.includes("Query response error")) { + if (str_response.includes(`"errorCode":3`)) { + this.log.info( + `Initial max block height is: ${this.currentBlockHeight}`, + ); + break; + } else { + this.log.error(`Runtime error caught: ${str_response}`); + break; + } + } + this.currentBlockHeight++; + } + } + + private async monitoringRoutine(baseConfig: any) { + let transactionReceipt: any; + let next: IrohaBlockProgress; + + const args = [this.currentBlockHeight]; + const methodName: string = IrohaQuery.GetBlock; + this.log.debug(`Current block: ${this.currentBlockHeight}`); + + const requestBody = this.createRequestBody(baseConfig, methodName, args); + const response = await this.transaction.transact(requestBody); + const str_response = String(response.transactionReceipt); + + if (str_response.includes("Query response error")) { + if (str_response.includes(`"errorCode":3`)) { + this.log.debug("Waiting for new blocks..."); + } else { + this.log.error(`Runtime error caught: ${str_response}`); + } + } else { + this.log.debug(`New block found`); + transactionReceipt = response.transactionReceipt; + next = { transactionReceipt }; + this.socket.emit(IrohaSocketSession.Next, next); + this.currentBlockHeight++; + } + } + + public async startMonitor(monitorOptions: any): Promise { + this.log.debug(`${IrohaSocketSession.Subscribe} => ${this.socket.id}`); + this.log.info(`Starting monitoring blocks...`); + + this.monitorMode = true; + await this.getInitialMaxBlockHeight(monitorOptions.baseConfig); + + this.monitoringInterval = setInterval(() => { + this.monitoringRoutine(monitorOptions.baseConfig); + }, monitorOptions.pollTime); + } + + private async validateMethodName(methodName: string): Promise { + let isValidMethod = false; + + if (Object.values(IrohaQuery as any).includes(methodName)) { + this.log.debug(`Method name: ${methodName} (IrohaQuery) is valid`); + isValidMethod = true; + } + if (Object.values(IrohaCommand as any).includes(methodName)) { + this.log.debug(`Method name: ${methodName} (IrohaCommand) is valid`); + isValidMethod = true; + } + return isValidMethod; + } + + public async stopMonitor(): Promise { + this.log.info(`Stopping monitor...`); + this.monitorMode = false; + clearInterval(this.monitoringInterval); + } + + public async sendAsyncRequest(asyncRequestData: any): Promise { + this.log.debug(`Inside ##sendAsyncRequest()`); + this.log.debug( + `asyncRequestData: ${JSON.stringify(asyncRequestData, null, 4)}`, + ); + + this.log.debug(asyncRequestData.methodName); + this.log.debug(asyncRequestData.args); + this.log.debug(asyncRequestData.baseConfig); + + // Validate method name + if ((await this.validateMethodName(asyncRequestData.methodName)) === true) { + const requestBody = this.createRequestBody( + asyncRequestData.baseConfig, + asyncRequestData.methodName, + asyncRequestData.args, + ); + + const response = await this.transaction.transact(requestBody); + if (asyncRequestData?.syncRequest) { + this.log.debug(response); + return response; + } + } else { + const error = `Unrecognized method name: ${asyncRequestData.methodName}`; + this.log.debug(error); + } + } + + public async sendSyncRequest(syncRequestData: any): Promise { + this.log.debug(`Inside ##sendSyncRequest()`); + + syncRequestData["syncRequest"] = true; + + try { + const block = await this.sendAsyncRequest(syncRequestData); + this.log.debug(block); + this.socket.emit("response", block.transactionReceipt); + } catch (err: any) { + this.socket.emit("error", err); + } + } +} diff --git a/packages/cactus-plugin-ledger-connector-iroha/src/test/typescript/integration/iroha-iroha-transfer-example.test.ts b/packages/cactus-plugin-ledger-connector-iroha/src/test/typescript/integration/iroha-iroha-transfer-example.test.ts index c58c8cd33e..7e5a897205 100644 --- a/packages/cactus-plugin-ledger-connector-iroha/src/test/typescript/integration/iroha-iroha-transfer-example.test.ts +++ b/packages/cactus-plugin-ledger-connector-iroha/src/test/typescript/integration/iroha-iroha-transfer-example.test.ts @@ -13,7 +13,7 @@ import { } from "@hyperledger/cactus-test-tooling"; import { PluginRegistry } from "@hyperledger/cactus-core"; import { PluginImportType } from "@hyperledger/cactus-core-api"; - +import { Server as SocketIoServer } from "socket.io"; import { IListenOptions, LogLevelDesc, @@ -34,6 +34,7 @@ import { KeyPair, } from "../../../main/typescript/generated/openapi/typescript-axios"; import cryptoHelper from "iroha-helpers-ts/lib/cryptoHelper"; +import { Constants } from "@hyperledger/cactus-core-api"; const testCase = "runs tx on an Iroha v1.2.0 ledger"; const logLevel: LogLevelDesc = "ERROR"; @@ -111,16 +112,23 @@ test.skip(testCase, async (t: Test) => { const factory1 = new PluginFactoryLedgerConnector({ pluginImportType: PluginImportType.Local, }); + + const rpcApiWsHost1 = await iroha1.getRpcApiWsHost(); + const rpcApiWsHost2 = await iroha2.getRpcApiWsHost(); + const connector1: PluginLedgerConnectorIroha = await factory1.create({ rpcToriiPortHost: rpcToriiPortHost1, instanceId: uuidv4(), + rpcApiWsHost: rpcApiWsHost1, pluginRegistry: new PluginRegistry(), }); const factory2 = new PluginFactoryLedgerConnector({ pluginImportType: PluginImportType.Local, }); + const connector2: PluginLedgerConnectorIroha = await factory2.create({ rpcToriiPortHost: rpcToriiPortHost2, + rpcApiWsHost: rpcApiWsHost2, instanceId: uuidv4(), pluginRegistry: new PluginRegistry(), }); @@ -153,10 +161,18 @@ test.skip(testCase, async (t: Test) => { const apiConfig2 = new Configuration({ basePath: apiHost2 }); const apiClient2 = new IrohaApi(apiConfig2); + const wsApi1 = new SocketIoServer(server1, { + path: Constants.SocketIoConnectionPathV1, + }); + + const wsApi2 = new SocketIoServer(server2, { + path: Constants.SocketIoConnectionPathV1, + }); + await connector1.getOrCreateWebServices(); - await connector1.registerWebServices(expressApp1); + await connector1.registerWebServices(expressApp1, wsApi1); await connector2.getOrCreateWebServices(); - await connector2.registerWebServices(expressApp2); + await connector2.registerWebServices(expressApp2, wsApi2); const adminPriv1 = await iroha1.getGenesisAccountPrivKey(); const admin1 = iroha1.getDefaultAdminAccount(); diff --git a/packages/cactus-plugin-ledger-connector-iroha/src/test/typescript/integration/openapi/openapi-validation.test.ts b/packages/cactus-plugin-ledger-connector-iroha/src/test/typescript/integration/openapi/openapi-validation.test.ts index 244db07bee..f1bf4c3237 100644 --- a/packages/cactus-plugin-ledger-connector-iroha/src/test/typescript/integration/openapi/openapi-validation.test.ts +++ b/packages/cactus-plugin-ledger-connector-iroha/src/test/typescript/integration/openapi/openapi-validation.test.ts @@ -5,6 +5,7 @@ import { v4 as uuidv4 } from "uuid"; import { v4 as internalIpV4 } from "internal-ip"; import bodyParser from "body-parser"; import express from "express"; +import { Server as SocketIoServer } from "socket.io"; import { Containers, @@ -38,6 +39,7 @@ import cryptoHelper from "iroha-helpers-ts/lib/cryptoHelper"; import OAS from "../../../../main/json/openapi.json"; import { installOpenapiValidationMiddleware } from "@hyperledger/cactus-core"; +import { Constants } from "@hyperledger/cactus-core-api"; const testCase = "Iroha plugin openapi validation"; const logLevel: LogLevelDesc = "INFO"; @@ -91,12 +93,14 @@ test(testCase, async (t: Test) => { await iroha.start(); const irohaPort = await iroha.getRpcToriiPort(); const rpcToriiPortHost = await iroha.getRpcToriiPortHost(); + const rpcApiWsHost = await iroha.getRpcApiWsHost(); const factory = new PluginFactoryLedgerConnector({ pluginImportType: PluginImportType.Local, }); const connector: PluginLedgerConnectorIroha = await factory.create({ rpcToriiPortHost, + rpcApiWsHost: rpcApiWsHost, instanceId: uuidv4(), pluginRegistry: new PluginRegistry(), }); @@ -104,6 +108,11 @@ test(testCase, async (t: Test) => { const expressApp = express(); expressApp.use(bodyParser.json({ limit: "250mb" })); const server = http.createServer(expressApp); + + const wsApi = new SocketIoServer(server, { + path: Constants.SocketIoConnectionPathV1, + }); + const listenOptions: IListenOptions = { hostname: "localhost", port: 0, @@ -123,7 +132,7 @@ test(testCase, async (t: Test) => { }); await connector.getOrCreateWebServices(); - await connector.registerWebServices(expressApp); + await connector.registerWebServices(expressApp, wsApi); const admin = iroha.getDefaultAdminAccount(); const domain = iroha.getDefaultDomain(); diff --git a/packages/cactus-plugin-ledger-connector-iroha/src/test/typescript/integration/run-transaction-endpoint-v1.test.ts b/packages/cactus-plugin-ledger-connector-iroha/src/test/typescript/integration/run-transaction-endpoint-v1.test.ts index 6ed2fa03ec..d6d03710e7 100644 --- a/packages/cactus-plugin-ledger-connector-iroha/src/test/typescript/integration/run-transaction-endpoint-v1.test.ts +++ b/packages/cactus-plugin-ledger-connector-iroha/src/test/typescript/integration/run-transaction-endpoint-v1.test.ts @@ -5,7 +5,7 @@ import { v4 as uuidv4 } from "uuid"; import { v4 as internalIpV4 } from "internal-ip"; import bodyParser from "body-parser"; import express from "express"; - +import { Server as SocketIoServer } from "socket.io"; import { Containers, pruneDockerAllIfGithubAction, @@ -35,6 +35,7 @@ import { KeyPair, } from "../../../main/typescript/generated/openapi/typescript-axios"; import cryptoHelper from "iroha-helpers-ts/lib/cryptoHelper"; +import { Constants } from "@hyperledger/cactus-core-api"; const testCase = "runs tx on an Iroha v1.2.0 ledger"; const logLevel: LogLevelDesc = "INFO"; @@ -89,6 +90,7 @@ test.skip(testCase, async (t: Test) => { await iroha.start(); const irohaPort = await iroha.getRpcToriiPort(); const rpcToriiPortHost = await iroha.getRpcToriiPortHost(); + const rpcApiWsHost = await iroha.getRpcApiWsHost(); const internalAddr = iroha.getInternalAddr(); const factory = new PluginFactoryLedgerConnector({ pluginImportType: PluginImportType.Local, @@ -96,6 +98,7 @@ test.skip(testCase, async (t: Test) => { const connector: PluginLedgerConnectorIroha = await factory.create({ rpcToriiPortHost, + rpcApiWsHost: rpcApiWsHost, instanceId: uuidv4(), pluginRegistry: new PluginRegistry(), }); @@ -108,6 +111,11 @@ test.skip(testCase, async (t: Test) => { port: 0, server, }; + + const wsApi = new SocketIoServer(server, { + path: Constants.SocketIoConnectionPathV1, + }); + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; test.onFinish(async () => await Servers.shutdown(server)); const { address, port } = addressInfo; @@ -116,7 +124,7 @@ test.skip(testCase, async (t: Test) => { const apiClient = new IrohaApi(apiConfig); await connector.getOrCreateWebServices(); - await connector.registerWebServices(expressApp); + await connector.registerWebServices(expressApp, wsApi); let firstTxHash; const admin = iroha.getDefaultAdminAccount(); diff --git a/packages/cactus-plugin-ledger-connector-iroha/src/test/typescript/integration/socket-api.test.ts b/packages/cactus-plugin-ledger-connector-iroha/src/test/typescript/integration/socket-api.test.ts new file mode 100644 index 0000000000..85b5dc1089 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-iroha/src/test/typescript/integration/socket-api.test.ts @@ -0,0 +1,444 @@ +import KeyEncoder from "key-encoder"; +import { PluginRegistry } from "@hyperledger/cactus-core"; + +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; +import { + IListenOptions, + KeyFormat, + Logger, + LoggerProvider, + LogLevelDesc, + Secp256k1Keys, + Servers, +} from "@hyperledger/cactus-common"; + +import { + IrohaTestLedger, + PostgresTestContainer, + pruneDockerAllIfGithubAction, +} from "@hyperledger/cactus-test-tooling"; + +import { v4 as internalIpV4 } from "internal-ip"; +import { v4 as uuidv4 } from "uuid"; +import { RuntimeError } from "run-time-error"; +import cryptoHelper from "iroha-helpers-ts/lib/cryptoHelper"; +import { + IrohaBlockProgress, + IrohaBlockResponse, + IrohaCommand, + KeyPair, +} from "../../../main/typescript/generated/openapi/typescript-axios"; + +import { + IPluginLedgerConnectorIrohaOptions, + IrohaApiClient, + IrohaApiClientOptions, + PluginFactoryLedgerConnector, +} from "../../../main/typescript"; + +import { AddressInfo } from "net"; +import { Constants, PluginImportType } from "@hyperledger/cactus-core-api"; +import express from "express"; +import bodyParser from "body-parser"; +import http from "http"; +import { Server as SocketIoServer } from "socket.io"; + +const logLevel: LogLevelDesc = "DEBUG"; + +const log: Logger = LoggerProvider.getOrCreate({ + label: "socket-api.test", + level: logLevel, +}); + +type IrohaLedgerInfo = { + testLedger: IrohaTestLedger; + host: string; + adminPriv: string; + adminAccount: string; + port: number; + domain: string; +}; + +type PostgresContainerInfo = { + container: PostgresTestContainer; + host: string; + port: number; +}; + +async function setupPostgres(): Promise { + const postgresTestContainer = new PostgresTestContainer({ logLevel }); + + await postgresTestContainer.start(); + + const postgresHost = await internalIpV4(); + const postgresPort = await postgresTestContainer.getPostgresPort(); + + if (!postgresHost) { + throw new RuntimeError("Could not determine the internal IPV4 address."); + } else { + return { + container: postgresTestContainer, + host: postgresHost, + port: postgresPort, + }; + } +} + +async function setupIrohaTestLedger(postgres: any): Promise { + const keyPair1: KeyPair = cryptoHelper.generateKeyPair(); + const adminPriv = keyPair1.privateKey; + const adminPubA = keyPair1.publicKey; + + const keyPair2: KeyPair = cryptoHelper.generateKeyPair(); + const nodePrivA = keyPair2.privateKey; + const nodePubA = keyPair2.publicKey; + + const iroha = new IrohaTestLedger({ + adminPriv: adminPriv, + adminPub: adminPubA, + nodePriv: nodePrivA, + nodePub: nodePubA, + postgresHost: postgres.host, + // postgresHost: "172.17.0.1", for docker + postgresPort: postgres.port, + logLevel: logLevel, + rpcApiWsPort: 50051, + }); + + log.debug("Starting Iroha test ledger"); + await iroha.start(true); + + const adminAccount = iroha.getDefaultAdminAccount(); + const irohaHost = await internalIpV4(); + const irohaPort = await iroha.getRpcToriiPort(); + const domain = iroha.getDefaultDomain(); + + if (!irohaHost) { + throw new RuntimeError("Could not determine the internal IPV4 address."); + } else { + return { + testLedger: iroha, + host: irohaHost, + adminPriv: adminPriv, + adminAccount: adminAccount, + port: irohaPort, + domain: domain, + }; + } +} + +async function createPluginRegistry(): Promise { + const keyEncoder: KeyEncoder = new KeyEncoder("secp256k1"); + const keychainRef = uuidv4(); + const keychainId = uuidv4(); + + const { privateKey } = Secp256k1Keys.generateKeyPairsBuffer(); + const keyHex = privateKey.toString("hex"); + const pem = keyEncoder.encodePrivate(keyHex, KeyFormat.Raw, KeyFormat.PEM); + + const keychain = new PluginKeychainMemory({ + backend: new Map([[keychainRef, pem]]), + keychainId, + logLevel, + instanceId: uuidv4(), + }); + + log.debug("Instantiating plugin registry"); + const pluginRegistry = new PluginRegistry({ plugins: [keychain] }); + + return pluginRegistry; +} + +describe("Iroha SocketIo TestSuite", () => { + let postgres: PostgresContainerInfo; + let iroha: IrohaLedgerInfo; + let apiClient: IrohaApiClient; + let server: http.Server; + + beforeAll(async () => { + const pruning = await pruneDockerAllIfGithubAction({ logLevel }); + expect(pruning).toBeTruthy(); + }); + + test("Prepare Iroha test ledger and Postgres Container", async () => { + log.debug("Setting up Postgres"); + postgres = await setupPostgres(); + + log.debug("Setting up Iroha test ledger"); + iroha = await setupIrohaTestLedger(postgres); + + expect(iroha).not.toBe(undefined); + expect(postgres).not.toBe(undefined); + }); + + test("Prepare API client", async () => { + const rpcApiHttpHost = await iroha.testLedger.getRpcToriiPortHost(); + const rpcApiWsHost = await iroha.testLedger.getRpcApiWsHost(); + + log.debug("Instantiating plugin registry"); + const pluginRegistry = await createPluginRegistry(); + + log.debug("Creating API server object"); + const options: IPluginLedgerConnectorIrohaOptions = { + rpcToriiPortHost: rpcApiHttpHost, + rpcApiWsHost: rpcApiWsHost, + pluginRegistry: pluginRegistry, + logLevel: logLevel, + instanceId: uuidv4(), + }; + + const factory = new PluginFactoryLedgerConnector({ + pluginImportType: PluginImportType.Local, + }); + + const connector = await factory.create(options); + + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + server = http.createServer(expressApp); + + const wsApi = new SocketIoServer(server, { + path: Constants.SocketIoConnectionPathV1, + }); + + const listenOptions: IListenOptions = { + hostname: "localhost", + port: 0, + server, + }; + + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + const { address, port } = addressInfo; + const apiHost = `http://${address}:${port}`; + + const wsBasePath = apiHost + Constants.SocketIoConnectionPathV1; + log.info(`ws base path: ${wsBasePath}`); + + const irohaApiClientOptions = new IrohaApiClientOptions({ + basePath: apiHost, + }); + apiClient = new IrohaApiClient(irohaApiClientOptions); + + await connector.getOrCreateWebServices(); + await connector.registerWebServices(expressApp, wsApi); + + expect(apiClient).not.toBe(undefined); + }); + + async function testBlock(block: IrohaBlockResponse): Promise { + log.debug("Testing given block..."); + let blockOk = true; + + const topLevelProperties = ["payload", "signaturesList"]; + + const payloadProperties = [ + "transactionsList", + "txNumber", + "height", + "prevBlockHash", + "createdTime", + "rejectedTransactionsHashesList", + ]; + + for (let iter = 0; iter < topLevelProperties.length; iter++) { + if ( + !Object.prototype.hasOwnProperty.call(block, topLevelProperties[iter]) + ) { + log.error( + `Tested block is missing property: ${topLevelProperties[iter]}`, + ); + blockOk = false; + } + if ( + block[topLevelProperties[iter] as keyof IrohaBlockResponse] === + undefined + ) { + log.error( + `Property ${topLevelProperties[iter]} is undefined: ${topLevelProperties[iter]}`, + ); + blockOk = false; + } + } + + for (let iter = 0; iter < payloadProperties.length; iter++) { + if ( + !Object.prototype.hasOwnProperty.call( + block.payload, + payloadProperties[iter], + ) + ) { + log.error( + `Payload in tested block is missing property: ${payloadProperties[iter]}`, + ); + blockOk = false; + } + } + log.debug(`Tested block is: ${blockOk ? "ok" : "not ok"}`); + return blockOk; + } + + async function createSampleAsset(assetID: string) { + // Create asset on ledger to create new block + const assetId = assetID; + + const createAssetRequest = { + commandName: IrohaCommand.CreateAsset, + baseConfig: { + irohaHost: iroha.host, + irohaPort: iroha.port, + creatorAccountId: `${iroha.adminAccount}@${iroha.domain}`, + privKey: [iroha.adminPriv], + quorum: 1, + timeoutLimit: 5000, + tls: false, + }, + params: [assetId, iroha.domain, 3], + }; + await apiClient.runTransactionV1(createAssetRequest); + } + + test("Monitoring", async () => { + const monitoringOptions = { + baseConfig: { + irohaHost: iroha.host, + irohaPort: iroha.port, + creatorAccountId: `${iroha.adminAccount}@${iroha.domain}`, + privKey: [iroha.adminPriv], + quorum: 1, + timeoutLimit: 10000, + }, + pollTime: 5000, + }; + + // Start monitoring + const blocks = apiClient.watchBlocksV1(monitoringOptions); + + // Make sample action on ledger + await createSampleAsset("coolcoin"); + + // Check for arrival of new block + const arrivedBlock = await new Promise( + (resolve, reject) => { + let done = false; + const timerId = setTimeout(() => { + if (!done) { + reject("Waiting for block notification to arrive timed out"); + } + }, 30000); + + const subscription = blocks.subscribe((res: IrohaBlockProgress) => { + subscription.unsubscribe(); + done = true; + clearTimeout(timerId); + resolve(res.transactionReceipt); + }); + }, + ); + + expect(arrivedBlock).not.toBe(undefined); + log.debug(`Block arrived: ${JSON.stringify(arrivedBlock, null, 4)}`); + + // Checking block structure + expect( + Object.prototype.hasOwnProperty.call(arrivedBlock, "payload"), + ).toBeTrue(); + expect(await testBlock(arrivedBlock)).toBeTrue(); + + log.debug("Monitoring successfully completed"); + }); + + test("Async Request", async () => { + // Start monitoring to catch block created in async request + // const blocks = apiClient.watchBlocksV1(requestData); + + const monitoringRequestData = { + baseConfig: { + irohaHost: iroha.host, + irohaPort: iroha.port, + creatorAccountId: `${iroha.adminAccount}@${iroha.domain}`, + privKey: [iroha.adminPriv], + quorum: 1, + timeoutLimit: 10000, + }, + pollTime: 3000, + }; + + const blocks = apiClient.watchBlocksV1(monitoringRequestData); + + // Create new asset to check if request was successfull + const assetID = "eth"; + const commandName = IrohaCommand.CreateAsset; + const baseConfig = { + irohaHost: iroha.host, + irohaPort: iroha.port, + creatorAccountId: `${iroha.adminAccount}@${iroha.domain}`, + privKey: [iroha.adminPriv], + quorum: 1, + timeoutLimit: 5000, + }; + const params = [assetID, iroha.domain, 3]; + + log.debug(`Sending Async Request with ${commandName} command.`); + apiClient.sendAsyncRequest(params, baseConfig, commandName); + + const arrivedBlock = await new Promise( + (resolve, reject) => { + let done = false; + const timerId = setTimeout(() => { + if (!done) { + reject("Waiting for block notification to arrive timed out"); + } + }, 30000); + + const subscription = blocks.subscribe((res: IrohaBlockProgress) => { + subscription.unsubscribe(); + done = true; + clearTimeout(timerId); + resolve(res.transactionReceipt); + }); + }, + ); + + expect(await testBlock(arrivedBlock)).toBeTrue(); + log.debug("Async call successfully completed"); + }); + + test("Sync Request", async () => { + // Get asset info on previously created coolcoin + const assetID = "btc"; + const commandName = IrohaCommand.CreateAsset; + const baseConfig = { + irohaHost: iroha.host, + irohaPort: iroha.port, + creatorAccountId: `${iroha.adminAccount}@${iroha.domain}`, + privKey: [iroha.adminPriv], + quorum: 1, + timeoutLimit: 10000, + }; + const params = [assetID, iroha.domain, 3]; + + log.debug(`Sending Sync Request with ${commandName} command.`); + const response = await apiClient.sendSyncRequest( + params, + baseConfig, + commandName, + ); + + expect(response).not.toBe(undefined || " "); + expect(Object.keys(response)).toContain("status"); + expect(Object.keys(response)).toContain("data"); + expect(response.status).toBe("COMMITTED"); + log.debug("Sync call successfully completed"); + }); + + afterAll(async () => await Servers.shutdown(server)); + + afterAll(async () => { + // Remove Iroha after all tests are done + await iroha.testLedger.stop(); + await iroha.testLedger.destroy(); + + const pruning = await pruneDockerAllIfGithubAction({ logLevel }); + expect(pruning).toBeTruthy(); + }); +}); diff --git a/packages/cactus-test-tooling/src/main/typescript/iroha/iroha-test-ledger.ts b/packages/cactus-test-tooling/src/main/typescript/iroha/iroha-test-ledger.ts index 76510a450a..4b104a2dff 100644 --- a/packages/cactus-test-tooling/src/main/typescript/iroha/iroha-test-ledger.ts +++ b/packages/cactus-test-tooling/src/main/typescript/iroha/iroha-test-ledger.ts @@ -31,6 +31,7 @@ export interface IIrohaTestLedgerOptions { readonly envVars?: string[]; readonly logLevel?: LogLevelDesc; readonly emitContainerLogs?: boolean; + readonly rpcApiWsPort?: number; } /* @@ -52,6 +53,7 @@ export const IROHA_TEST_LEDGER_DEFAULT_OPTIONS = Object.freeze({ "IROHA_POSTGRES_PASSWORD=my-secret-password", "KEY=node0", ], + rpcApiWsPort: 50052, }); /* @@ -87,6 +89,7 @@ export class IrohaTestLedger implements ITestLedger { public readonly adminPub: string; public readonly nodePriv: string; public readonly nodePub: string; + public readonly rpcApiWsPort: number; public readonly tlsCert?: string; public readonly tlsKey?: string; public readonly toriiTlsPort?: number; @@ -126,6 +129,8 @@ export class IrohaTestLedger implements ITestLedger { this.tlsKey = options.tlsKey || IROHA_TEST_LEDGER_DEFAULT_OPTIONS.tlsKey; this.toriiTlsPort = options.toriiTlsPort || IROHA_TEST_LEDGER_DEFAULT_OPTIONS.toriiTlsPort; + this.rpcApiWsPort = + options.rpcApiWsPort || IROHA_TEST_LEDGER_DEFAULT_OPTIONS.rpcApiWsPort; this.envVars.push(`IROHA_POSTGRES_HOST=${this.postgresHost}`); this.envVars.push(`IROHA_POSTGRES_PORT=${this.postgresPort}`); @@ -199,6 +204,14 @@ export class IrohaTestLedger implements ITestLedger { return "test"; } + public async getRpcApiWsHost(): Promise { + const { rpcApiWsPort } = this; + const ipAddress = "127.0.0.1"; + const containerInfo = await this.getContainerInfo(); + const port = await Containers.getPublicPort(rpcApiWsPort, containerInfo); + return `ws://${ipAddress}:${port}`; + } + /** * Output is based on the standard Iroha admin user public key file location. * 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..9f6e4e90cf 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 { + IrohaApiClient, + IrohaApiClientOptions, +} from "@hyperledger/cactus-plugin-ledger-connector-iroha"; + /** * 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; }; + IROHA: { + in: IrohaApiClientOptions; + out: IrohaApiClient; + }; }; /** @@ -55,6 +64,8 @@ export function getValidatorApiClient( case "BESU_1X": case "BESU_2X": return new BesuApiClient(options as BesuApiClientOptions); + case "IROHA": + return new IrohaApiClient(options as IrohaApiClientOptions); default: // Will not compile if any ClientApiConfig key was not handled by this switch const _: never = validatorType; diff --git a/tools/docker/iroha-testnet/docker-compose.yml b/tools/docker/iroha-testnet/docker-compose.yml index 96ec30dd52..87e1de0173 100644 --- a/tools/docker/iroha-testnet/docker-compose.yml +++ b/tools/docker/iroha-testnet/docker-compose.yml @@ -4,7 +4,8 @@ services: node: image: ${IROHA_PRJ}/${IROHA_IMG} ports: - - "50051:50051" + - "50051:50051" # http + - "50052:50052" # ws environment: - IROHA_HOME=/opt/iroha - IROHA_CONF=config.docker