diff --git a/packages/cactus-api-client/package.json b/packages/cactus-api-client/package.json index a6ab296e814..cf8c723632c 100644 --- a/packages/cactus-api-client/package.json +++ b/packages/cactus-api-client/package.json @@ -64,6 +64,8 @@ "@hyperledger/cactus-core": "1.0.0-rc.3", "@hyperledger/cactus-core-api": "1.0.0-rc.3", "@hyperledger/cactus-plugin-consortium-manual": "1.0.0-rc.3", + "@hyperledger/cactus-plugin-ledger-connector-besu": "1.0.0-rc.3", + "js-yaml": "3.14.1", "rxjs": "7.3.0" }, "devDependencies": { 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..7a0a4e983d9 100644 --- a/packages/cactus-api-client/src/main/typescript/public-api.ts +++ b/packages/cactus-api-client/src/main/typescript/public-api.ts @@ -6,3 +6,8 @@ export { SocketIOApiClientOptions, } from "./socketio-api-client"; export { Verifier, VerifierEventListener } from "./verifier"; +export { + getValidatorApiClient, + VerifierFactory, + VerifierFactoryConfig, +} from "./verifier-factory"; 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..73221ddee6a 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"; diff --git a/packages/cactus-api-client/src/main/typescript/verifier-factory.ts b/packages/cactus-api-client/src/main/typescript/verifier-factory.ts new file mode 100644 index 00000000000..a2e40c0dc06 --- /dev/null +++ b/packages/cactus-api-client/src/main/typescript/verifier-factory.ts @@ -0,0 +1,124 @@ +/* + * Copyright 2020-2022 Hyperledger Cactus Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * verifier-factory.ts + */ + +import { Verifier } from "./verifier"; + +import { + Logger, + LoggerProvider, + LogLevelDesc, +} from "@hyperledger/cactus-common"; + +import { + SocketIOApiClient, + SocketIOApiClientOptions, +} from "./socketio-api-client"; + +import { + BesuApiClient, + BesuApiClientOptions, +} from "@hyperledger/cactus-plugin-ledger-connector-besu"; +import { ISocketApiClient } from "@hyperledger/cactus-core-api"; + +// All ApiClients supported by the factory must be added here! +type ClientApiConfig = { + "legacy-socketio": { + in: SocketIOApiClientOptions; + out: SocketIOApiClient; + }; + besu: { + in: BesuApiClientOptions; + out: BesuApiClient; + }; +}; + +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": + return new BesuApiClient(options as BesuApiClientOptions); + default: + // Will not compile if any ClientApiConfig keys was not handled by this switch + const _exhaustiveCheck: never = validatorType; + return _exhaustiveCheck; + } +} + +// Verifier Factory class +export type VerifierFactoryConfig = { + logLevel: LogLevelDesc; + validators: (Record & { + validatorID: string; + clientType: keyof ClientApiConfig; + })[]; +}; + +export class VerifierFactory { + private verifierMap = new Map>>(); + private readonly log: Logger; + + readonly className: string; + + constructor(private readonly verifierConfig: VerifierFactoryConfig) { + this.className = this.constructor.name; + if ( + !verifierConfig.validators || + verifierConfig.validators.some((v) => !v.validatorID) || + verifierConfig.validators.some((v) => !v.clientType) + ) { + throw new Error( + "Invalid VerifierFactory configuration; all validators needs to define at least 'validatorID' and 'clientType'", + ); + } + + const level: LogLevelDesc = verifierConfig.logLevel || "info"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + getVerifier( + validatorId: string, + type?: K, + ): Verifier { + // Read validator config + const validatorConfig = this.verifierConfig.validators.find( + (v) => v.validatorID === validatorId, + ); + if (!validatorConfig) { + throw new Error( + `VerifierFactory - Missing validator (type = ${type}) with ID ${validatorId}`, + ); + } + + // Assert ClientApi types + if (type && type !== validatorConfig.clientType) { + throw new Error( + `VerifierFactory - Validator ${validatorId} type mismatch; requested=${type}, config=${validatorConfig.clientType}`, + ); + } + + // Return / create verifier + if (this.verifierMap.has(validatorId)) { + return this.verifierMap.get(validatorId) as Verifier< + ClientApiConfig[K]["out"] + >; + } else { + const clientApi = getValidatorApiClient( + validatorConfig.clientType, + (validatorConfig as unknown) as ClientApiConfig[K]["in"], + ); + + const verifier = new Verifier(clientApi); + this.verifierMap.set(validatorId, verifier); + return verifier; + } + } +} diff --git a/packages/cactus-api-client/src/main/typescript/verifier.ts b/packages/cactus-api-client/src/main/typescript/verifier.ts index 1acfddd5793..d73a9c0e735 100644 --- a/packages/cactus-api-client/src/main/typescript/verifier.ts +++ b/packages/cactus-api-client/src/main/typescript/verifier.ts @@ -31,6 +31,8 @@ type BlockTypeFromSocketApi = T extends ISocketApiClient * * @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> { private readonly log: Logger; @@ -66,6 +68,10 @@ export class Verifier> { eventListener: VerifierEventListener>, monitorOptions?: Record, ): void { + 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!`); } @@ -109,15 +115,57 @@ export class Verifier> { * @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. + */ + sendAsyncRequest( + contract: Record, + method: Record, + args: any, + ): void { + if (!this.ledgerApi.sendAsyncRequest) { + throw new Error("stopMonitor not supported on this ledger"); + } + + return 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) { + throw new Error("stopMonitor not supported on this ledger"); + } + + return this.ledgerApi.sendSyncRequest(contract, method, args); + } } diff --git a/packages/cactus-api-client/src/test/typescript/unit/verifier.test.ts b/packages/cactus-api-client/src/test/typescript/unit/verifier.test.ts index 448475d8352..11ebfa1d8c8 100644 --- a/packages/cactus-api-client/src/test/typescript/unit/verifier.test.ts +++ b/packages/cactus-api-client/src/test/typescript/unit/verifier.test.ts @@ -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(apiClient, sutLogLevel); + + // Monitoring + const eventListenerMock = new MockEventListener(); + expect(() => sut.startMonitor("someId", eventListenerMock)).toThrowError(); + expect(() => sut.stopMonitor("someId")).toThrowError(); + + // Sending Requests + expect(() => sut.sendSyncRequest({}, {}, "")).toThrowError(); + expect(() => sut.sendAsyncRequest({}, {}, "")).toThrowError(); +}); + describe("Monitoring Tests", () => { // Assume block data format is string let apiClientMock: MockApiClient; @@ -169,3 +184,44 @@ 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(apiClientMock, sutLogLevel); + }, setupTimeout); + + test("Send async request call proxied to the apiClient", () => { + const inContract = { foo: "bar" }; + const inMethod = { func: "a" }; + const inArgs = 5; + + sut.sendAsyncRequest(inContract, inMethod, inArgs); + + expect(apiClientMock.sendAsyncRequest).toBeCalledWith( + inContract, + inMethod, + inArgs, + ); + }); + + test("Send sync request call proxied to the apiClient", () => { + const inContract = { foo: "bar" }; + const inMethod = { func: "a" }; + const inArgs = 5; + + sut.sendSyncRequest(inContract, inMethod, inArgs); + + expect(apiClientMock.sendSyncRequest).toBeCalledWith( + inContract, + inMethod, + inArgs, + ); + }); +}); diff --git a/packages/cactus-api-client/tsconfig.json b/packages/cactus-api-client/tsconfig.json index 24b536ef9d2..f2aa174248f 100644 --- a/packages/cactus-api-client/tsconfig.json +++ b/packages/cactus-api-client/tsconfig.json @@ -25,6 +25,9 @@ }, { "path": "../cactus-test-tooling/tsconfig.json" + }, + { + "path": "../cactus-plugin-ledger-connector-besu/tsconfig.json" } ] } \ No newline at end of file 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-test-api-client/src/test/typescript/integration/verifier-factory.test.ts b/packages/cactus-test-api-client/src/test/typescript/integration/verifier-factory.test.ts new file mode 100644 index 00000000000..068e2ce72b2 --- /dev/null +++ b/packages/cactus-test-api-client/src/test/typescript/integration/verifier-factory.test.ts @@ -0,0 +1,87 @@ +import { + VerifierFactory, + VerifierFactoryConfig, +} from "@hyperledger/cactus-api-client"; + +// Can be read from local filesystem +const verifierConfig: VerifierFactoryConfig = { + logLevel: "debug", + validators: [ + { + validatorID: "some_socketio_validator", + clientType: "legacy-socketio", + validatorURL: "localhost:5050", + validatorKeyPath: "./validatorKey/84jUisrs/key84jUisrs.crt", + maxCounterRequestID: 100, + syncFunctionTimeoutMillisecond: 5000, + socketOptions: { + rejectUnauthorized: false, + reconnection: false, + timeout: 20000, + }, + }, + { + validatorID: "besu_openapi_connector", + clientType: "besu", + username: "admin", + password: "password", + basePath: "localhost", + }, + ], +}; + +// 1 factory / BLP +const verifierFactory = new VerifierFactory(verifierConfig); + +// legacy socketio verifier +const socketioVerifier = verifierFactory.getVerifier( + "some_socketio_validator", + "legacy-socketio", +); // type: Verifier +console.log("socketioVerifier ctor:", socketioVerifier.constructor); // [class Verifier] +console.log( + "socketioVerifier api options:", + socketioVerifier.ledgerApi.options, +); +/* +socketioVerifier api options: { + validatorID: 'some_socketio_validator', + clientType: 'legacy-socketio', + validatorURL: 'localhost:5050', + validatorKeyPath: './validatorKey/84jUisrs/key84jUisrs.crt', + maxCounterRequestID: 100, + syncFunctionTimeoutMillisecond: 5000, + socketOptions: { + rejectUnauthorized: false, + reconnection: false, + timeout: 20000, + path: '/socket.io', + hostname: 'undefined', + secure: false, + port: '80' + } +} +*/ + +// openapi besu verifier +const besuVerifier = verifierFactory.getVerifier( + "besu_openapi_connector", + "besu", +); // type: Verifier +console.log("besuVerifier ctor:", besuVerifier.constructor); // [class Verifier] +console.log("besuVerifier api options:", besuVerifier.ledgerApi.options); +/* +besuVerifier api options: { + validatorID: 'besu_openapi_connector', + clientType: 'besu', + username: 'admin', + password: 'password', + basePath: 'localhost' +} +*/ + +// besu without type declaration +const untypedBesuVerifier = verifierFactory.getVerifier( + "besu_openapi_connector", +); // type: Verifier +console.log("untypedBesuVerifier ctor:", untypedBesuVerifier.constructor); // [class Verifier]