From 2b57f6991aebbde38cd35a087090fc1f447ab8ec Mon Sep 17 00:00:00 2001 From: Michal Bajer Date: Mon, 28 Feb 2022 17:46:15 +0100 Subject: [PATCH] feat(cactus-api-client): common verifier-factory Adjust SocketIOApiClient and common Verifier behavior to previous one in cmd-socketio, fix related tests. Extract Verifier interfaces to common location to ensure interface compatibility between old and new verifier / verifier factory (both should implement same interface). Create new package cactus-verifier-client for common verifier related stuff, to prevent circular dependencies. Add verifier-factory to create verifiers by supplying it's ID only, based on initial configuration. Configuration is set in ctor, but can be read from a file as in cmd-socketio scenarious. VerifierFactory config should be compatible with existing ledgerPluginInfo. Closes: #1878 Signed-off-by: Michal Bajer --- .../TestEthereumVerifier.ts | 46 +++-- .../src/main/typescript/public-api.ts | 1 - .../main/typescript/socketio-api-client.ts | 32 +-- .../src/main/typescript/verifier.ts | 123 ----------- .../unit/socketio-api-client.test.ts | 26 +-- .../cactus-cmd-socketio-server/package.json | 1 + .../BusinessLogicBase.ts | 2 +- .../BusinessLogicPlugin.ts | 6 +- .../TransactionManagement.ts | 6 +- .../main/typescript/verifier/LedgerPlugin.ts | 54 +---- .../src/main/typescript/verifier/Verifier.ts | 26 +-- .../typescript/verifier/VerifierFactory.ts | 9 +- .../src/test/typescript/unit/Verifier.test.ts | 36 ++-- .../typescript/unit/VerifierFactory.test.ts | 19 +- .../cactus-cmd-socketio-server/tsconfig.json | 3 + .../src/main/typescript/client/i-verifier.ts | 47 +++++ .../ledger-connector/i-socket-api-client.ts | 2 +- .../src/main/typescript/public-api.ts | 6 + packages/cactus-test-api-client/package.json | 1 + ...ntegration-with-openapi-connectors.test.ts | 45 ++-- packages/cactus-test-api-client/tsconfig.json | 3 + packages/cactus-verifier-client/package.json | 59 ++++++ .../typescript/get-validator-api-client.ts | 63 ++++++ .../src/main/typescript/index.ts | 1 + .../src/main/typescript/public-api.ts | 7 + .../src/main/typescript/verifier-factory.ts | 170 +++++++++++++++ .../src/main/typescript/verifier.ts | 195 ++++++++++++++++++ .../unit/get-validator-api-client.test.ts | 56 +++++ .../typescript/unit/verifier-factory.test.ts | 180 ++++++++++++++++ .../src/test/typescript/unit/verifier.test.ts | 113 +++++++--- packages/cactus-verifier-client/tsconfig.json | 27 +++ tsconfig.json | 3 + 32 files changed, 1046 insertions(+), 322 deletions(-) delete mode 100644 packages/cactus-api-client/src/main/typescript/verifier.ts create mode 100644 packages/cactus-core-api/src/main/typescript/client/i-verifier.ts create mode 100644 packages/cactus-verifier-client/package.json create mode 100644 packages/cactus-verifier-client/src/main/typescript/get-validator-api-client.ts create mode 100644 packages/cactus-verifier-client/src/main/typescript/index.ts create mode 100644 packages/cactus-verifier-client/src/main/typescript/public-api.ts create mode 100644 packages/cactus-verifier-client/src/main/typescript/verifier-factory.ts create mode 100644 packages/cactus-verifier-client/src/main/typescript/verifier.ts create mode 100644 packages/cactus-verifier-client/src/test/typescript/unit/get-validator-api-client.test.ts create mode 100644 packages/cactus-verifier-client/src/test/typescript/unit/verifier-factory.test.ts rename packages/{cactus-api-client => cactus-verifier-client}/src/test/typescript/unit/verifier.test.ts (58%) create mode 100644 packages/cactus-verifier-client/tsconfig.json diff --git a/examples/cactus-check-connection-ethereum-validator/TestEthereumVerifier.ts b/examples/cactus-check-connection-ethereum-validator/TestEthereumVerifier.ts index c64e463981f..58ab1f83519 100644 --- a/examples/cactus-check-connection-ethereum-validator/TestEthereumVerifier.ts +++ b/examples/cactus-check-connection-ethereum-validator/TestEthereumVerifier.ts @@ -1,7 +1,7 @@ import { Verifier } from "../../packages/cactus-cmd-socketio-server/src/main/typescript/verifier/Verifier"; import { ConfigUtil } from "../../packages/cactus-cmd-socketio-server/src/main/typescript/routing-interface/util/ConfigUtil"; import { verifierFactory } from "../../packages/cactus-cmd-socketio-server/src/main/typescript/routing-interface/routes/index"; -import { VerifierEventListener } from "../../packages/cactus-cmd-socketio-server/src/main/typescript/verifier/LedgerPlugin"; +import { IVerifierEventListener } from "../../packages/cactus-cmd-socketio-server/src/main/typescript/verifier/LedgerPlugin"; const config: any = ConfigUtil.getConfig(); import { getLogger } from "log4js"; @@ -32,14 +32,18 @@ export class TestEthereumVerifier { this.validatorId, this.appId, this.monitorOptions, - this.monitorMode + this.monitorMode, ); } - - sendAsyncRequest(contract: object, method: { type: string; command: string; }, args: object): Promise { + sendAsyncRequest( + contract: object, + method: { type: string; command: string }, + args: object, + ): Promise { return new Promise((resolve, reject) => { - this.verifierEthereum.sendAsyncRequest(contract, method, args) + this.verifierEthereum + .sendAsyncRequest(contract, method, args) .then(() => { logger.debug(`Successfully sent async request to: ${method.command}`); resolve(true); @@ -51,7 +55,11 @@ export class TestEthereumVerifier { }); } - sendSyncRequest(contract: object, method: { type: string; command: string; }, args: object): Promise { + sendSyncRequest( + contract: object, + method: { type: string; command: string }, + args: object, + ): Promise { return new Promise((resolve, reject) => { this.verifierEthereum .sendSyncRequest(contract, method, args) @@ -60,12 +68,12 @@ export class TestEthereumVerifier { if (method.command === "getBalance") { response = { status: result.status, - amount: parseFloat(result.data) + amount: parseFloat(result.data), }; } else { response = { status: result.status, - data: result.data + data: result.data, }; } resolve(response); @@ -78,7 +86,6 @@ export class TestEthereumVerifier { } getBalance(account: string, requestType: string): any { - this.createVerifierWithoutMonitoring(); const contract = {}; @@ -93,8 +100,12 @@ export class TestEthereumVerifier { } } - transferAsset(srcAccount: string, destAccount: string, amount: string, requestType: string) { - + transferAsset( + srcAccount: string, + destAccount: string, + amount: string, + requestType: string, + ) { this.createVerifierWithoutMonitoring(); const contract = {}; @@ -119,18 +130,16 @@ export class TestEthereumVerifier { } stopMonitor() { - logger.debug(`StartingMonitor`); this.createVerifierWithoutMonitoring(); logger.debug(`Stopping Monitor`); - const blpMonitorModuleName = "BusinessLogicCheckEthereumValidator" + const blpMonitorModuleName = "BusinessLogicCheckEthereumValidator"; - this.verifierEthereum.stopMonitor(blpMonitorModuleName) + this.verifierEthereum.stopMonitor(blpMonitorModuleName); } startMonitor() { - logger.debug(`StartingMonitor`); const blpMonitorModuleName = "BusinessLogicCheckEthereumValidator"; @@ -138,9 +147,10 @@ export class TestEthereumVerifier { this.validatorId, blpMonitorModuleName, this.monitorOptions, - true) - let eventListener: VerifierEventListener + true, + ); + let eventListener: IVerifierEventListener; - verifier.startMonitor(blpMonitorModuleName, {}, eventListener) + verifier.startMonitor(blpMonitorModuleName, {}, eventListener); } } diff --git a/packages/cactus-api-client/src/main/typescript/public-api.ts b/packages/cactus-api-client/src/main/typescript/public-api.ts index d1079aeba48..8e843dd7d64 100644 --- a/packages/cactus-api-client/src/main/typescript/public-api.ts +++ b/packages/cactus-api-client/src/main/typescript/public-api.ts @@ -5,4 +5,3 @@ export { SocketLedgerEvent, SocketIOApiClientOptions, } from "./socketio-api-client"; -export { Verifier, VerifierEventListener } from "./verifier"; diff --git a/packages/cactus-api-client/src/main/typescript/socketio-api-client.ts b/packages/cactus-api-client/src/main/typescript/socketio-api-client.ts index c6e7b39a3ec..87ac852bcf3 100644 --- a/packages/cactus-api-client/src/main/typescript/socketio-api-client.ts +++ b/packages/cactus-api-client/src/main/typescript/socketio-api-client.ts @@ -8,8 +8,12 @@ const defaultMaxCounterRequestID = 100; const defaultSyncFunctionTimeoutMillisecond = 5 * 1000; // 5 seconds -import { Logger, Checks } from "@hyperledger/cactus-common"; -import { LogLevelDesc, LoggerProvider } from "@hyperledger/cactus-common"; +import { + Logger, + Checks, + LogLevelDesc, + LoggerProvider, +} from "@hyperledger/cactus-common"; import { ISocketApiClient } from "@hyperledger/cactus-core-api"; import { Socket, SocketOptions, ManagerOptions, io } from "socket.io-client"; @@ -77,11 +81,10 @@ export type SocketIOApiClientOptions = { /** * Type of the message emitted from ledger monitoring. */ -export class SocketLedgerEvent { - id = ""; - verifierId = ""; - data: Record | null = null; -} +export type SocketLedgerEvent = { + status: number; + blockData: [Record]; +}; /** * Client for sending requests to some socketio ledger connectors (validators) using socketio protocol. @@ -330,13 +333,9 @@ export class SocketIOApiClient implements ISocketApiClient { status: res.status, blockData: decodedData.blockData, }; - this.log.debug("resultObj =", resultObj); - const event = new SocketLedgerEvent(); - event.verifierId = this.options.validatorID; - this.log.debug(`##event.verifierId: ${event.verifierId}`); - event.data = resultObj; + this.log.debug("resultObj=", resultObj); if (this.monitorSubject) { - this.monitorSubject.next(event); + this.monitorSubject.next(resultObj); } }) .catch((err) => { @@ -392,4 +391,11 @@ export class SocketIOApiClient implements ISocketApiClient { } return `${this.options.validatorID}_${this.counterReqID++}`; } + + /** + * Closes internal socket.io connection to the validator. + */ + public close(): void { + this.socket.close(); + } } diff --git a/packages/cactus-api-client/src/main/typescript/verifier.ts b/packages/cactus-api-client/src/main/typescript/verifier.ts deleted file mode 100644 index 1acfddd5793..00000000000 --- a/packages/cactus-api-client/src/main/typescript/verifier.ts +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright 2020-2021 Hyperledger Cactus Contributors - * SPDX-License-Identifier: Apache-2.0 - * - * Verifier.ts - */ - -import { Subscription } from "rxjs"; - -import { Logger } from "@hyperledger/cactus-common"; -import { LogLevelDesc, LoggerProvider } from "@hyperledger/cactus-common"; -import { ISocketApiClient } from "@hyperledger/cactus-core-api"; - -/** - * Interface required for monitoring ledger event using callback to Verifier.startMonitor() - */ -export interface VerifierEventListener { - onEvent(ledgerEvent: BlockType): void; - onError?(err: any): void; -} - -/** - * Utility type for retrieving monitoring event / new block type from generic ISocketApiClient interface. - */ -type BlockTypeFromSocketApi = T extends ISocketApiClient - ? U - : never; - -/** - * Extends ledger connector ApiClient with additional monitoring methods (using callbacks, instead of reactive). - * - * @remarks - * Migrated from cmd-socketio-server for merging the codebases. - */ -export class Verifier> { - private readonly log: Logger; - readonly className: string; - readonly runningMonitors = new Map(); - - /** - * @param ledgerApi - ApiClient for communicating with ledger connector plugin (must implement `ISocketApiClient`) - * @param logLevel - Log level used by the Verifier. - */ - constructor( - public readonly ledgerApi: LedgerApiType, - logLevel: LogLevelDesc = "INFO", - ) { - this.className = this.constructor.name; - this.log = LoggerProvider.getOrCreate({ - level: logLevel, - label: this.className, - }); - this.log.debug("Created Verifier for ledger API"); - } - - /** - * Start monitoring for new events / blocks from underlying ledger. - * - * @param appId - Used to track different apps that use the monitoring. - * Each app has one subscription to common monitoring subject returned by the ApiClient watch method. - * @param eventListener - Type that supplies callbacks called when new event / error was encountered. - * @param monitorOptions - Options passed to the validator. - */ - startMonitor( - appId: string, - eventListener: VerifierEventListener>, - monitorOptions?: Record, - ): void { - if (this.runningMonitors.has(appId)) { - throw new Error(`Monitor with appId '${appId}' is already running!`); - } - - this.log.debug("call : startMonitor appId =", appId); - - try { - const blocksObservable = this.ledgerApi.watchBlocksV1(monitorOptions); - - const watchBlocksSub = blocksObservable.subscribe({ - next: (blockData: unknown) => { - eventListener.onEvent( - blockData as BlockTypeFromSocketApi, - ); - }, - error: (err) => { - this.log.error("Error when watching for new blocks, err:", err); - if (eventListener.onError) { - eventListener.onError(err); - } - }, - complete: () => { - this.log.info("Watch completed"); - }, - }); - - this.runningMonitors.set(appId, watchBlocksSub); - this.log.debug( - "New monitor added, runningMonitors.size ==", - this.runningMonitors.size, - ); - } catch (err) { - this.log.error(`##Error: startMonitor, ${err}`); - this.runningMonitors.delete(appId); - } - } - - /** - * Stops the monitor for specified app, removes it's subscription from internal storage. - * - * @param appId - ID of application that requested the monitoring. - */ - stopMonitor(appId: string): void { - const watchBlocksSub = this.runningMonitors.get(appId); - if (!watchBlocksSub) { - throw new Error("No monitor running with appId: " + appId); - } - watchBlocksSub.unsubscribe(); - this.runningMonitors.delete(appId); - this.log.debug( - "Monitor removed, runningMonitors.size ==", - this.runningMonitors.size, - ); - } -} diff --git a/packages/cactus-api-client/src/test/typescript/unit/socketio-api-client.test.ts b/packages/cactus-api-client/src/test/typescript/unit/socketio-api-client.test.ts index c8a4cdf9bcc..3f514e7a87e 100644 --- a/packages/cactus-api-client/src/test/typescript/unit/socketio-api-client.test.ts +++ b/packages/cactus-api-client/src/test/typescript/unit/socketio-api-client.test.ts @@ -533,21 +533,9 @@ describe("SocketIOApiClient Tests", function () { // Receive events from the validator sut.watchBlocksV1(options).subscribe({ next(ev: SocketLedgerEvent) { - expect(ev.id).toEqual(""); - expect(ev.verifierId).toEqual(defaultConfigOptions.validatorID); - - if (!ev.data) { - done("Event data is empty or null!"); - } else { - expect((ev.data as { [key: string]: number })["status"]).toEqual( - eventStatus, - ); - expect((ev.data as { [key: string]: string })["blockData"]).toEqual( - decryptedBlockData, - ); - - done(); - } + expect(ev.status).toEqual(eventStatus); + expect(ev.blockData).toEqual(decryptedBlockData); + done(); }, error(err) { done(err); @@ -623,7 +611,7 @@ describe("SocketIOApiClient Tests", function () { (resolve, reject) => { blockObservable.subscribe({ next(ev: SocketLedgerEvent) { - if (!ev.data) { + if (!ev.blockData) { reject("First event data is empty or null!"); // todo - test negative } else { log.info("First observer received event:", ev); @@ -642,7 +630,7 @@ describe("SocketIOApiClient Tests", function () { (resolve, reject) => { blockObservable.subscribe({ next(ev: SocketLedgerEvent) { - if (!ev.data) { + if (!ev.blockData) { reject("Second event data is empty or null!"); } else { log.info("Second observer received event:", ev); @@ -684,7 +672,7 @@ describe("SocketIOApiClient Tests", function () { (resolve, reject) => { const sub = sut.watchBlocksV1(options).subscribe({ next(ev: SocketLedgerEvent) { - if (!ev.data) { + if (!ev.blockData) { sub.unsubscribe(); reject("First event data is empty or null!"); // todo - test negative } else { @@ -705,7 +693,7 @@ describe("SocketIOApiClient Tests", function () { (resolve, reject) => { const sub = sut.watchBlocksV1(options).subscribe({ next(ev: SocketLedgerEvent) { - if (!ev.data) { + if (!ev.blockData) { sub.unsubscribe(); reject("2nd event data is empty or null!"); // todo - test negative } else { diff --git a/packages/cactus-cmd-socketio-server/package.json b/packages/cactus-cmd-socketio-server/package.json index a0bc6271594..fc1ad06b014 100644 --- a/packages/cactus-cmd-socketio-server/package.json +++ b/packages/cactus-cmd-socketio-server/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@hyperledger/cactus-common": "1.0.0-rc.3", + "@hyperledger/cactus-core-api": "1.0.0-rc.3", "@types/node": "^14.0.24", "body-parser": "^1.19.0", "cookie-parser": "1.4.5", diff --git a/packages/cactus-cmd-socketio-server/src/main/typescript/business-logic-plugin/BusinessLogicBase.ts b/packages/cactus-cmd-socketio-server/src/main/typescript/business-logic-plugin/BusinessLogicBase.ts index a62fd1fd924..4e3a21210da 100644 --- a/packages/cactus-cmd-socketio-server/src/main/typescript/business-logic-plugin/BusinessLogicBase.ts +++ b/packages/cactus-cmd-socketio-server/src/main/typescript/business-logic-plugin/BusinessLogicBase.ts @@ -7,7 +7,7 @@ import { Request } from "express"; import { BusinessLogicPlugin } from "./BusinessLogicPlugin"; -import { VerifierEventListener, LedgerEvent } from "../verifier/LedgerPlugin"; +import { LedgerEvent } from "../verifier/LedgerPlugin"; import { json2str } from "../verifier/DriverCommon"; import { ConfigUtil } from "../routing-interface/util/ConfigUtil"; diff --git a/packages/cactus-cmd-socketio-server/src/main/typescript/business-logic-plugin/BusinessLogicPlugin.ts b/packages/cactus-cmd-socketio-server/src/main/typescript/business-logic-plugin/BusinessLogicPlugin.ts index f3d481f4ee2..ccda2293031 100644 --- a/packages/cactus-cmd-socketio-server/src/main/typescript/business-logic-plugin/BusinessLogicPlugin.ts +++ b/packages/cactus-cmd-socketio-server/src/main/typescript/business-logic-plugin/BusinessLogicPlugin.ts @@ -6,13 +6,13 @@ */ import { Request } from "express"; -import { VerifierEventListener, LedgerEvent } from "../verifier/LedgerPlugin"; +import { LedgerEvent } from "../verifier/LedgerPlugin"; export interface BusinessLogicPlugin { startTransaction( req: Request, businessLogicID: string, - tradeID: string + tradeID: string, ): void; getOperationStatus(tradeID: string): object; setConfig(data: []): object; @@ -20,7 +20,7 @@ export interface BusinessLogicPlugin { getEventDataNum(ledgerEvent: LedgerEvent): number; getTxIDFromEvent( ledgerEvent: LedgerEvent, - targetIndex: number + targetIndex: number, ): string | null; hasTxIDInTransactions(txID: string): boolean; } diff --git a/packages/cactus-cmd-socketio-server/src/main/typescript/routing-interface/TransactionManagement.ts b/packages/cactus-cmd-socketio-server/src/main/typescript/routing-interface/TransactionManagement.ts index 6aec5153d35..ae251825b32 100644 --- a/packages/cactus-cmd-socketio-server/src/main/typescript/routing-interface/TransactionManagement.ts +++ b/packages/cactus-cmd-socketio-server/src/main/typescript/routing-interface/TransactionManagement.ts @@ -11,7 +11,7 @@ import { BLPRegistry } from "./util/BLPRegistry"; import { LPInfoHolder } from "./util/LPInfoHolder"; import { json2str } from "../verifier/DriverCommon"; import { Verifier } from "../verifier/Verifier"; -import { VerifierEventListener, LedgerEvent } from "../verifier/LedgerPlugin"; +import { IVerifierEventListener, LedgerEvent } from "../verifier/LedgerPlugin"; import { getTargetBLPInstance } from "../business-logic-plugin/BLP_config"; import { ConfigUtil } from "./util/ConfigUtil"; @@ -23,7 +23,7 @@ const moduleName = "TransactionManagement"; const logger = getLogger(`${moduleName}`); logger.level = config.logLevel; -export class TransactionManagement implements VerifierEventListener { +export class TransactionManagement implements IVerifierEventListener { private blpRegistry: BLPRegistry; // Verifier information used in business logic // private connectInfo: LPInfoHolder = null; // connection information // private verifierArray: [] = []; // Verifier @@ -168,7 +168,7 @@ export class TransactionManagement implements VerifierEventListener { return this.blpRegistry.getBLPRegistryInfo(businessLogicId); } - // interface VerifierEventListener + // interface IVerifierEventListener onEvent(ledgerEvent: LedgerEvent): void { // logger.debug(`####in onEvent: event: ${json2str(ledgerEvent)}`); const eventNum = this.getEventNum(ledgerEvent); diff --git a/packages/cactus-cmd-socketio-server/src/main/typescript/verifier/LedgerPlugin.ts b/packages/cactus-cmd-socketio-server/src/main/typescript/verifier/LedgerPlugin.ts index 95781f8aab6..a27e182cf43 100644 --- a/packages/cactus-cmd-socketio-server/src/main/typescript/verifier/LedgerPlugin.ts +++ b/packages/cactus-cmd-socketio-server/src/main/typescript/verifier/LedgerPlugin.ts @@ -5,29 +5,11 @@ * LedgerPlugin.ts */ -import { LedgerOperation } from "./../business-logic-plugin/LedgerOperation"; - -export interface IVerifier { - // BLP -> Verifier - sendAsyncRequest( - contract: object, - method: object, - args: object - ): Promise; - sendSyncRequest(contract: object, method: object, args: object): Promise; - startMonitor( - id: string, - options: Object, - eventListener: VerifierEventListener - ): Promise; - stopMonitor(id?: string): void; - - // Validator -> Verifier - // NOTE: The following methods are not implemented this time - // connect(): void; - // disconnect(): void; - // getVerifierInfo(): VerifierInfo[]; -} +export { + IVerifier, + LedgerEvent, + IVerifierEventListener, +} from "@hyperledger/cactus-core-api"; export class ApiInfo { apiType = ""; @@ -39,29 +21,3 @@ export class RequestedData { dataType = ""; } -export class LedgerEvent { - id = ""; - verifierId = ""; - data: object | null = null; - // NOTE: A class that represents an event. - // The purpose is to receive the event of Ledger on the Verifier side. -} - -export interface VerifierEventListener { - onEvent(ledgerEvent: LedgerEvent): void; - // getEventFilter(): object | null; - // isTargetEvent(ledgerEvent: LedgerEvent): boolean; -} - -// NOTE: The following methods are not implemented this time -// class VerifierInfo { -// version: string = ""; -// name: string = ""; -// ID: string = ""; -// otherData: VerifierInfoOtherData[] = []; -// } - -// class VerifierInfoOtherData { -// dataName: string = ""; -// dataType: string[] = []; -// } diff --git a/packages/cactus-cmd-socketio-server/src/main/typescript/verifier/Verifier.ts b/packages/cactus-cmd-socketio-server/src/main/typescript/verifier/Verifier.ts index 1fd8f4a65cb..c0fd11a8478 100644 --- a/packages/cactus-cmd-socketio-server/src/main/typescript/verifier/Verifier.ts +++ b/packages/cactus-cmd-socketio-server/src/main/typescript/verifier/Verifier.ts @@ -9,7 +9,7 @@ import { IVerifier, ApiInfo, LedgerEvent, - VerifierEventListener, + IVerifierEventListener, } from "./LedgerPlugin"; import { json2str } from "./DriverCommon"; import { LedgerOperation } from "../business-logic-plugin/LedgerOperation"; @@ -45,7 +45,7 @@ export class Verifier implements IVerifier { validatorKeyPath = ""; apiInfo: Array = []; counterReqID = 1; - eventListenerHash: { [key: string]: VerifierEventListener } = {}; // Listeners for events from Ledger + eventListenerHash: { [key: string]: IVerifierEventListener } = {}; // Listeners for events from Ledger static mapUrlSocket: Map = new Map(); checkValidator: (key: string, data: string) => Promise = VerifierAuthentication.verify; @@ -322,11 +322,11 @@ export class Verifier implements IVerifier { static makeOpenApiEvent(resp: object, validatorID: string): LedgerEvent { logger.debug(`##in makeOpenApiEvent, resp = ${JSON.stringify(resp)}`); - const event = new LedgerEvent(); - event.verifierId = validatorID; - // TODO: for debug - const txID = "openapi-txid-00001"; - event.data = { txId: txID, blockData: [resp] }; + const event: LedgerEvent = { + id: "", + verifierId: validatorID, + data: { txId: "openapi-txid-00001", blockData: [resp] }, + }; logger.debug(`##event: ${JSON.stringify(event)}`); return event; } @@ -334,7 +334,7 @@ export class Verifier implements IVerifier { public startMonitor( id: string, options: Object, - eventListener: VerifierEventListener, + eventListener: IVerifierEventListener, ): Promise { return new Promise((resolve, reject) => { logger.debug("call : startMonitor"); @@ -394,10 +394,12 @@ export class Verifier implements IVerifier { blockData: decodedData.blockData, }; logger.debug("resultObj =", resultObj); - const event = new LedgerEvent(); - event.verifierId = this.validatorID; + const event: LedgerEvent = { + id: "", + verifierId: this.validatorID, + data: resultObj, + }; logger.debug(`##event.verifierId: ${event.verifierId}`); - event.data = resultObj; for (const key in this.eventListenerHash) { const eventListener = this.eventListenerHash[key]; if (eventListener != null) { @@ -464,7 +466,7 @@ export class Verifier implements IVerifier { private setEventListener( appId: string, - eventListener: VerifierEventListener | null, + eventListener: IVerifierEventListener | null, ): void { logger.debug(`##call : setEventListener`); if (eventListener) { diff --git a/packages/cactus-cmd-socketio-server/src/main/typescript/verifier/VerifierFactory.ts b/packages/cactus-cmd-socketio-server/src/main/typescript/verifier/VerifierFactory.ts index 63532d656f4..7330afed426 100644 --- a/packages/cactus-cmd-socketio-server/src/main/typescript/verifier/VerifierFactory.ts +++ b/packages/cactus-cmd-socketio-server/src/main/typescript/verifier/VerifierFactory.ts @@ -11,7 +11,7 @@ import { ConfigUtil } from "../routing-interface/util/ConfigUtil"; const config: any = ConfigUtil.getConfig(); import { getLogger } from "log4js"; -import { VerifierEventListener } from "./LedgerPlugin"; +import { IVerifierEventListener } from "./LedgerPlugin"; const moduleName = "VerifierFactory"; const logger = getLogger(`${moduleName}`); logger.level = config.logLevel; @@ -20,7 +20,7 @@ export class VerifierFactory { static verifierHash: { [key: string]: Verifier } = {}; // Verifier constructor( - private eventListener: VerifierEventListener, + private eventListener: IVerifierEventListener, private connectInfo = new LPInfoHolder(), ) {} @@ -36,8 +36,9 @@ export class VerifierFactory { if (VerifierFactory.verifierHash[validatorId]) { return VerifierFactory.verifierHash[validatorId]; } else { - const ledgerPluginInfo: string = - this.connectInfo.getLegerPluginInfo(validatorId); + const ledgerPluginInfo: string = this.connectInfo.getLegerPluginInfo( + validatorId, + ); // TODO: I want to manage an instance using the validatorId as a key instead of a dedicated member variable VerifierFactory.verifierHash[validatorId] = new Verifier( ledgerPluginInfo, diff --git a/packages/cactus-cmd-socketio-server/src/test/typescript/unit/Verifier.test.ts b/packages/cactus-cmd-socketio-server/src/test/typescript/unit/Verifier.test.ts index 57b9e03480f..5cdc1d9d7ca 100644 --- a/packages/cactus-cmd-socketio-server/src/test/typescript/unit/Verifier.test.ts +++ b/packages/cactus-cmd-socketio-server/src/test/typescript/unit/Verifier.test.ts @@ -73,7 +73,7 @@ import { Verifier } from "../../../main/typescript/verifier/Verifier"; import { LedgerEvent, - VerifierEventListener, + IVerifierEventListener, } from "../../../main/typescript/verifier/LedgerPlugin"; import { SocketIOTestSetupHelpers } from "@hyperledger/cactus-test-tooling"; @@ -172,8 +172,10 @@ describe("SocketIO Validator Tests", function () { let sut: Verifier; beforeAll(async () => { - [testServer, testServerPort] = - await SocketIOTestSetupHelpers.createListeningMockServer(); + [ + testServer, + testServerPort, + ] = await SocketIOTestSetupHelpers.createListeningMockServer(); }, setupTimeout); afterAll((done) => { @@ -358,7 +360,7 @@ describe("SocketIO Validator Tests", function () { const appId = "TestTrade"; const options = { opt: "yes" }; const onEventMock = jest.fn(); - const listenerMock: VerifierEventListener = { + const listenerMock: IVerifierEventListener = { onEvent: onEventMock, }; @@ -405,12 +407,8 @@ describe("SocketIO Validator Tests", function () { if (!ev.data) { done("Event data is empty or null!"); } else { - expect((ev.data as { [key: string]: number })["status"]).toEqual( - eventStatus, - ); - expect((ev.data as { [key: string]: string })["blockData"]).toEqual( - decryptedBlockData, - ); + expect(ev.data.status).toEqual(eventStatus); + expect(ev.data.blockData).toEqual(decryptedBlockData); } if (onEventCalledTimes === 2) { @@ -419,8 +417,8 @@ describe("SocketIO Validator Tests", function () { }; // Two listeners - const listenerMockFirst: VerifierEventListener = { onEvent: onEventMock }; - const listenerMockSecond: VerifierEventListener = { + const listenerMockFirst: IVerifierEventListener = { onEvent: onEventMock }; + const listenerMockSecond: IVerifierEventListener = { onEvent: onEventMock, }; @@ -576,15 +574,9 @@ test("makeOpenApiEvent creates valid ledger event", () => { expect(event.id).toEqual(""); expect(event.verifierId).toEqual(validatorId); - expect( - (event.data as { [key: string]: string })["txId"].length, - ).toBeGreaterThan(0); - expect( - (event.data as { [key: string]: Array })["blockData"].length, - ).toEqual(1); - expect( - (event.data as { [key: string]: Array })["blockData"][0], - ).toEqual(response); + expect(event.data!.txId!.length).toBeGreaterThan(0); + expect(event.data!.blockData.length).toEqual(1); + expect(event.data!.blockData[0]).toEqual(response); }); describe("OpenAPI Async Request Tests", function () { @@ -633,7 +625,7 @@ describe("OpenAPI Async Request Tests", function () { test("Sends event from ledger to listeners for openapi verifier", async () => { const onEventMock = jest.fn(); - const listenerMock: VerifierEventListener = { + const listenerMock: IVerifierEventListener = { onEvent: onEventMock, }; sut.eventListenerHash["TestApp"] = listenerMock; diff --git a/packages/cactus-cmd-socketio-server/src/test/typescript/unit/VerifierFactory.test.ts b/packages/cactus-cmd-socketio-server/src/test/typescript/unit/VerifierFactory.test.ts index 0d98ffc2a3a..178f65c1c1d 100644 --- a/packages/cactus-cmd-socketio-server/src/test/typescript/unit/VerifierFactory.test.ts +++ b/packages/cactus-cmd-socketio-server/src/test/typescript/unit/VerifierFactory.test.ts @@ -42,14 +42,14 @@ import { Verifier } from "../../../main/typescript/verifier/Verifier"; jest.mock("../../../main/typescript/verifier/Verifier"); import { VerifierFactory } from "../../../main/typescript/verifier/VerifierFactory"; -import { VerifierEventListener } from "../../../main/typescript/verifier/LedgerPlugin"; +import { IVerifierEventListener } from "../../../main/typescript/verifier/LedgerPlugin"; ////////////////////////// // UNIT TESTS ///////////////////////// describe("VerifierFactory getVerifier tests", () => { - const listenerMock: VerifierEventListener = { + const listenerMock: IVerifierEventListener = { onEvent: () => log.warn("listenerMock::onEvent() called!"), }; const mockInfoHolder = new LPInfoHolder(); @@ -78,12 +78,21 @@ describe("VerifierFactory getVerifier tests", () => { test("Starts monitoring when requested on newly added verifier", () => { const verifierId = "someVerifierId"; const appId = "myAppId"; - const monitorOptions = {name: "debug", debug: true}; + const monitorOptions = { name: "debug", debug: true }; - const newVerifier = sut.getVerifier(verifierId, appId, monitorOptions, true); + const newVerifier = sut.getVerifier( + verifierId, + appId, + monitorOptions, + true, + ); expect(Object.keys(VerifierFactory.verifierHash).length).toBe(1); - expect(newVerifier.startMonitor).toBeCalledWith(appId, monitorOptions, listenerMock); + expect(newVerifier.startMonitor).toBeCalledWith( + appId, + monitorOptions, + listenerMock, + ); }); test("Reuses already seen verifier from the hash", () => { diff --git a/packages/cactus-cmd-socketio-server/tsconfig.json b/packages/cactus-cmd-socketio-server/tsconfig.json index a4f258dbbc7..da82ead53f5 100644 --- a/packages/cactus-cmd-socketio-server/tsconfig.json +++ b/packages/cactus-cmd-socketio-server/tsconfig.json @@ -21,6 +21,9 @@ { "path": "../cactus-common/tsconfig.json" }, + { + "path": "../cactus-core-api/tsconfig.json" + }, { "path": "../cactus-test-tooling/tsconfig.json" } diff --git a/packages/cactus-core-api/src/main/typescript/client/i-verifier.ts b/packages/cactus-core-api/src/main/typescript/client/i-verifier.ts new file mode 100644 index 00000000000..6db13b4a606 --- /dev/null +++ b/packages/cactus-core-api/src/main/typescript/client/i-verifier.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2020-2022 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * verifier-factory.ts + */ + +/** + * A class that represents an event. + * The purpose is to receive the event of Ledger on the Verifier side. + */ +export type LedgerEvent = { + id: string; + verifierId: string; + data: BlockType | null; +}; + +/** + * Interface required for monitoring ledger event using callback to Verifier.startMonitor() + */ +export interface IVerifierEventListener { + onEvent(ledgerEvent: LedgerEvent): void; + onError?(err: any): void; +} + +/** + * Common interface for common verifier implementation. + */ +export interface IVerifier { + // BLP -> Verifier + sendAsyncRequest( + contract: Record, + method: Record, + args: Record, + ): Promise; + sendSyncRequest( + contract: Record, + method: Record, + args: Record, + ): Promise; + startMonitor( + id: string, + options: Record, + eventListener: IVerifierEventListener, + ): void; + stopMonitor(id?: string): void; +} 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 30cfe9c7db3..48d30acf739 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 @@ -19,7 +19,7 @@ export interface ISocketApiClient { args: any, ): Promise; - watchBlocksV1( + watchBlocksV1?( monitorOptions?: Record, ): Observable; } diff --git a/packages/cactus-core-api/src/main/typescript/public-api.ts b/packages/cactus-core-api/src/main/typescript/public-api.ts index ce2ffe7d3af..31b6e40b61e 100755 --- a/packages/cactus-core-api/src/main/typescript/public-api.ts +++ b/packages/cactus-core-api/src/main/typescript/public-api.ts @@ -36,3 +36,9 @@ export { export { IPluginObjectStore } from "./plugin/object-store/i-plugin-object-store"; export { isIPluginObjectStore } from "./plugin/object-store/is-i-plugin-object-store"; + +export { + IVerifier, + LedgerEvent, + IVerifierEventListener, +} from "./client/i-verifier"; diff --git a/packages/cactus-test-api-client/package.json b/packages/cactus-test-api-client/package.json index 1360b5897b2..c85b8e4ef27 100644 --- a/packages/cactus-test-api-client/package.json +++ b/packages/cactus-test-api-client/package.json @@ -67,6 +67,7 @@ "@hyperledger/cactus-core-api": "1.0.0-rc.3", "@hyperledger/cactus-plugin-consortium-manual": "1.0.0-rc.3", "@hyperledger/cactus-plugin-ledger-connector-quorum": "1.0.0-rc.3", + "@hyperledger/cactus-verifier-client": "1.0.0-rc.3", "jose": "4.1.0", "web3": "1.5.2" }, diff --git a/packages/cactus-test-api-client/src/test/typescript/integration/verifier-integration-with-openapi-connectors.test.ts b/packages/cactus-test-api-client/src/test/typescript/integration/verifier-integration-with-openapi-connectors.test.ts index 53c509294ff..6b8a1a4d592 100644 --- a/packages/cactus-test-api-client/src/test/typescript/integration/verifier-integration-with-openapi-connectors.test.ts +++ b/packages/cactus-test-api-client/src/test/typescript/integration/verifier-integration-with-openapi-connectors.test.ts @@ -42,8 +42,9 @@ import { AddressInfo } from "net"; import { BesuApiClientOptions } from "@hyperledger/cactus-plugin-ledger-connector-besu"; import { Verifier, - VerifierEventListener, -} from "@hyperledger/cactus-api-client"; + IVerifierEventListener, + LedgerEvent, +} from "@hyperledger/cactus-verifier-client"; // Unit Test logger setup const log: Logger = LoggerProvider.getOrCreate({ @@ -159,7 +160,7 @@ describe("Verifier integration with openapi connectors tests", () => { ////////////////////////////////// test("Verifier is constructed on BesuApiClient", async () => { - const sut = new Verifier(apiClient, sutLogLevel); + const sut = new Verifier("BESU_2X", apiClient, sutLogLevel); expect(sut.ledgerApi).toBe(apiClient); }); @@ -202,19 +203,28 @@ describe("Verifier integration with openapi connectors tests", () => { return expect(newBlock).toResolve(); }); - test("Verifier works with BesuApiClient", async () => { + test("Verifier works with BesuApiClient", () => { const newBlock = new Promise((resolve, reject) => { const appId = "testMonitor"; - const sut = new Verifier(apiClient, sutLogLevel); - - const monitor: VerifierEventListener = { - onEvent(ledgerEvent: WatchBlocksV1Progress): void { - log.info( - "Listener received ledgerEvent, block number", - ledgerEvent.blockHeader.number, - ); - sut.stopMonitor(appId); - resolve(ledgerEvent); + const sut = new Verifier("BESU_2X", apiClient, sutLogLevel); + + const monitor: IVerifierEventListener = { + onEvent(ledgerEvent: LedgerEvent): void { + try { + log.info("Received event:", ledgerEvent); + + if (!ledgerEvent.data) { + throw Error("No block data"); + } + log.info( + "Listener received ledgerEvent, block number", + ledgerEvent.data.blockHeader.number, + ); + sut.stopMonitor(appId); + resolve(ledgerEvent.data); + } catch (err) { + reject(err); + } }, onError(err: any): void { log.error("Ledger monitoring error:", err); @@ -222,10 +232,11 @@ describe("Verifier integration with openapi connectors tests", () => { }, }; - sut.startMonitor(appId, monitor); + sut.startMonitor(appId, {}, monitor); }); - await sendTransactionOnBesuLedger(); - return expect(newBlock).toResolve(); + return sendTransactionOnBesuLedger().then(() => + expect(newBlock).not.toReject(), + ); }); }); diff --git a/packages/cactus-test-api-client/tsconfig.json b/packages/cactus-test-api-client/tsconfig.json index 3d0ea23dd1b..35a6dd15b68 100644 --- a/packages/cactus-test-api-client/tsconfig.json +++ b/packages/cactus-test-api-client/tsconfig.json @@ -15,6 +15,9 @@ { "path": "../cactus-api-client/tsconfig.json" }, + { + "path": "../cactus-verifier-client/tsconfig.json" + }, { "path": "../cactus-cmd-api-server/tsconfig.json" }, diff --git a/packages/cactus-verifier-client/package.json b/packages/cactus-verifier-client/package.json new file mode 100644 index 00000000000..451bf767d1d --- /dev/null +++ b/packages/cactus-verifier-client/package.json @@ -0,0 +1,59 @@ +{ + "name": "@hyperledger/cactus-verifier-client", + "version": "1.0.0-rc.3", + "description": "Verifier cactus client library to communicate with validators through socket.io", + "main": "dist/main/typescript/index.js", + "module": "dist/main/typescript/index.js", + "types": "dist/main/typescript/index.d.ts", + "scripts": { + "build": "tsc" + }, + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/hyperledger/cactus.git" + }, + "keywords": [ + "Hyperledger", + "Cactus", + "Integration", + "Blockchain", + "Distributed Ledger Technology" + ], + "author": { + "name": "Hyperledger Cactus Contributors", + "email": "cactus@lists.hyperledger.org", + "url": "https://www.hyperledger.org/use/cactus" + }, + "contributors": [ + { + "name": "Please add yourself to the list of contributors", + "email": "your.name@example.com", + "url": "https://example.com" + }, + { + "name": "Michal Bajer", + "email": "michal.bajer@fujitsu.com", + "url": "https://www.fujitsu.com/global/" + } + ], + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/hyperledger/cactus/issues" + }, + "homepage": "https://github.com/hyperledger/cactus#readme", + "dependencies": { + "@hyperledger/cactus-api-client": "1.0.0-rc.3", + "@hyperledger/cactus-common": "1.0.0-rc.3", + "@hyperledger/cactus-core-api": "1.0.0-rc.3", + "@hyperledger/cactus-plugin-ledger-connector-besu": "1.0.0-rc.3", + "jest-extended": "0.11.5", + "rxjs": "7.3.0" + } +} diff --git a/packages/cactus-verifier-client/src/main/typescript/get-validator-api-client.ts b/packages/cactus-verifier-client/src/main/typescript/get-validator-api-client.ts new file mode 100644 index 00000000000..9cf062d10aa --- /dev/null +++ b/packages/cactus-verifier-client/src/main/typescript/get-validator-api-client.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2020-2022 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * get-validator-api-client.ts + */ + +import { + SocketIOApiClient, + SocketIOApiClientOptions, +} from "@hyperledger/cactus-api-client"; + +import { + BesuApiClient, + BesuApiClientOptions, +} from "@hyperledger/cactus-plugin-ledger-connector-besu"; + +/** + * 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. + * Each entry value defines two values: in, for input options for ApiClient, and out, for type of ApiClient. + * @warning Remember to keep this list updated to have new ApiClients visible in VerifierFactory interface. + */ +export type ClientApiConfig = { + "legacy-socketio": { + in: SocketIOApiClientOptions; + out: SocketIOApiClient; + }; + BESU_1X: { + in: BesuApiClientOptions; + out: BesuApiClient; + }; + BESU_2X: { + in: BesuApiClientOptions; + out: BesuApiClient; + }; +}; + +/** + * Getter function for ApiClient based only on it's type/ledger name. + * + * @param validatorType: what kind of validator to create, must be defined in ClientApiConfig. + * @param options: Configuration for given ApiClients, depends on validatorType supplied earlier + * @returns Api coresponding to validatorType requested in arguments. + * + * @todo Use dynamic import to save space and not require all the ApiClient packages. + */ +export function getValidatorApiClient( + validatorType: K, + options: ClientApiConfig[K]["in"], +): ClientApiConfig[K]["out"] { + switch (validatorType) { + case "legacy-socketio": + return new SocketIOApiClient(options as SocketIOApiClientOptions); + case "BESU_1X": + case "BESU_2X": + return new BesuApiClient(options as BesuApiClientOptions); + default: + // Will not compile if any ClientApiConfig key was not handled by this switch + const _: never = validatorType; + return _; + } +} diff --git a/packages/cactus-verifier-client/src/main/typescript/index.ts b/packages/cactus-verifier-client/src/main/typescript/index.ts new file mode 100644 index 00000000000..87cb558397c --- /dev/null +++ b/packages/cactus-verifier-client/src/main/typescript/index.ts @@ -0,0 +1 @@ +export * from "./public-api"; diff --git a/packages/cactus-verifier-client/src/main/typescript/public-api.ts b/packages/cactus-verifier-client/src/main/typescript/public-api.ts new file mode 100644 index 00000000000..0a18c20af4e --- /dev/null +++ b/packages/cactus-verifier-client/src/main/typescript/public-api.ts @@ -0,0 +1,7 @@ +export { Verifier, IVerifierEventListener, LedgerEvent } from "./verifier"; +export { getValidatorApiClient } from "./get-validator-api-client"; +export { + VerifierFactory, + ValidatorConfigEntry, + VerifierFactoryConfig, +} from "./verifier-factory"; diff --git a/packages/cactus-verifier-client/src/main/typescript/verifier-factory.ts b/packages/cactus-verifier-client/src/main/typescript/verifier-factory.ts new file mode 100644 index 00000000000..91751f04825 --- /dev/null +++ b/packages/cactus-verifier-client/src/main/typescript/verifier-factory.ts @@ -0,0 +1,170 @@ +/* + * Copyright 2020-2022 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * verifier-factory.ts + */ + +import { Verifier } from "./verifier"; + +import { + Checks, + Logger, + LoggerProvider, + LogLevelDesc, +} from "@hyperledger/cactus-common"; + +import { ISocketApiClient } from "@hyperledger/cactus-core-api"; + +import { + ClientApiConfig, + getValidatorApiClient, +} from "./get-validator-api-client"; + +////////////////////////////////// +// Type Declarations +////////////////////////////////// + +type RequestedData = { + dataName: string; + dataType: string; +}; + +type ApiInfo = { + apiType: string; + requestedData: Array; +}; + +/** + * Validator config entry fields that are mandatory for all ledgers. + */ +type BaseValidatorFields = { + validatorID: string; + validatorType: keyof ClientApiConfig; + ledgerInfo?: { + ledgerAbstract: string; + }; + apiInfo?: Array; +}; + +/** + * Validator config entry fields specific to any supported ledger. + */ +type LedgerSpecificValidatorFields = Record; + +/** + * Validator config entry. + */ +export type ValidatorConfigEntry = BaseValidatorFields & + LedgerSpecificValidatorFields; + +/** + * VerifierFactory config is a list of validator configs that constitute a cactus network. + */ +export type VerifierFactoryConfig = ValidatorConfigEntry[]; + +////////////////////////////////// +// VerifierFactory class +////////////////////////////////// + +/** + * VerifierFactory creates Verifier instances based only on validator id and it's ledger type. + * Returned Verifiers are stored internally and reused for any future requests. + */ +export class VerifierFactory { + private verifierMap = new Map>>(); + private readonly log: Logger; + + readonly className: string; + + /** + * @param verifierConfig: Configuration of validators + * @param loglevel: Debug logging level + */ + constructor( + private readonly verifierConfig: VerifierFactoryConfig, + private readonly loglevel: LogLevelDesc = "info", + ) { + this.className = this.constructor.name; + + verifierConfig.forEach((v) => + Checks.nonBlankString(v.validatorID, `${v.validatorID} validator config`), + ); + verifierConfig.forEach((v) => + Checks.nonBlankString( + v.validatorType, + `${v.validatorType} validator config`, + ), + ); + + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level: this.loglevel, label }); + this.log.debug(`${this.className} created with config:`, verifierConfig); + } + + /** + * Get single validator from internal VerifierFactory config. + * + * @param validatorId: Id of verifier to create. + * @returns Single validator config entry. + */ + private getValidatorConfigEntryOrThrow( + validatorId: string, + ): ValidatorConfigEntry { + this.log.debug("Search for config of validator with id", validatorId); + + const validatorConfig = this.verifierConfig.find( + (v) => v.validatorID === validatorId, + ); + + if (!validatorConfig) { + throw new Error( + `VerifierFactory - Missing validator config with ID ${validatorId}`, + ); + } + + return validatorConfig; + } + + /** + * Get verifier from internal map, or create new one based on requested validator ID. + * + * @param validatorId: Id of verifier to create. + * @param type: optional parameter, will determine the return type. + * @returns Verifier or Verifier if type argument was not provided. + */ + getVerifier( + validatorId: string, + type?: K, + ): Verifier { + const validatorConfig = this.getValidatorConfigEntryOrThrow(validatorId); + + // Assert ClientApi types + if (type && type !== validatorConfig.validatorType) { + throw new Error( + `VerifierFactory - Validator ${validatorId} type mismatch; requested=${type}, config=${validatorConfig.validatorType}`, + ); + } + + if (this.verifierMap.has(validatorId)) { + this.log.info( + `Verifier for Validator ${validatorId} found in internal map - reuse.`, + ); + return this.verifierMap.get(validatorId) as Verifier< + ClientApiConfig[K]["out"] + >; + } else { + this.log.info(`No Verifier for Validator ${validatorId} - create new.`); + + const clientApi = getValidatorApiClient( + validatorConfig.validatorType, + (validatorConfig as unknown) as ClientApiConfig[K]["in"], + ); + + const verifier = new Verifier(validatorId, clientApi, this.loglevel); + this.verifierMap.set(validatorId, verifier); + + return verifier; + } + } +} diff --git a/packages/cactus-verifier-client/src/main/typescript/verifier.ts b/packages/cactus-verifier-client/src/main/typescript/verifier.ts new file mode 100644 index 00000000000..ed60323cbb4 --- /dev/null +++ b/packages/cactus-verifier-client/src/main/typescript/verifier.ts @@ -0,0 +1,195 @@ +/* + * Copyright 2020-2021 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Verifier.ts + */ + +import { Subscription } from "rxjs"; + +import { + Logger, + LogLevelDesc, + LoggerProvider, +} from "@hyperledger/cactus-common"; + +import { + ISocketApiClient, + IVerifier, + IVerifierEventListener, +} from "@hyperledger/cactus-core-api"; + +export { + IVerifierEventListener, + LedgerEvent, +} from "@hyperledger/cactus-core-api"; + +/** + * Utility type for retrieving monitoring event / new block type from generic ISocketApiClient interface. + */ +type BlockTypeFromSocketApi = T extends ISocketApiClient + ? U + : never; + +/** + * Extends ledger connector ApiClient with additional monitoring methods (using callbacks, instead of reactive). + * + * @remarks + * Migrated from cmd-socketio-server for merging the codebases. + * + * @todo Don't throw exception for not supported operations, don't include these methods at all (if possible) + */ +export class Verifier> + implements IVerifier { + private readonly log: Logger; + readonly className: string; + readonly runningMonitors = new Map(); + + /** + * @param ledgerApi - ApiClient for communicating with ledger connector plugin (must implement `ISocketApiClient`) + * @param logLevel - Log level used by the Verifier. + */ + constructor( + public readonly verifierID: string, + public readonly ledgerApi: LedgerApiType, + logLevel: LogLevelDesc = "INFO", + ) { + this.className = this.constructor.name; + this.log = LoggerProvider.getOrCreate({ + level: logLevel, + label: this.className, + }); + this.log.debug("Created Verifier for ledger API"); + } + + /** + * Start monitoring for new events / blocks from underlying ledger. + * + * @param appId - Used to track different apps that use the monitoring. + * Each app has one subscription to common monitoring subject returned by the ApiClient watch method. + * @param options - Options passed to the validator. + * @param eventListener - Type that supplies callbacks called when new event / error was encountered. + * + * @todo Change return type from Promise to void, this method is already async by design. + */ + startMonitor( + appId: string, + options: Record, + eventListener: IVerifierEventListener< + BlockTypeFromSocketApi + >, + ): Promise { + return new Promise((resolve) => { + if (!this.ledgerApi.watchBlocksV1) { + throw new Error("startMonitor not supported on this ledger"); + } + + if (this.runningMonitors.has(appId)) { + throw new Error(`Monitor with appId '${appId}' is already running!`); + } + + this.log.debug("call : startMonitor appId =", appId); + + try { + const blocksObservable = this.ledgerApi.watchBlocksV1(options); + + const watchBlocksSub = blocksObservable.subscribe({ + next: (blockData: unknown) => { + const event = { + id: "", + verifierId: this.verifierID, + data: blockData as BlockTypeFromSocketApi, + }; + eventListener.onEvent(event); + }, + error: (err) => { + this.log.error("Error when watching for new blocks, err:", err); + if (eventListener.onError) { + eventListener.onError(err); + } + }, + complete: () => { + this.log.info("Watch completed"); + }, + }); + + this.runningMonitors.set(appId, watchBlocksSub); + this.log.debug( + "New monitor added, runningMonitors.size ==", + this.runningMonitors.size, + ); + } catch (err) { + this.log.error(`##Error: startMonitor, ${err}`); + this.runningMonitors.delete(appId); + } + + resolve(); + }); + } + + /** + * Stops the monitor for specified app, removes it's subscription from internal storage. + * + * @param appId - ID of application that requested the monitoring. + */ + stopMonitor(appId: string): void { + if (!this.ledgerApi.watchBlocksV1) { + throw new Error("stopMonitor not supported on this ledger"); + } + + const watchBlocksSub = this.runningMonitors.get(appId); + if (!watchBlocksSub) { + throw new Error("No monitor running with appId: " + appId); + } + watchBlocksSub.unsubscribe(); + this.runningMonitors.delete(appId); + + this.log.debug( + "Monitor removed, runningMonitors.size ==", + this.runningMonitors.size, + ); + } + + /** + * Immediately sends request to the validator, doesn't report any error or responses. + * @param contract - contract to execute on the ledger. + * @param method - function / method to be executed by validator. + * @param args - arguments. + * + * @todo Change return type from Promise to void, this method is already async by design. + */ + sendAsyncRequest( + contract: Record, + method: Record, + args: any, + ): Promise { + return new Promise((resolve) => { + if (!this.ledgerApi.sendAsyncRequest) { + throw new Error("sendAsyncRequest not supported on this ledger"); + } + + resolve(this.ledgerApi.sendAsyncRequest(contract, method, args)); + }); + } + + /** + * Sends request to be executed on the ledger, watches and reports any error and the response from a ledger. + * @param contract - contract to execute on the ledger. + * @param method - function / method to be executed by validator. + * @param args - arguments. + * @returns Promise that will resolve with response from the ledger, or reject when error occurred. + */ + sendSyncRequest( + contract: Record, + method: Record, + args: any, + ): Promise { + if (!this.ledgerApi.sendSyncRequest) { + return new Promise(() => { + throw new Error("sendSyncRequest not supported on this ledger"); + }); + } + + return this.ledgerApi.sendSyncRequest(contract, method, args); + } +} diff --git a/packages/cactus-verifier-client/src/test/typescript/unit/get-validator-api-client.test.ts b/packages/cactus-verifier-client/src/test/typescript/unit/get-validator-api-client.test.ts new file mode 100644 index 00000000000..3830d3220f4 --- /dev/null +++ b/packages/cactus-verifier-client/src/test/typescript/unit/get-validator-api-client.test.ts @@ -0,0 +1,56 @@ +// Base Class: packages/cactus-verifier-client/src/main/typescript/get-validator-api-client.ts + +import "jest-extended"; + +import { getValidatorApiClient } from "../../../main/typescript/get-validator-api-client"; + +import { + SocketIOApiClient, + SocketIOApiClientOptions, +} from "@hyperledger/cactus-api-client"; + +import { + BesuApiClient, + BesuApiClientOptions, +} from "@hyperledger/cactus-plugin-ledger-connector-besu"; + +test("Create legacy socketio client", () => { + const clientOptions: SocketIOApiClientOptions = { + validatorID: "someValId", + validatorURL: "invalid-url123asd", + validatorKeyPath: "key.pem", + }; + + const clientApi: SocketIOApiClient = getValidatorApiClient( + "legacy-socketio", + clientOptions, + ); + + expect(clientApi.className).toEqual("SocketIOApiClient"); + expect(clientApi.options).toEqual(clientOptions); + + // Close socket manually to prevent a warning + clientApi.close(); +}); + +test("Create besu client", () => { + const clientOptions = new BesuApiClientOptions({ basePath: "foo" }); + + // BESU 1 + const clientApi1: BesuApiClient = getValidatorApiClient( + "BESU_1X", + clientOptions, + ); + + expect(clientApi1.className).toEqual("BesuApiClient"); + expect(clientApi1.options).toEqual(clientOptions); + + // BESU 2 + const clientApi2: BesuApiClient = getValidatorApiClient( + "BESU_2X", + clientOptions, + ); + + expect(clientApi2.className).toEqual("BesuApiClient"); + expect(clientApi2.options).toEqual(clientOptions); +}); diff --git a/packages/cactus-verifier-client/src/test/typescript/unit/verifier-factory.test.ts b/packages/cactus-verifier-client/src/test/typescript/unit/verifier-factory.test.ts new file mode 100644 index 00000000000..5ec04879c5b --- /dev/null +++ b/packages/cactus-verifier-client/src/test/typescript/unit/verifier-factory.test.ts @@ -0,0 +1,180 @@ +// Base Class: packages/cactus-verifier-client/src/main/typescript/verifier-factory.ts + +import "jest-extended"; + +import { Verifier } from "../../../main/typescript/verifier"; +import { SocketIOApiClient } from "@hyperledger/cactus-api-client"; +import { + VerifierFactory, + VerifierFactoryConfig, +} from "../../../main/typescript/verifier-factory"; +import { BesuApiClient } from "@hyperledger/cactus-plugin-ledger-connector-besu"; + +describe("Constructor Tests", () => { + test("Basic construction", () => { + const ledgerPluginInfo: VerifierFactoryConfig = [ + { + validatorID: "sUr7d10R", + validatorType: "legacy-socketio", + validatorURL: "https://sawtooth-validator:5140", + validatorKeyPath: "./validatorKey/sUr7d10R/keysUr7d10R.crt", + ledgerInfo: { + ledgerAbstract: "Sawtooth Ledger", + }, + apiInfo: [], + }, + { + validatorID: "besu_openapi_connector", + validatorType: "BESU_2X", + basePath: "localhost", + ledgerInfo: { + ledgerAbstract: "Besu-OpenAPI Ledger", + }, + apiInfo: [], + }, + ]; + + const verifierFactory = new VerifierFactory(ledgerPluginInfo); + expect(verifierFactory.className).toEqual("VerifierFactory"); + expect(verifierFactory["verifierMap"].size).toBe(0); + expect(verifierFactory["verifierConfig"]).toEqual(ledgerPluginInfo); + }); + + test("Empty validatorID throws exception", () => { + const ledgerPluginInfo: VerifierFactoryConfig = [ + { + validatorID: "sUr7d10R", + validatorType: "legacy-socketio", + validatorURL: "https://sawtooth-validator:5140", + validatorKeyPath: "./validatorKey/sUr7d10R/keysUr7d10R.crt", + ledgerInfo: { + ledgerAbstract: "Sawtooth Ledger", + }, + apiInfo: [], + }, + { + validatorID: "", + validatorType: "BESU_2X", + basePath: "localhost", + username: "admin", + password: "password", + ledgerInfo: { + ledgerAbstract: "Besu-OpenAPI Ledger", + }, + apiInfo: [], + }, + ]; + + expect(() => new VerifierFactory(ledgerPluginInfo)).toThrow(); + }); +}); + +describe("getVerifier Tests", () => { + const ledgerPluginInfo: VerifierFactoryConfig = [ + { + validatorID: "mySocketSawtoothValidatorId", + validatorType: "legacy-socketio", + validatorURL: "https://sawtooth-validator:5140", + validatorKeyPath: "./validatorKey/sUr7d10R/keysUr7d10R.crt", + ledgerInfo: { + ledgerAbstract: "Sawtooth Ledger", + }, + apiInfo: [], + }, + { + validatorID: "myBesuValidatorId", + validatorType: "BESU_2X", + basePath: "myBesuPath", + username: "admin", + password: "password", + ledgerInfo: { + ledgerAbstract: "Besu-OpenAPI Ledger", + }, + apiInfo: [], + }, + ]; + + let sut: VerifierFactory; + + beforeEach(() => { + sut = new VerifierFactory(ledgerPluginInfo); + }); + + test("Throws when requesting validator not defined in config", () => { + expect(() => sut.getVerifier("missingValidatorId")).toThrow(); + }); + + test("Throws when requested client type differs from configured type", () => { + expect(() => + sut.getVerifier("myBesuValidatorId", "legacy-socketio"), + ).toThrow(); + + // even though the same clientApi is used for both BESU_1X and BESU_2X this should fail + // client code should not depend on internal implementation detail. + expect(() => sut.getVerifier("myBesuValidatorId", "BESU_1X")).toThrow(); + }); + + test("Creates a legacy socketio client", () => { + const validatorId = "mySocketSawtoothValidatorId"; + + const client: Verifier = sut.getVerifier( + validatorId, + "legacy-socketio", + ); + + expect(client.verifierID).toEqual(validatorId); + expect(client.ledgerApi.className).toEqual("SocketIOApiClient"); + expect(client.ledgerApi.options.validatorID).toEqual(validatorId); + + client.ledgerApi.close(); + }); + + test("Creates a open-api based client", () => { + const validatorId = "myBesuValidatorId"; + + const client: Verifier = sut.getVerifier( + validatorId, + "BESU_2X", + ); + + expect(client.verifierID).toEqual(validatorId); + expect(client.ledgerApi.className).toEqual("BesuApiClient"); + expect(client.ledgerApi.options.basePath).toEqual("myBesuPath"); + }); + + test("Creates correct api client without explicit type specification", () => { + const validatorId = "mySocketSawtoothValidatorId"; + + const client: Verifier = sut.getVerifier( + validatorId, + ) as Verifier; + + expect(client.verifierID).toEqual(validatorId); + expect(client.ledgerApi.className).toEqual("SocketIOApiClient"); + expect(client.ledgerApi.options.validatorID).toEqual(validatorId); + + client.ledgerApi.close(); + }); + + test("Factory reuses already created verifiers", () => { + const validatorId = "mySocketSawtoothValidatorId"; + + const client: Verifier = sut.getVerifier( + validatorId, + "legacy-socketio", + ); + expect(client.verifierID).toEqual(validatorId); + expect(sut["verifierMap"].size).toBe(1); + + const anotherClient: Verifier = sut.getVerifier( + validatorId, + "legacy-socketio", + ); + expect(anotherClient.verifierID).toEqual(validatorId); + expect(sut["verifierMap"].size).toBe(1); // No new verifier added + + expect(client).toBe(anotherClient); + + client.ledgerApi.close(); + }); +}); diff --git a/packages/cactus-api-client/src/test/typescript/unit/verifier.test.ts b/packages/cactus-verifier-client/src/test/typescript/unit/verifier.test.ts similarity index 58% rename from packages/cactus-api-client/src/test/typescript/unit/verifier.test.ts rename to packages/cactus-verifier-client/src/test/typescript/unit/verifier.test.ts index 448475d8352..f4864304603 100644 --- a/packages/cactus-api-client/src/test/typescript/unit/verifier.test.ts +++ b/packages/cactus-verifier-client/src/test/typescript/unit/verifier.test.ts @@ -1,8 +1,7 @@ -/* Base Class: packages/cactus-api-client/src/main/typescript/verifier.ts - */ +// Base Class: packages/cactus-api-client/src/main/typescript/verifier.ts -const testLogLevel: LogLevelDesc = "info"; -const sutLogLevel: LogLevelDesc = "info"; +const testLogLevel: LogLevelDesc = "debug"; +const sutLogLevel: LogLevelDesc = "debug"; const testTimeout = 1000 * 5; // 5 second timeout per test const setupTimeout = 1000 * 60; // 1 minute timeout for setup @@ -22,7 +21,8 @@ const log: Logger = LoggerProvider.getOrCreate({ import { ISocketApiClient } from "@hyperledger/cactus-core-api"; import { Verifier, - VerifierEventListener, + IVerifierEventListener, + LedgerEvent, } from "../../../main/typescript/verifier"; ////////////////////////////////// @@ -46,7 +46,7 @@ class MockApiClient implements ISocketApiClient { watchBlocksV1 = jest.fn().mockName("watchBlocksV1"); } -class MockEventListener implements VerifierEventListener { +class MockEventListener implements IVerifierEventListener { onEvent = jest.fn().mockName("onEvent"); onError = jest.fn().mockName("onError"); } @@ -55,6 +55,21 @@ class MockEventListener implements VerifierEventListener { // Monitoring Tests ////////////////////////////// +test("Using operation not implemented on the ledger throws error", () => { + class EmptyImpl implements ISocketApiClient {} + const apiClient = new EmptyImpl(); + const sut = new Verifier("test-id", apiClient, sutLogLevel); + + // Monitoring + const eventListenerMock = new MockEventListener(); + expect(sut.startMonitor("someId", {}, eventListenerMock)).toReject(); + expect(() => sut.stopMonitor("someId")).toThrowError(); + + // Sending Requests + expect(sut.sendSyncRequest({}, {}, "")).toReject(); + expect(sut.sendAsyncRequest({}, {}, "")).toReject(); +}); + describe("Monitoring Tests", () => { // Assume block data format is string let apiClientMock: MockApiClient; @@ -66,28 +81,22 @@ describe("Monitoring Tests", () => { apiClientMock.watchBlocksV1.mockReturnValue( new Observable(() => log.debug("Mock subscribe called")), ); - sut = new Verifier(apiClientMock, sutLogLevel); + sut = new Verifier("test-id", apiClientMock, sutLogLevel); eventListenerMock = new MockEventListener(); }, setupTimeout); - test("Entry is added to runningMonitors for new monitoring requests", () => { + test("Entry is added to runningMonitors for new monitoring requests", async () => { const monitorOptions = { test: true }; + expect(sut.runningMonitors.size).toEqual(0); - sut.startMonitor("someId", eventListenerMock, monitorOptions); + await sut.startMonitor("someId", monitorOptions, eventListenerMock); expect(sut.runningMonitors.size).toEqual(1); expect(apiClientMock.watchBlocksV1).toBeCalledWith(monitorOptions); }); - // test("ApiClient is called without monitorOptions if monitor was already started", () => { - // const monitorOptions = { test: true }; - // sut.startMonitor("someId", eventListenerMock, monitorOptions); - // sut.startMonitor("anotherId", eventListenerMock, monitorOptions); - // expect(apiClientMock.watchBlocksV1).lastCalledWith(undefined); - // }); - test("Running multiple monitors with same ID throws an Error", () => { - sut.startMonitor("someId", eventListenerMock); - expect(() => sut.startMonitor("someId", eventListenerMock)).toThrow(); + expect(sut.startMonitor("someId", {}, eventListenerMock)).toResolve(); + expect(sut.startMonitor("someId", {}, eventListenerMock)).toReject(); }); test("In case of ApiClient exception runningMonitors is not updated", () => { @@ -95,10 +104,10 @@ describe("Monitoring Tests", () => { apiClientMock.watchBlocksV1.mockImplementation(() => { throw Error("Some mock error in watchBlocks"); }); - sut = new Verifier(apiClientMock, sutLogLevel); + sut = new Verifier("test-id", apiClientMock, sutLogLevel); expect(sut.runningMonitors.size).toEqual(0); - sut.startMonitor("someId", eventListenerMock); + expect(sut.startMonitor("someId", {}, eventListenerMock)).toResolve(); expect(sut.runningMonitors.size).toEqual(0); }); @@ -116,16 +125,18 @@ describe("Monitoring Tests", () => { subscriber.next(mockBlockData); }), ); - sut = new Verifier(apiClientMock, sutLogLevel); + sut = new Verifier("test-id", apiClientMock, sutLogLevel); eventListenerMock = new MockEventListener(); - eventListenerMock.onEvent.mockImplementation((blockData: string) => { - log.debug("onEvent() called with blockData:", blockData); - expect(blockData).toEqual(mockBlockData); - done(); - }); + eventListenerMock.onEvent.mockImplementation( + (ledgerEvent: LedgerEvent) => { + log.debug("onEvent() called with ledger event:", ledgerEvent); + expect(ledgerEvent.data).toEqual(mockBlockData); + done(); + }, + ); - sut.startMonitor("someId", eventListenerMock); + sut.startMonitor("someId", {}, eventListenerMock); }); test("onError callback is called in when monitoring failed", (done) => { @@ -137,7 +148,7 @@ describe("Monitoring Tests", () => { subscriber.error(mockError); }), ); - sut = new Verifier(apiClientMock, sutLogLevel); + sut = new Verifier("test-id", apiClientMock, sutLogLevel); eventListenerMock = new MockEventListener(); eventListenerMock.onError.mockImplementation((err: any) => { @@ -146,15 +157,15 @@ describe("Monitoring Tests", () => { done(); }); - sut.startMonitor("someId", eventListenerMock); + sut.startMonitor("someId", {}, eventListenerMock); }); - test("stopMonitor unsubscribes and deletes entry from runningMonitors", () => { + test("stopMonitor unsubscribes and deletes entry from runningMonitors", async () => { const thisAppId = "someId"; // Start monitor expect(sut.runningMonitors.size).toEqual(0); - sut.startMonitor(thisAppId, eventListenerMock); + await sut.startMonitor(thisAppId, {}, eventListenerMock); expect(sut.runningMonitors.size).toEqual(1); // Assert monitor is running @@ -169,3 +180,43 @@ describe("Monitoring Tests", () => { expect(mon?.closed).toBeTrue(); }); }); + +describe("Sending Requests Tests", () => { + let apiClientMock: MockApiClient; + let sut: Verifier>; + + beforeEach(() => { + apiClientMock = new MockApiClient(); + apiClientMock.watchBlocksV1.mockReturnValue( + new Observable(() => log.debug("Mock subscribe called")), + ); + sut = new Verifier("test-id", apiClientMock, sutLogLevel); + }, setupTimeout); + + test("Send async request call proxied to the apiClient", async () => { + const inContract = { foo: "bar" }; + const inMethod = { func: "a" }; + const inArgs = 5; + + await sut.sendAsyncRequest(inContract, inMethod, inArgs); + + expect(apiClientMock.sendAsyncRequest).toBeCalledWith( + inContract, + inMethod, + inArgs, + ); + }); + + test("Send sync request call proxied to the apiClient", async () => { + const inContract = { foo: "bar" }; + const inMethod = { func: "a" }; + const inArgs = 5; + + await sut.sendSyncRequest(inContract, inMethod, inArgs); + expect(apiClientMock.sendSyncRequest).toBeCalledWith( + inContract, + inMethod, + inArgs, + ); + }); +}); diff --git a/packages/cactus-verifier-client/tsconfig.json b/packages/cactus-verifier-client/tsconfig.json new file mode 100644 index 00000000000..ded4a0553dc --- /dev/null +++ b/packages/cactus-verifier-client/tsconfig.json @@ -0,0 +1,27 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist", + "declarationDir": "./dist", + "rootDir": "./src", + "tsBuildInfoFile": "../../.build-cache/cactus-verifier-client.tsbuildinfo" + }, + "include": [ + "./src" + ], + "references": [ + { + "path": "../cactus-api-client/tsconfig.json" + }, + { + "path": "../cactus-common/tsconfig.json" + }, + { + "path": "../cactus-core-api/tsconfig.json" + }, + { + "path": "../cactus-plugin-ledger-connector-besu/tsconfig.json" + } + ] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index a66b267cd0b..6d67c83b2ab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -70,6 +70,9 @@ { "path": "./packages/cactus-plugin-ledger-connector-xdai/tsconfig.json" }, + { + "path": "./packages/cactus-verifier-client/tsconfig.json" + }, { "path": "./packages/cactus-test-api-client/tsconfig.json" },