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 18554855ba9..bdb260acb2c 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,11 +19,7 @@ export interface ISocketApiClient { args: any, ): Promise; - watchBlocksV1?( - monitorOptions?: Record, - ): Observable; + watchBlocksV1?(monitorOptions?: any): Observable; - watchBlocksAsyncV1?( - monitorOptions?: Record, - ): Promise>; + watchBlocksAsyncV1?(monitorOptions?: any): Promise>; } diff --git a/packages/cactus-plugin-ledger-connector-fabric/README.md b/packages/cactus-plugin-ledger-connector-fabric/README.md index 784a74741c5..dbb0c6385da 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/README.md +++ b/packages/cactus-plugin-ledger-connector-fabric/README.md @@ -10,6 +10,9 @@ - [1.4.1. Identity Providers](#141-identity-providers) - [1.4.2. Setting up a WS-X.509 provider](#142-setting-up-a-ws-x509-provider) - [1.4.3. Building the ws-identity docker image](#143-building-the-ws-identity-docker-image) + - [1.5 Monitoring new blocks (WatchBlocks)](#15-monitoring-new-blocks-watchblocks) + - [1.5.1 Example](#151-example) + - [1.5.2 Listener Type](#152-listener-type) - [2. Architecture](#2-architecture) - [2.1. run-transaction-endpoint](#21-run-transaction-endpoint) - [3. Containerization](#3-containerization) @@ -27,9 +30,10 @@ - [6. License](#6-license) - [7. Acknowledgments](#7-acknowledgments) + ## 1. Usage -This plugin provides a way to interact with Fabric networks. +This plugin provides a way to interact with Fabric networks. Using this one can perform: * Deploy smart contracts (chaincode). * Execute transactions on the ledger. @@ -81,7 +85,7 @@ try { ### 1.3. Using Via The API Client -**Prerequisites** +**Prerequisites** - A running Fabric ledger (network) - You have a running Cactus API server on `$HOST:$PORT` with the Fabric connector plugin installed on it (and the latter configured to have access to the Fabric ledger from point 1) @@ -250,7 +254,7 @@ await connector.rotateKey( Identity providers allows client to manage their private more effectively and securely. Cactus Fabric Connector support multiple type of providers. Each provider differ based upon where the private are stored. On High level certificate credential are stored as ```typescript -{ +{ type: FabricSigningCredentialType; credentials: { certificate: string; @@ -277,6 +281,58 @@ The following packages are used to access private keys (via web-socket) stored TBD +### 1.5 Monitoring new blocks (WatchBlocks) +- Use `ApiClient` to receive new blocks from a fabric ledger. +- Type of the response can be configured. +- Credentials must be configured using `gatewayOptions` argument (you can either send them directly in request or use wallet stored in keychain). + +#### 1.5.1 Example +For more detailed example check [fabric-watch-blocks-v1-endpoint.test.ts](./src/test/typescript/integration/fabric-v2-2-x/fabric-watch-blocks-v1-endpoint.test.ts) + +``` typescript +// Setup +const signingCredential = { + keychainId: uuidv4(), + keychainRef: "user2", +}; + +// Create RxJS Observable. +// This will connect to the fabric connector and start the monitoring operation. +const watchObservable = apiClient.watchBlocksV1({ + channelName: "mychannel", // fabric channel name + gatewayOptions: { // use signing credential from keychain + identity: signingCredential.keychainRef, + wallet: { + keychain: signingCredential, + }, + }, + WatchBlocksListenerTypeV1.Full, // return full block data +}); + +// Subscribe to the observable to receive new blocks +const subscription = watchObservable.subscribe({ + next(event) { + // Handle new event + }, + error(err) { + // Handle error from connector + }, +}); +``` + +#### 1.5.2 Listener Type +There are two types of listener type - original and cactus ones. + +##### Original +Corresponds directly to `BlockType` from `fabric-common`: + - `WatchBlocksListenerTypeV1.Filtered`, + - `WatchBlocksListenerTypeV1.Full`, + - `WatchBlocksListenerTypeV1.Private`, + +##### Cactus (custom) +Parses the data and returns custom formatted block. +- `WatchBlocksListenerTypeV1.CactusTransactions`: Returns transactions summary. Compatible with legacy `fabric-socketio` monitoring operation. + ## 2. Architecture The sequence diagrams for various endpoints are mentioned below @@ -292,7 +348,7 @@ The above diagram shows the sequence diagram of transact() method of the PluginL ![run-transaction-endpoint-enroll](docs/architecture/images/run-transaction-endpoint-enroll.png) -The above diagram shows the sequence diagram of enroll() method of the PluginLedgerConnectorFabric class. The caller to this function, which in reference to the above sequence diagram is API server, sends Signer object along with EnrollmentRequest as an argument to the enroll() method. Based on the singerType (FabricSigningCredentialType.X509, FabricSigningCredentialType.VaultX509, FabricSigningCredentialType.WsX509), corresponding identity is enrolled and stored inside keychain. +The above diagram shows the sequence diagram of enroll() method of the PluginLedgerConnectorFabric class. The caller to this function, which in reference to the above sequence diagram is API server, sends Signer object along with EnrollmentRequest as an argument to the enroll() method. Based on the singerType (FabricSigningCredentialType.X509, FabricSigningCredentialType.VaultX509, FabricSigningCredentialType.WsX509), corresponding identity is enrolled and stored inside keychain. diff --git a/packages/cactus-plugin-ledger-connector-fabric/package.json b/packages/cactus-plugin-ledger-connector-fabric/package.json index 3a9599c6e79..d9501f07873 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/package.json +++ b/packages/cactus-plugin-ledger-connector-fabric/package.json @@ -61,10 +61,11 @@ "bl": "5.0.0", "bn.js": "4.12.0", "express": "4.17.1", - "fabric-ca-client": "2.3.0-snapshot.62", - "fabric-common": "2.3.0-snapshot.63", - "fabric-network": "2.3.0-snapshot.62", - "fabric-protos": "2.3.0-snapshot.63", + "fabric-ca-client": "2.5.0-snapshot.8", + "fabric-common": "2.5.0-snapshot.8", + "fabric-network": "2.5.0-snapshot.8", + "fabric-protos": "2.5.0-snapshot.8", + "fast-safe-stringify": "2.1.1", "form-data": "4.0.0", "http-status-codes": "2.1.4", "jsrsasign": "10.4.0", @@ -74,7 +75,9 @@ "node-vault": "0.9.22", "openapi-types": "9.1.0", "prom-client": "13.2.0", + "rxjs": "7.3.0", "sanitize-filename": "1.6.3", + "sanitize-html": "2.7.0", "secp256k1": "4.0.3", "temp": "0.9.4", "typescript-optional": "2.0.1", @@ -91,6 +94,7 @@ "@types/node-vault": "0.9.13", "@types/temp": "0.9.1", "@types/uuid": "8.3.1", + "@types/sanitize-html": "2.6.2", "fs-extra": "10.0.0", "ws-wallet": "1.1.5" }, diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/json/openapi.json b/packages/cactus-plugin-ledger-connector-fabric/src/main/json/openapi.json index 9b74b37b099..c2b3a8f7060 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/json/openapi.json +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/json/openapi.json @@ -952,6 +952,210 @@ "PrometheusExporterMetricsResponse": { "type": "string", "nullable": false + }, + "WatchBlocksV1": { + "type": "string", + "description": "Websocket requests for monitoring new blocks.", + "enum": [ + "org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Subscribe", + "org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Next", + "org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Unsubscribe", + "org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Error", + "org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Complete" + ], + "x-enum-varnames": [ + "Subscribe", + "Next", + "Unsubscribe", + "Error", + "Complete" + ] + }, + "WatchBlocksListenerTypeV1": { + "type": "string", + "description": "Response type from WatchBlocks. 'Cactus*' are custom views, others correspond to fabric SDK call.", + "enum": [ + "filtered", + "full", + "private", + "cactus:transactions" + ], + "x-enum-varnames": [ + "Filtered", + "Full", + "Private", + "CactusTransactions" + ] + }, + "WatchBlocksOptionsV1": { + "type": "object", + "description": "Options passed when subscribing to block monitoring.", + "required": [ + "channelName", + "gatewayOptions", + "type" + ], + "properties": { + "channelName": { + "type": "string", + "description": "Hyperledger Fabric channel to connect to.", + "minLength": 1, + "maxLength": 100, + "nullable": false + }, + "gatewayOptions": { + "$ref": "#/components/schemas/GatewayOptions", + "description": "Options to Hyperledger Fabric Node SDK Gateway", + "nullable": false + }, + "type": { + "$ref": "#/components/schemas/WatchBlocksListenerTypeV1", + "description": "Type of response block to return.", + "nullable": false + }, + "startBlock": { + "type": "string", + "description": "From which block start monitoring. Defaults to latest.", + "minLength": 1, + "maxLength": 100, + "nullable": false + } + } + }, + "WatchBlocksCactusTransactionsEventV1": { + "type": "object", + "description": "Transaction summary from commited block.", + "required": [ + "chaincodeId", + "transactionId", + "functionName", + "functionArgs" + ], + "properties": { + "chaincodeId": { + "description": "ChainCode containing function that was executed.", + "nullable": false, + "type": "string" + }, + "transactionId": { + "description": "Transaction identifier.", + "nullable": false, + "type": "string" + }, + "functionName": { + "description": "Function name that was executed.", + "nullable": false, + "type": "string" + }, + "functionArgs": { + "description": "List of function arguments.", + "type": "array", + "items": { + "type": "string", + "minLength": 0, + "nullable": false + } + } + } + }, + "WatchBlocksCactusTransactionsResponseV1": { + "type": "object", + "description": "Custom response containing block transactions summary. Compatible with legacy fabric-socketio connector monitoring.", + "required": [ + "cactusTransactionsEvents" + ], + "properties": { + "cactusTransactionsEvents": { + "description": "List of transactions summary", + "type": "array", + "items": { + "$ref": "#/components/schemas/WatchBlocksCactusTransactionsEventV1", + "nullable": false + } + } + } + }, + "WatchBlocksFullResponseV1": { + "type": "object", + "description": "Response that corresponds to Fabric SDK 'full' EventType.", + "required": [ + "fullBlock" + ], + "properties": { + "fullBlock": { + "description": "Full commited block.", + "nullable": false + } + } + }, + "WatchBlocksFilteredResponseV1": { + "type": "object", + "description": "Response that corresponds to Fabric SDK 'filtered' EventType.", + "required": [ + "filteredBlock" + ], + "properties": { + "filteredBlock": { + "description": "Filtered commited block.", + "nullable": false + } + } + }, + "WatchBlocksPrivateResponseV1": { + "type": "object", + "description": "Response that corresponds to Fabric SDK 'private' EventType.", + "required": [ + "privateBlock" + ], + "properties": { + "privateBlock": { + "description": "Private commited block.", + "nullable": false + } + } + }, + "WatchBlocksCactusErrorResponseV1": { + "type": "object", + "description": "Error response from WatchBlocks operation.", + "required": [ + "code", + "errorMessage" + ], + "properties": { + "code": { + "description": "Error code.", + "type": "number" + }, + "errorMessage": { + "description": "Description of the error.", + "type": "string" + } + } + }, + "WatchBlocksResponseV1": { + "description": "Response block from WatchBlocks endpoint. Depends on 'type' passed in subscription options.", + "oneOf": [ + { + "$ref": "#/components/schemas/WatchBlocksCactusTransactionsResponseV1", + "nullable": false + }, + { + "$ref": "#/components/schemas/WatchBlocksFullResponseV1", + "nullable": false + }, + { + "$ref": "#/components/schemas/WatchBlocksFilteredResponseV1", + "nullable": false + }, + { + "$ref": "#/components/schemas/WatchBlocksPrivateResponseV1", + "nullable": false + }, + { + "$ref": "#/components/schemas/WatchBlocksCactusErrorResponseV1", + "nullable": false + } + ] } } }, diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/api-client/fabric-api-client.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/api-client/fabric-api-client.ts new file mode 100644 index 00000000000..e0e01df36ca --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/api-client/fabric-api-client.ts @@ -0,0 +1,133 @@ +/** + * Extension of ApiClient genereted from OpenAPI. + * Allows operations not handled by OpenAPI (i.e. socketIO or grpc endpoints). + */ + +import { Observable, ReplaySubject } from "rxjs"; +import { finalize } from "rxjs/operators"; +import { io } from "socket.io-client"; +import { Logger, Checks } from "@hyperledger/cactus-common"; +import { LogLevelDesc, LoggerProvider } from "@hyperledger/cactus-common"; +import { Constants, ISocketApiClient } from "@hyperledger/cactus-core-api"; +import { + DefaultApi, + WatchBlocksV1, + WatchBlocksOptionsV1, + WatchBlocksResponseV1, +} from "../generated/openapi/typescript-axios"; +import { Configuration } from "../generated/openapi/typescript-axios/configuration"; + +/** + * Configuration for FabricApiClient + */ +export class FabricApiClientOptions extends Configuration { + readonly logLevel?: LogLevelDesc; + readonly wsApiHost?: string; + readonly wsApiPath?: string; +} + +/** + * Extended ApiClient that can be used to communicate with Fabric connector. + */ +export class FabricApiClient + extends DefaultApi + implements ISocketApiClient { + public static readonly CLASS_NAME = "FabricApiClient"; + + private readonly log: Logger; + private readonly wsApiHost: string; + private readonly wsApiPath: string; + + /** + * Registry of started monitoring sessions. + */ + private monitorSubjects = new Map< + string, + ReplaySubject + >(); + + /** + * Get this class name. + */ + public get className(): string { + return FabricApiClient.CLASS_NAME; + } + + constructor(public readonly options: FabricApiClientOptions) { + 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}`); + } + + /** + * Watch for new blocks on Fabric ledger. Type of response must be configured in monitorOptions. + * + * @param monitorOptions Monitoring configuration. + * + * @returns Observable that will receive new blocks once they appear. + */ + public watchBlocksV1( + monitorOptions: WatchBlocksOptionsV1, + ): Observable { + const socket = io(this.wsApiHost, { path: this.wsApiPath }); + const subject = new ReplaySubject(0); + + socket.on(WatchBlocksV1.Next, (data: WatchBlocksResponseV1) => { + this.log.debug("Received WatchBlocksV1.Next"); + subject.next(data); + }); + + socket.on(WatchBlocksV1.Error, (ex: string) => { + this.log.error("Received WatchBlocksV1.Error:", ex); + subject.error(ex); + }); + + socket.on(WatchBlocksV1.Complete, () => { + this.log.debug("Received WatchBlocksV1.Complete"); + subject.complete(); + }); + + socket.on("connect", () => { + this.log.info( + `Connected client '${socket.id}', sending WatchBlocksV1.Subscribe...`, + ); + this.monitorSubjects.set(socket.id, subject); + socket.emit(WatchBlocksV1.Subscribe, monitorOptions); + }); + + socket.connect(); + + return subject.pipe( + finalize(() => { + this.log.info( + `FINALIZE client ${socket.id} - unsubscribing from the stream...`, + ); + socket.emit(WatchBlocksV1.Unsubscribe); + socket.disconnect(); + this.monitorSubjects.delete(socket.id); + }), + ); + } + + /** + * Stop all ongoing monitors, terminate connections. + * + * @note Might take few seconds to clean up all the connections. + */ + public close() { + this.log.debug("Close all running monitors."); + this.monitorSubjects.forEach((subject) => subject.complete()); + this.monitorSubjects.clear(); + } +} diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/api.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/api.ts index f26a1541a91..2ff5620faf6 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/api.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/generated/openapi/typescript-axios/api.ts @@ -986,6 +986,173 @@ export interface VaultTransitKey { */ token: string; } +/** + * Error response from WatchBlocks operation. + * @export + * @interface WatchBlocksCactusErrorResponseV1 + */ +export interface WatchBlocksCactusErrorResponseV1 { + /** + * Error code. + * @type {number} + * @memberof WatchBlocksCactusErrorResponseV1 + */ + code: number; + /** + * Description of the error. + * @type {string} + * @memberof WatchBlocksCactusErrorResponseV1 + */ + errorMessage: string; +} +/** + * Transaction summary from commited block. + * @export + * @interface WatchBlocksCactusTransactionsEventV1 + */ +export interface WatchBlocksCactusTransactionsEventV1 { + /** + * ChainCode containing function that was executed. + * @type {string} + * @memberof WatchBlocksCactusTransactionsEventV1 + */ + chaincodeId: string; + /** + * Transaction identifier. + * @type {string} + * @memberof WatchBlocksCactusTransactionsEventV1 + */ + transactionId: string; + /** + * Function name that was executed. + * @type {string} + * @memberof WatchBlocksCactusTransactionsEventV1 + */ + functionName: string; + /** + * List of function arguments. + * @type {Array} + * @memberof WatchBlocksCactusTransactionsEventV1 + */ + functionArgs: Array; +} +/** + * Custom response containing block transactions summary. Compatible with legacy fabric-socketio connector monitoring. + * @export + * @interface WatchBlocksCactusTransactionsResponseV1 + */ +export interface WatchBlocksCactusTransactionsResponseV1 { + /** + * List of transactions summary + * @type {Array} + * @memberof WatchBlocksCactusTransactionsResponseV1 + */ + cactusTransactionsEvents: Array; +} +/** + * Response that corresponds to Fabric SDK \'filtered\' EventType. + * @export + * @interface WatchBlocksFilteredResponseV1 + */ +export interface WatchBlocksFilteredResponseV1 { + /** + * Filtered commited block. + * @type {any} + * @memberof WatchBlocksFilteredResponseV1 + */ + filteredBlock: any; +} +/** + * Response that corresponds to Fabric SDK \'full\' EventType. + * @export + * @interface WatchBlocksFullResponseV1 + */ +export interface WatchBlocksFullResponseV1 { + /** + * Full commited block. + * @type {any} + * @memberof WatchBlocksFullResponseV1 + */ + fullBlock: any; +} +/** + * Response type from WatchBlocks. \'Cactus*\' are custom views, others correspond to fabric SDK call. + * @export + * @enum {string} + */ + +export enum WatchBlocksListenerTypeV1 { + Filtered = 'filtered', + Full = 'full', + Private = 'private', + CactusTransactions = 'cactus:transactions' +} + +/** + * Options passed when subscribing to block monitoring. + * @export + * @interface WatchBlocksOptionsV1 + */ +export interface WatchBlocksOptionsV1 { + /** + * Hyperledger Fabric channel to connect to. + * @type {string} + * @memberof WatchBlocksOptionsV1 + */ + channelName: string; + /** + * + * @type {GatewayOptions} + * @memberof WatchBlocksOptionsV1 + */ + gatewayOptions: GatewayOptions; + /** + * + * @type {WatchBlocksListenerTypeV1} + * @memberof WatchBlocksOptionsV1 + */ + type: WatchBlocksListenerTypeV1; + /** + * From which block start monitoring. Defaults to latest. + * @type {string} + * @memberof WatchBlocksOptionsV1 + */ + startBlock?: string; +} +/** + * Response that corresponds to Fabric SDK \'private\' EventType. + * @export + * @interface WatchBlocksPrivateResponseV1 + */ +export interface WatchBlocksPrivateResponseV1 { + /** + * Private commited block. + * @type {any} + * @memberof WatchBlocksPrivateResponseV1 + */ + privateBlock: any; +} +/** + * @type WatchBlocksResponseV1 + * Response block from WatchBlocks endpoint. Depends on \'type\' passed in subscription options. + * @export + */ +export type WatchBlocksResponseV1 = WatchBlocksCactusErrorResponseV1 | WatchBlocksCactusTransactionsResponseV1 | WatchBlocksFilteredResponseV1 | WatchBlocksFullResponseV1 | WatchBlocksPrivateResponseV1; + +/** + * Websocket requests for monitoring new blocks. + * @export + * @enum {string} + */ + +export enum WatchBlocksV1 { + Subscribe = 'org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Subscribe', + Next = 'org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Next', + Unsubscribe = 'org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Unsubscribe', + Error = 'org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Error', + Complete = 'org.hyperledger.cactus.api.async.hlfabric.WatchBlocksV1.Complete' +} + /** * web-socket key details for signing fabric message with private key stored with external client * @export diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/plugin-ledger-connector-fabric.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/plugin-ledger-connector-fabric.ts index bfea6d566dc..a1e4b7dc23e 100644 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/plugin-ledger-connector-fabric.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/plugin-ledger-connector-fabric.ts @@ -15,13 +15,18 @@ import { DefaultEventHandlerOptions, DefaultEventHandlerStrategies, Gateway, - GatewayOptions, + GatewayOptions as FabricGatewayOptions, Wallets, X509Identity, TransientMap, Wallet, } from "fabric-network"; +import type { + Server as SocketIoServer, + Socket as SocketIoSocket, +} from "socket.io"; + import OAS from "../json/openapi.json"; import { @@ -55,6 +60,8 @@ import { GetPrometheusExporterMetricsEndpointV1, } from "./get-prometheus-exporter-metrics/get-prometheus-exporter-metrics-endpoint-v1"; +import { WatchBlocksV1Endpoint } from "./watch-blocks/watch-blocks-v1-endpoint"; + import { ConnectionProfile, GatewayDiscoveryOptions, @@ -72,6 +79,9 @@ import { DefaultEventHandlerStrategy, FabricSigningCredentialType, GetTransactionReceiptResponse, + GatewayOptions, + WatchBlocksV1, + WatchBlocksOptionsV1, } from "./generated/openapi/typescript-axios/index"; import { @@ -161,6 +171,7 @@ export class PluginLedgerConnectorFabric private endpoints: IWebServiceEndpoint[] | undefined; private readonly secureIdentity: SecureIdentityProviders; private readonly certStore: CertDatastore; + private runningWatchBlocksMonitors = new Set(); public get className(): string { return PluginLedgerConnectorFabric.CLASS_NAME; @@ -214,7 +225,8 @@ export class PluginLedgerConnectorFabric } public async shutdown(): Promise { - return; + this.runningWatchBlocksMonitors.forEach((m) => m.close()); + this.runningWatchBlocksMonitors.clear(); } public getPrometheusExporter(): PrometheusExporter { @@ -780,9 +792,70 @@ export class PluginLedgerConnectorFabric } } - async registerWebServices(app: Express): Promise { + /** + * Register WatchBlocksV1 endpoint, will be triggered in response to + * dedicated socketio request. + * + * Adds and removes monitors from `this.runningWatchBlocksMonitors`. + * + * @param socket connected client socket. + * @returns socket from argument. + */ + private registerWatchBlocksSocketIOEndpoint( + socket: SocketIoSocket, + ): SocketIoSocket { + this.log.debug("Register WatchBlocks.Subscribe handler."); + + socket.on( + WatchBlocksV1.Subscribe, + async (options: WatchBlocksOptionsV1) => { + // Start monitoring + const monitor = new WatchBlocksV1Endpoint({ + socket, + logLevel: this.opts.logLevel, + gateway: await this.createGatewayWithOptions(options.gatewayOptions), + }); + this.runningWatchBlocksMonitors.add(monitor); + await monitor.subscribe(options); + this.log.debug( + "Running monitors count:", + this.runningWatchBlocksMonitors.size, + ); + + socket.on("disconnect", () => { + this.runningWatchBlocksMonitors.delete(monitor); + this.log.debug( + "Running monitors count:", + this.runningWatchBlocksMonitors.size, + ); + }); + }, + ); + + return socket; + } + + /** + * Register HTTP and SocketIO service endpoints. + * + * @param app express server. + * @param wsApi socketio server. + * @returns list of http endpoints. + */ + async registerWebServices( + app: Express, + wsApi?: SocketIoServer, + ): Promise { const webServices = await this.getOrCreateWebServices(); await Promise.all(webServices.map((ws) => ws.registerExpress(app))); + + if (wsApi) { + wsApi.on("connection", (socket: SocketIoSocket) => { + this.log.debug(`New Socket connected. ID=${socket.id}`); + this.registerWatchBlocksSocketIOEndpoint(socket); + }); + } + return webServices; } @@ -823,6 +896,7 @@ export class PluginLedgerConnectorFabric const endpoint = new RunTransactionEndpointV1(opts); endpoints.push(endpoint); } + { const opts: IRunTransactionEndpointV1Options = { connector: this, @@ -847,30 +921,62 @@ export class PluginLedgerConnectorFabric return endpoints; } + /** + * Create gateway from request (will choose logic based on request) + * + * @node It seems that Gateway is not supposed to be created and destroyed rapidly, but + * rather kept around for longer. Possible issues: + * - Disconnect is async and takes a while until all internal services are closed. + * - Possible memory and connection pool leak (see https://github.com/hyperledger/fabric-sdk-node/issues/529). + * - Performance: there's a setup overhead that might be significant after scaling up. Hence... + * @todo Cache and reuse gateways (destroy only ones not used for a while). + * Or maybe add separate methods "start/stopSession" that would leave session management to the client? + * + * @param req must contain either gatewayOptions or signingCredential. + * @returns Fabric SDK Gateway + */ protected async createGateway(req: RunTransactionRequest): Promise { if (req.gatewayOptions) { - return createGateway({ - logLevel: this.opts.logLevel, - pluginRegistry: this.opts.pluginRegistry, - defaultConnectionProfile: this.opts.connectionProfile, - defaultDiscoveryOptions: this.opts.discoveryOptions || { - enabled: true, - asLocalhost: true, - }, - defaultEventHandlerOptions: this.opts.eventHandlerOptions || { - endorseTimeout: 300, - commitTimeout: 300, - strategy: DefaultEventHandlerStrategy.NetworkScopeAllfortx, - }, - gatewayOptions: req.gatewayOptions, - secureIdentity: this.secureIdentity, - certStore: this.certStore, - }); + return this.createGatewayWithOptions(req.gatewayOptions); } else { return this.createGatewayLegacy(req.signingCredential); } } + /** + * Create Gateway from dedicated gateway options. + * + * @param options gateway options + * @returns Fabric SDK Gateway + */ + protected async createGatewayWithOptions( + options: GatewayOptions, + ): Promise { + return createGateway({ + logLevel: this.opts.logLevel, + pluginRegistry: this.opts.pluginRegistry, + defaultConnectionProfile: this.opts.connectionProfile, + defaultDiscoveryOptions: this.opts.discoveryOptions || { + enabled: true, + asLocalhost: true, + }, + defaultEventHandlerOptions: this.opts.eventHandlerOptions || { + endorseTimeout: 300, + commitTimeout: 300, + strategy: DefaultEventHandlerStrategy.NetworkScopeAllfortx, + }, + gatewayOptions: options, + secureIdentity: this.secureIdentity, + certStore: this.certStore, + }); + } + + /** + * Create Gateway from signing credential (legacy, can be done with gateway options) + * + * @param signingCredential sign data. + * @returns Fabric SDK Gateway + */ protected async createGatewayLegacy( signingCredential: FabricSigningCredential, ): Promise { @@ -933,7 +1039,7 @@ export class PluginLedgerConnectorFabric DefaultEventHandlerStrategies[eho.strategy]; } - const gatewayOptions: GatewayOptions = { + const gatewayOptions: FabricGatewayOptions = { discovery: this.opts.discoveryOptions, eventHandlerOptions, identity: identity, diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/public-api.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/public-api.ts index b32cdf1a458..17565f96304 100755 --- a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/public-api.ts +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/public-api.ts @@ -1,5 +1,7 @@ export * from "./generated/openapi/typescript-axios/index"; +export { FabricApiClient } from "./api-client/fabric-api-client"; + export { PluginLedgerConnectorFabric, IPluginLedgerConnectorFabricOptions, diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/watch-blocks/watch-blocks-v1-endpoint.ts b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/watch-blocks/watch-blocks-v1-endpoint.ts new file mode 100644 index 00000000000..5befe3dbe06 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/main/typescript/watch-blocks/watch-blocks-v1-endpoint.ts @@ -0,0 +1,329 @@ +import { Socket as SocketIoSocket } from "socket.io"; + +import { BlockEvent, BlockListener, EventType, Gateway } from "fabric-network"; + +import { + Logger, + LogLevelDesc, + LoggerProvider, + Checks, +} from "@hyperledger/cactus-common"; + +import { + WatchBlocksV1, + WatchBlocksResponseV1, + WatchBlocksListenerTypeV1, + WatchBlocksOptionsV1, + WatchBlocksCactusTransactionsEventV1, +} from "../generated/openapi/typescript-axios"; + +import safeStringify from "fast-safe-stringify"; +import sanitizeHtml from "sanitize-html"; + +/** + * WatchBlocksV1Endpoint configuration. + */ +export interface IWatchBlocksV1EndpointConfiguration { + logLevel?: LogLevelDesc; + socket: SocketIoSocket; + gateway: Gateway; +} + +/** + * Return secure string representation of error from the input. + * Handles circular structures and removes HTML.` + * + * @param error Any object to return as an error, preferable `Error` + * @returns Safe string representation of an error. + * + * @todo use one from cactus-common after #2089 is merged. + */ +export function safeStringifyException(error: unknown) { + if (error instanceof Error) { + return sanitizeHtml(error.stack || error.message); + } + + return sanitizeHtml(safeStringify(error)); +} + +/** + * Endpoint to watch for new blocks on fabric ledger and report them + * to client using socketio. + */ +export class WatchBlocksV1Endpoint { + public static readonly CLASS_NAME = "WatchBlocksV1Endpoint"; + + private readonly log: Logger; + private readonly socket: SocketIoSocket< + Record void> + >; + + constructor(public readonly config: IWatchBlocksV1EndpointConfiguration) { + const fnTag = `${this.className}#constructor()`; + Checks.truthy(config, `${fnTag} arg options`); + Checks.truthy(config.socket, `${fnTag} arg options.socket`); + + this.socket = config.socket; + + const level = this.config.logLevel || "INFO"; + const label = this.className; + this.log = LoggerProvider.getOrCreate({ level, label }); + } + + /** + * Get this class name. + */ + public get className(): string { + return WatchBlocksV1Endpoint.CLASS_NAME; + } + + /** + * Callback executed when receiving block with custom cactus type "cactus:transactions" + * Sends WatchBlocksV1.Next with new block to the client. + * + * @param blockEvent full block + * + * @returns Nothing. + */ + private monitorCactusTransactionsCallback(blockEvent: BlockEvent) { + const { socket, log } = this; + const clientId = socket.id; + log.debug( + `CactusTransactions BlockEvent received: #${blockEvent.blockNumber.toString()}, client: ${clientId}`, + ); + + if (!("data" in blockEvent.blockData)) { + log.error("Wrong blockEvent type received - should not happen!"); + return; + } + + const blockData = blockEvent.blockData.data?.data as any; + if (!blockData) { + log.debug("Block data empty - ignore..."); + return; + } + + const transactions: WatchBlocksCactusTransactionsEventV1[] = []; + for (const data of blockData) { + try { + const payload = data.payload; + const transaction = payload.data; + const actionPayload = transaction.actions[0].payload; + const proposalPayload = actionPayload.chaincode_proposal_payload; + const invocationSpec = proposalPayload.input; + + // Decode args and function name + const rawArgs = invocationSpec.chaincode_spec.input.args as Buffer[]; + const decodedArgs = rawArgs.map((arg: Buffer) => arg.toString("utf8")); + const functionName = decodedArgs.shift() ?? ""; + + const chaincodeId = invocationSpec.chaincode_spec.chaincode_id.name; + const channelHeader = payload.header.channel_header; + const transactionId = channelHeader.tx_id; + + transactions.push({ + chaincodeId, + transactionId, + functionName, + functionArgs: decodedArgs, + }); + } catch (error) { + log.error( + "Could not retrieve transaction from received block. Error:", + safeStringifyException(error), + ); + } + } + + socket.emit(WatchBlocksV1.Next, { + cactusTransactionsEvents: transactions, + }); + } + + /** + * Callback executed when receiving block with standard type "full" + * Sends WatchBlocksV1.Next with new block to the client. + * + * @param blockEvent full block + * + * @returns Nothing. + */ + private monitorFullCallback(blockEvent: BlockEvent) { + const { socket, log } = this; + const clientId = socket.id; + log.debug( + `Full BlockEvent received: #${blockEvent.blockNumber.toString()}, client: ${clientId}`, + ); + + if (!("data" in blockEvent.blockData)) { + log.error("Wrong blockEvent type received - should not happen!"); + return; + } + + socket.emit(WatchBlocksV1.Next, { + fullBlock: blockEvent, + }); + } + + /** + * Callback executed when receiving block with standard type "filtered" + * Sends WatchBlocksV1.Next with new block to the client. + * + * @param blockEvent filtered block + * + * @returns Nothing. + */ + private monitorFilteredCallback(blockEvent: BlockEvent) { + const { socket, log } = this; + const clientId = socket.id; + log.debug( + `Filtered BlockEvent received: #${blockEvent.blockNumber.toString()}, client: ${clientId}`, + ); + + if (!("filtered_transactions" in blockEvent.blockData)) { + log.error("Wrong blockEvent type received - should not happen!"); + return; + } + + socket.emit(WatchBlocksV1.Next, { + filteredBlock: blockEvent, + }); + } + + /** + * Callback executed when receiving block with standard type "private" + * Sends WatchBlocksV1.Next with new block to the client. + * + * @param blockEvent private block + * + * @returns Nothing. + */ + private monitorPrivateCallback(blockEvent: BlockEvent) { + const { socket, log } = this; + const clientId = socket.id; + log.debug( + `Private BlockEvent received: #${blockEvent.blockNumber.toString()}, client: ${clientId}`, + ); + + if (!("data" in blockEvent.blockData)) { + log.error("Wrong blockEvent type received - should not happen!"); + return; + } + + socket.emit(WatchBlocksV1.Next, { + privateBlock: blockEvent, + }); + } + + /** + * Get block listener callback and listener type it's expect. + * Returns separate function object each time it's called (this is required y fabric node SDK). + * + * @param type requested listener type (including custom Cactus ones). + * + * @returns listener: BlockListener; + * @returns listenerType: BlockType; + */ + private getBlockListener(type: WatchBlocksListenerTypeV1) { + let listener: BlockListener; + let listenerType: EventType; + + switch (type) { + case WatchBlocksListenerTypeV1.Full: + listener = async (blockEvent) => this.monitorFullCallback(blockEvent); + listenerType = "full"; + break; + case WatchBlocksListenerTypeV1.Filtered: + listener = async (blockEvent) => + this.monitorFilteredCallback(blockEvent); + listenerType = "filtered"; + break; + case WatchBlocksListenerTypeV1.Private: + listener = async (blockEvent) => + this.monitorPrivateCallback(blockEvent); + listenerType = "private"; + break; + case WatchBlocksListenerTypeV1.CactusTransactions: + listener = async (blockEvent) => + this.monitorCactusTransactionsCallback(blockEvent); + listenerType = "full"; + break; + default: + // Will not compile if any type was not handled by above switch. + const unknownType: never = type; + throw new Error( + `Unknown block listen type - '${unknownType}'. Check name and connector version.`, + ); + } + + if (!listener || !listenerType) { + // Should never happen + throw new Error("Could not determine listener or listenerType."); + } + + return { listener, listenerType }; + } + + /** + * Subscribe to new blocks on fabric ledger, push them to the client via socketio. + * + * @param options Block monitoring options. + */ + public async subscribe(options: WatchBlocksOptionsV1) { + const { socket, log } = this; + const clientId = socket.id; + log.info(`${WatchBlocksV1.Subscribe} => clientId: ${clientId}`); + log.debug( + "WatchBlocksV1.Subscribe args: channelName:", + options.channelName, + ", startBlock:", + options.startBlock, + ", type: ", + options.type, + ); + + try { + Checks.truthy(options.channelName, "Missing channel name"); + const network = await this.config.gateway.getNetwork(options.channelName); + + const { listener, listenerType } = this.getBlockListener(options.type); + + log.debug("Subscribing to new blocks... listenerType:", listenerType); + // @todo Add support for checkpointer (long-term improvement) + // https://hyperledger.github.io/fabric-sdk-node/release-2.2/module-fabric-network.Checkpointer.html + await network.addBlockListener(listener, { + startBlock: options.startBlock, + type: listenerType, + }); + + socket.on("disconnect", async (reason: string) => { + log.info( + "WebSocket:disconnect => reason=%o clientId=%s", + reason, + clientId, + ); + network.removeBlockListener(listener); + this.close(); + }); + + socket.on(WatchBlocksV1.Unsubscribe, () => { + log.info(`${WatchBlocksV1.Unsubscribe} => clientId: ${clientId}`); + this.close(); + }); + } catch (error: any) { + const errorMessage = safeStringifyException(error); + log.error(errorMessage); + socket.emit(WatchBlocksV1.Error, { + code: 500, + errorMessage, + }); + } + } + + close() { + if (this.socket.connected) { + this.socket.disconnect(true); + } + this.config.gateway.disconnect(); + } +} diff --git a/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/fabric-watch-blocks-v1-endpoint.test.ts b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/fabric-watch-blocks-v1-endpoint.test.ts new file mode 100644 index 00000000000..be09104d657 --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-fabric/src/test/typescript/integration/fabric-v2-2-x/fabric-watch-blocks-v1-endpoint.test.ts @@ -0,0 +1,403 @@ +/** + * Functional test of WatchBlocksV1Endpoint on connector-fabric (packages/cactus-plugin-ledger-connector-fabric) + * Assumes sample CC was already deployed on the test ledger. + * + * @note - this test sometimes hangs infinitely when used with fabric-node-sdk 2.3.0, + * probably due to bug in the underlying dependency grpc-js. Problem does not occur on 2.5.0. + */ + +////////////////////////////////// +// Constants +////////////////////////////////// + +// Ledger settings +const imageName = "ghcr.io/hyperledger/cactus-fabric2-all-in-one"; +const imageVersion = "2021-09-02--fix-876-supervisord-retries"; +const fabricEnvVersion = "2.2.0"; +const fabricEnvCAVersion = "1.4.9"; +const ledgerChannelName = "mychannel"; +const ledgerContractName = "basic"; + +// Log settings +const testLogLevel: LogLevelDesc = "info"; // default: info +const sutLogLevel: LogLevelDesc = "info"; // default: info + +import "jest-extended"; +import http from "http"; +import { AddressInfo } from "net"; +import { v4 as uuidv4 } from "uuid"; +import bodyParser from "body-parser"; +import express from "express"; +import { Server as SocketIoServer } from "socket.io"; +import { DiscoveryOptions } from "fabric-network"; + +import { + FabricTestLedgerV1, + pruneDockerAllIfGithubAction, +} from "@hyperledger/cactus-test-tooling"; + +import { + LogLevelDesc, + LoggerProvider, + Logger, + IListenOptions, + Servers, +} from "@hyperledger/cactus-common"; + +import { Constants, Configuration } from "@hyperledger/cactus-core-api"; + +import { PluginRegistry } from "@hyperledger/cactus-core"; + +import { PluginKeychainMemory } from "@hyperledger/cactus-plugin-keychain-memory"; + +import { + PluginLedgerConnectorFabric, + FabricContractInvocationType, + DefaultEventHandlerStrategy, + FabricSigningCredential, + FabricApiClient, + WatchBlocksListenerTypeV1, + WatchBlocksResponseV1, +} from "../../../../main/typescript/public-api"; + +// Logger setup +const log: Logger = LoggerProvider.getOrCreate({ + label: "fabric-watch-blocks-v1-endpoint.test", + level: testLogLevel, +}); + +/** + * Main test suite + */ +describe("Fabric-SocketIO connector tests", () => { + let ledger: FabricTestLedgerV1; + let signingCredential: FabricSigningCredential; + let fabricConnectorPlugin: PluginLedgerConnectorFabric; + let connectorServer: http.Server; + let socketioServer: SocketIoServer; + let apiClient: FabricApiClient; + + ////////////////////////////////// + // Environment Setup + ////////////////////////////////// + + beforeAll(async () => { + log.info("Prune Docker..."); + await pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + + // Start Ledger + log.info("Start FabricTestLedgerV1..."); + log.debug("Version:", fabricEnvVersion, "CA Version:", fabricEnvCAVersion); + ledger = new FabricTestLedgerV1({ + emitContainerLogs: false, + publishAllPorts: true, + logLevel: testLogLevel, + imageName, + imageVersion, + envVars: new Map([ + ["FABRIC_VERSION", fabricEnvVersion], + ["CA_VERSION", fabricEnvCAVersion], + ]), + }); + log.debug("Fabric image:", ledger.getContainerImageName()); + await ledger.start(); + + // Get connection profile + log.info("Get fabric connection profile for Org1..."); + const connectionProfile = await ledger.getConnectionProfileOrg1(); + expect(connectionProfile).toBeTruthy(); + + // Enroll admin and user + const enrollAdminOut = await ledger.enrollAdmin(); + const adminWallet = enrollAdminOut[1]; + const [userIdentity] = await ledger.enrollUser(adminWallet); + + // Create Keychain Plugin + const keychainId = uuidv4(); + const keychainEntryKey = "user2"; + const keychainPlugin = new PluginKeychainMemory({ + instanceId: uuidv4(), + keychainId, + logLevel: sutLogLevel, + backend: new Map([[keychainEntryKey, JSON.stringify(userIdentity)]]), + }); + signingCredential = { + keychainId, + keychainRef: keychainEntryKey, + }; + + // Create Connector Plugin + const discoveryOptions: DiscoveryOptions = { + enabled: true, + asLocalhost: true, + }; + fabricConnectorPlugin = new PluginLedgerConnectorFabric({ + instanceId: uuidv4(), + pluginRegistry: new PluginRegistry({ plugins: [keychainPlugin] }), + sshConfig: await ledger.getSshConfig(), + cliContainerEnv: {}, + peerBinary: "/fabric-samples/bin/peer", + logLevel: sutLogLevel, + connectionProfile, + discoveryOptions, + eventHandlerOptions: { + strategy: DefaultEventHandlerStrategy.NetworkScopeAnyfortx, + commitTimeout: 300, + }, + }); + + // Run http server + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + connectorServer = http.createServer(expressApp); + const listenOptions: IListenOptions = { + hostname: "127.0.0.1", + port: 0, + server: connectorServer, + }; + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + const apiHost = `http://${addressInfo.address}:${addressInfo.port}`; + + // Run socketio server + socketioServer = new SocketIoServer(connectorServer, { + path: Constants.SocketIoConnectionPathV1, + }); + + // Register services + await fabricConnectorPlugin.getOrCreateWebServices(); + await fabricConnectorPlugin.registerWebServices(expressApp, socketioServer); + + // Create ApiClient + const apiConfig = new Configuration({ basePath: apiHost }); + apiClient = new FabricApiClient(apiConfig); + }); + + afterAll(async () => { + log.info("FINISHING THE TESTS"); + + if (fabricConnectorPlugin) { + log.info("Close ApiClient connections..."); + fabricConnectorPlugin.shutdown(); + } + + if (apiClient) { + log.info("Close ApiClient connections..."); + apiClient.close(); + } + + if (socketioServer) { + log.info("Stop the SocketIO server connector..."); + await new Promise((resolve) => + socketioServer.close(() => resolve()), + ); + } + + if (connectorServer) { + log.info("Stop the HTTP server connector..."); + await new Promise((resolve) => + connectorServer.close(() => resolve()), + ); + } + + if (ledger) { + log.info("Stop the fabric ledger..."); + await ledger.stop(); + await ledger.destroy(); + } + + log.info("Prune Docker..."); + await pruneDockerAllIfGithubAction({ logLevel: testLogLevel }); + }); + + ////////////////////////////////// + // Helpers + ////////////////////////////////// + + /** + * Common logic for executing watchBlock monitoring tests. + * Will subscribe to new blocks and send new transaction, to trigger creation of the new block. + * + * @param monitorName Unique name, will be used for identification and in transaction argument. + * @param type Type of block to receive. + * @param checkEventCallback Callback called when received the event from the connector. + * + * @returns Monitoring promise - will resolve if `checkEventCallback` passes, reject if it throws. + */ + async function testWatchBlock( + monitorName: string, + type: WatchBlocksListenerTypeV1, + checkEventCallback: (event: WatchBlocksResponseV1) => void, + ) { + // Start monitoring + const monitorPromise = new Promise((resolve, reject) => { + const watchObservable = apiClient.watchBlocksV1({ + channelName: ledgerChannelName, + gatewayOptions: { + identity: signingCredential.keychainRef, + wallet: { + keychain: signingCredential, + }, + }, + type, + }); + + const subscription = watchObservable.subscribe({ + next(event) { + log.debug("Received event:", JSON.stringify(event)); + try { + checkEventCallback(event); + subscription.unsubscribe(); + resolve(); + } catch (err) { + log.error("watchBlocksV1() event check error:", err); + subscription.unsubscribe(); + reject(err); + } + }, + error(err) { + log.error("watchBlocksV1() error:", err); + subscription.unsubscribe(); + reject(err); + }, + }); + }); + + // Create new asset to trigger new block creation + const createAssetResponse = await apiClient.runTransactionV1({ + signingCredential, + channelName: ledgerChannelName, + invocationType: FabricContractInvocationType.Send, + contractName: ledgerContractName, + methodName: "CreateAsset", + params: [monitorName, "green", "111", "someOwner", "299"], + }); + expect(createAssetResponse).toBeTruthy(); + expect(createAssetResponse.status).toEqual(200); + expect(createAssetResponse.data).toBeTruthy(); + expect(createAssetResponse.data.success).toBeTrue(); + expect(createAssetResponse.data.transactionId).toBeTruthy(); + log.debug( + "runTransactionV1 response:", + JSON.stringify(createAssetResponse.data), + ); + + return monitorPromise; + } + + ////////////////////////////////// + // Tests + ////////////////////////////////// + + /** + * Check full block monitoring + */ + test("Monitoring with type Full returns entire raw block", async () => { + const monitorPromise = testWatchBlock( + "FullBlockTest", + WatchBlocksListenerTypeV1.Full, + (event) => { + expect(event).toBeTruthy(); + + if (!("fullBlock" in event)) { + throw new Error( + `Unexpected response from the connector: ${JSON.stringify(event)}`, + ); + } + + const fullBlock = event.fullBlock; + expect(fullBlock.blockNumber).toBeTruthy(); + expect(fullBlock.blockData).toBeTruthy(); + expect(fullBlock.blockData.header).toBeTruthy(); + expect(fullBlock.blockData.data).toBeTruthy(); + expect(fullBlock.blockData.metadata).toBeTruthy(); + }, + ); + + await monitorPromise; + }); + + /** + * Check filtered block monitoring + */ + test("Monitoring with type Filtered returns filtered block", async () => { + const monitorPromise = testWatchBlock( + "FilteredBlockTest", + WatchBlocksListenerTypeV1.Filtered, + (event) => { + expect(event).toBeTruthy(); + + if (!("filteredBlock" in event)) { + throw new Error( + `Unexpected response from the connector: ${JSON.stringify(event)}`, + ); + } + + const filteredBlock = event.filteredBlock; + expect(filteredBlock.blockNumber).toBeTruthy(); + expect(filteredBlock.blockData).toBeTruthy(); + expect(filteredBlock.blockData.channel_id).toBeTruthy(); + expect(filteredBlock.blockData.number).toBeTruthy(); + expect(filteredBlock.blockData.filtered_transactions).toBeTruthy(); + }, + ); + + await monitorPromise; + }); + + /** + * Check private block monitoring + */ + test("Monitoring with type Private returns private block", async () => { + const monitorPromise = testWatchBlock( + "PrivateBlockTest", + WatchBlocksListenerTypeV1.Private, + (event) => { + expect(event).toBeTruthy(); + + if (!("privateBlock" in event)) { + throw new Error( + `Unexpected response from the connector: ${JSON.stringify(event)}`, + ); + } + + const fullBlock = event.privateBlock; + expect(fullBlock.blockNumber).toBeTruthy(); + expect(fullBlock.blockData).toBeTruthy(); + expect(fullBlock.blockData.header).toBeTruthy(); + expect(fullBlock.blockData.data).toBeTruthy(); + expect(fullBlock.blockData.metadata).toBeTruthy(); + }, + ); + + await monitorPromise; + }); + + /** + * Check Cactus custom transactions summary block monitoring. + * This format is compatible with legacy fabric-socketio output. + */ + test("Monitoring with type CactusTransactions returns transactions summary", async () => { + const monitorPromise = testWatchBlock( + "CactusTransactionsTest", + WatchBlocksListenerTypeV1.CactusTransactions, + (event) => { + expect(event).toBeTruthy(); + + if (!("cactusTransactionsEvents" in event)) { + throw new Error( + `Unexpected response from the connector: ${JSON.stringify(event)}`, + ); + } + + const eventData = event.cactusTransactionsEvents; + expect(eventData.length).toBeGreaterThan(0); + expect(eventData[0].chaincodeId).toBeTruthy(); + expect(eventData[0].transactionId).toBeTruthy(); + expect(eventData[0].functionName).toBeTruthy(); + expect(eventData[0].functionArgs).toBeTruthy(); + }, + ); + + await monitorPromise; + }); +}); diff --git a/yarn.lock b/yarn.lock index a935f15f439..1bd54934a30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2893,6 +2893,14 @@ dependencies: "@types/node" ">=12.12.47" +"@grpc/grpc-js@1.6.7": + version "1.6.7" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.6.7.tgz#4c4fa998ff719fe859ac19fe977fdef097bb99aa" + integrity sha512-eBM03pu9hd3VqDQG+kHahiG1x80RGkkqqRb1Pchcwqej/KkAH95gAvKs6laqaHCycYaPK+TKuNQnOz9UXYA8qw== + dependencies: + "@grpc/proto-loader" "^0.6.4" + "@types/node" ">=12.12.47" + "@grpc/grpc-js@^1.3.4", "@grpc/grpc-js@~1.5.0": version "1.5.7" resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.5.7.tgz#c83a5dc1d0cf7b8aa82371cfa7125955d1f25a96" @@ -2923,6 +2931,17 @@ protobufjs "^6.10.0" yargs "^16.2.0" +"@grpc/proto-loader@^0.6.10": + version "0.6.13" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.6.13.tgz#008f989b72a40c60c96cd4088522f09b05ac66bc" + integrity sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g== + dependencies: + "@types/long" "^4.0.1" + lodash.camelcase "^4.3.0" + long "^4.0.0" + protobufjs "^6.11.3" + yargs "^16.2.0" + "@hapi/hoek@^9.0.0": version "9.2.1" resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.1.tgz#9551142a1980503752536b5050fd99f4a7f13b17" @@ -6534,6 +6553,11 @@ async@^2.4.0, async@^2.6.2: dependencies: lodash "^4.17.14" +async@^3.0.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + async@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" @@ -6646,6 +6670,13 @@ axios@0.24.0: dependencies: follow-redirects "^1.14.4" +axios@^0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.26.1.tgz#1ede41c51fcf51bbbd6fd43669caaa4f0495aaa9" + integrity sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA== + dependencies: + follow-redirects "^1.14.8" + axobject-query@2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.0.2.tgz#ea187abe5b9002b377f925d8bf7d1c561adf38f9" @@ -11870,13 +11901,13 @@ fabric-ca-client@2.2.10: url "^0.11.0" winston "^2.4.5" -fabric-ca-client@2.3.0-snapshot.62: - version "2.3.0-snapshot.62" - resolved "https://registry.yarnpkg.com/fabric-ca-client/-/fabric-ca-client-2.3.0-snapshot.62.tgz#88770a6cb2c846770a00765b7c222e50ed28e5a4" - integrity sha512-5PGEL+oEaCXwrSHPkVkURF+f5LosZRzYygRPZWlCh81CDScwSmpaPrj3UMObYHbb2YUkodtll2aTfQPhhMnVSQ== +fabric-ca-client@2.5.0-snapshot.8: + version "2.5.0-snapshot.8" + resolved "https://registry.yarnpkg.com/fabric-ca-client/-/fabric-ca-client-2.5.0-snapshot.8.tgz#e9087c51d3171f79e397c7f09e8e5895ccb4dbf4" + integrity sha512-fosQlAhYSvaUNADiBgZKFIZjQk6oMS6/rsNnrxz6CQ4u+Lq9V4pqOOYo1dyTcbuIOYGZITx7WB2JWdNXqCiTcA== dependencies: - fabric-common "2.3.0-snapshot.63" - jsrsasign "^10.4.1" + fabric-common "2.5.0-snapshot.8" + jsrsasign "^10.5.25" url "^0.11.0" winston "^2.4.5" @@ -11930,18 +11961,17 @@ fabric-common@2.2.10: optionalDependencies: pkcs11js "^1.0.6" -fabric-common@2.3.0-snapshot.63: - version "2.3.0-snapshot.63" - resolved "https://registry.yarnpkg.com/fabric-common/-/fabric-common-2.3.0-snapshot.63.tgz#ecc3512e9a002221ea4edcd2ce92d41691887a1a" - integrity sha512-qfuwC/3f4ViHhjGrXikE63J1AWI6iB0U72ZY7IUiNLW444wum7g5kWlY5HURu794HsS5R2y9UA6GUdx1ng92Lg== +fabric-common@2.5.0-snapshot.8: + version "2.5.0-snapshot.8" + resolved "https://registry.yarnpkg.com/fabric-common/-/fabric-common-2.5.0-snapshot.8.tgz#a41226874b751e9b9e57a7bead95efb7e6c14853" + integrity sha512-zX1jsh+nlUjK3A9Ukq4DbmSHrvhsBmLj8Qq/7t1w29ggIC53Y5oy31IaIOu0Lgc5ZMULA0IwdxD4gsndVUKw4A== dependencies: callsite "^1.0.0" elliptic "^6.5.4" - fabric-protos "2.3.0-snapshot.63" + fabric-protos "2.5.0-snapshot.8" js-sha3 "^0.8.0" - jsrsasign "^10.4.1" - long "^4.0.0" - nconf "^0.11.2" + jsrsasign "^10.5.25" + nconf "^0.12.0" promise-settle "^0.3.0" sjcl "^1.0.8" winston "^2.4.5" @@ -11970,15 +12000,14 @@ fabric-network@2.2.10: long "^4.0.0" nano "^9.0.3" -fabric-network@2.3.0-snapshot.62: - version "2.3.0-snapshot.62" - resolved "https://registry.yarnpkg.com/fabric-network/-/fabric-network-2.3.0-snapshot.62.tgz#2a40ed1357a249fa47d8543432c241c9344b1248" - integrity sha512-a5krmkWtfTnbuQ9azbPOSUZjOHc9UJjQOmoXdsreoclsF4fSDcTbM3VscDbW1gRZBtdHdYJ9QB6tNTUEGVuPsQ== +fabric-network@2.5.0-snapshot.8: + version "2.5.0-snapshot.8" + resolved "https://registry.yarnpkg.com/fabric-network/-/fabric-network-2.5.0-snapshot.8.tgz#a86d6eefac48baefb7cfa37f0fda52732b6a23e1" + integrity sha512-4WZk+zEjqi1YK3nubh+qt5okoGLbvpTvSQzSW+muFOWczckJBWCwaZqzz47bKGSKZKJw9P6w+DW4pmokNaX6Eg== dependencies: - fabric-common "2.3.0-snapshot.63" - fabric-protos "2.3.0-snapshot.63" - long "^4.0.0" - nano "^9.0.3" + fabric-common "2.5.0-snapshot.8" + fabric-protos "2.5.0-snapshot.8" + nano "^10.0.0" fabric-protos@2.2.10: version "2.2.10" @@ -11989,13 +12018,13 @@ fabric-protos@2.2.10: "@grpc/proto-loader" "^0.6.2" protobufjs "^6.11.2" -fabric-protos@2.3.0-snapshot.63: - version "2.3.0-snapshot.63" - resolved "https://registry.yarnpkg.com/fabric-protos/-/fabric-protos-2.3.0-snapshot.63.tgz#fbe3d942b498df9cfcd40c4eb3fadf65f851e9aa" - integrity sha512-jJlc9haZtUTDl9kPyAaV0fftMqDu7IRgyHnAd9mNfy0EoKl8xUCyDfrNrNEHBzUyayMlMXqUlZ8GGGU6WLjnOA== +fabric-protos@2.5.0-snapshot.8: + version "2.5.0-snapshot.8" + resolved "https://registry.yarnpkg.com/fabric-protos/-/fabric-protos-2.5.0-snapshot.8.tgz#d3b1188065405472680a6469394f274a31139ce9" + integrity sha512-sL+qPSlinXFyo+bGBcjXap30Tf9qNBwBgxib+D7sVBaO6OrVhTyXCNXBZLFcqebviI5Wj1dwG3AgRVBxgptV9Q== dependencies: - "@grpc/grpc-js" "^1.3.4" - "@grpc/proto-loader" "^0.6.2" + "@grpc/grpc-js" "1.6.7" + "@grpc/proto-loader" "^0.6.10" protobufjs "^6.11.2" fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: @@ -12279,6 +12308,11 @@ follow-redirects@^1.0.0, follow-redirects@^1.12.1, follow-redirects@^1.14.0, fol resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== +follow-redirects@^1.14.8: + version "1.15.1" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5" + integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -15893,6 +15927,11 @@ jsrsasign@^10.4.0, jsrsasign@^10.4.1: resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-10.5.8.tgz#df269e5694ebb53245c4dadfca68ee2fc8aa41ec" integrity sha512-ewFUGPZJujIR9j84Q5LEzPTG4D1qQZ4CjJrgHfMEAAiArkC3xfdgNP0ZAXXxXbb+K8Phw15soOIJ8bX3+usEdQ== +jsrsasign@^10.5.25: + version "10.5.26" + resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-10.5.26.tgz#bc36d4c4019c83f144066725ea0ca6ab306702fc" + integrity sha512-TjEu1yPdI+8whpe6CA/6XNb7U1sm9+PUItOUfSThOLvx7JCfYHIfuvZK2Egz2DWUKioafn98LPuk+geLGckxMg== + jszip@^3.1.3: version "3.7.1" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.7.1.tgz#bd63401221c15625a1228c556ca8a68da6fda3d9" @@ -17780,6 +17819,17 @@ nano-json-stream-parser@^0.1.2: resolved "https://registry.yarnpkg.com/nano-json-stream-parser/-/nano-json-stream-parser-0.1.2.tgz#0cc8f6d0e2b622b479c40d499c46d64b755c6f5f" integrity sha1-DMj20OK2IrR5xA1JnEbWS3Vcb18= +nano@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/nano/-/nano-10.0.0.tgz#317a4dfa80c46ef97bbddc8623f4a36074106ac3" + integrity sha512-evvi6NpUkFlqT1hKxj6YZ2vaQwVWvL1/v/hbqrApDSG0ZE/fSsuzlIxr9pd9JlhkrwI+zw4RDIsitaOYTC94YQ== + dependencies: + "@types/tough-cookie" "^4.0.0" + axios "^0.26.1" + axios-cookiejar-support "^1.0.1" + qs "^6.10.3" + tough-cookie "^4.0.0" + nano@^6.4.4: version "6.4.4" resolved "https://registry.yarnpkg.com/nano/-/nano-6.4.4.tgz#4902a095e5186cfb23612c78826ea755b76fadf0" @@ -17879,6 +17929,16 @@ nconf@^0.11.2: secure-keys "^1.0.0" yargs "^16.1.1" +nconf@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/nconf/-/nconf-0.12.0.tgz#9cf70757aae4d440d43ed53c42f87da18471b8bf" + integrity sha512-T3fZPw3c7Dfrz8JBQEbEcZJ2s8f7cUMpKuyBtsGQe0b71pcXx6gNh4oti2xh5dxB+gO9ufNfISBlGvvWtfyMcA== + dependencies: + async "^3.0.0" + ini "^2.0.0" + secure-keys "^1.0.0" + yargs "^16.1.1" + needle@^2.5.2: version "2.9.1" resolved "https://registry.yarnpkg.com/needle/-/needle-2.9.1.tgz#22d1dffbe3490c2b83e301f7709b6736cd8f2684" @@ -17992,7 +18052,7 @@ node-gyp-build@^4.2.0, node-gyp-build@^4.2.2, node-gyp-build@^4.3.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== -node-gyp@8.x, node-gyp@^8.2.0: +node-gyp@8.x, node-gyp@^8.0.0, node-gyp@^8.2.0: version "8.4.1" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w== @@ -18041,22 +18101,6 @@ node-gyp@^7.1.0: tar "^6.0.2" which "^2.0.2" -node-gyp@^8.0.0, node-gyp@^8.2.0: - version "8.4.1" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" - integrity sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w== - dependencies: - env-paths "^2.2.0" - glob "^7.1.4" - graceful-fs "^4.2.6" - make-fetch-happen "^9.1.0" - nopt "^5.0.0" - npmlog "^6.0.0" - rimraf "^3.0.2" - semver "^7.3.5" - tar "^6.1.2" - which "^2.0.2" - node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -20467,6 +20511,25 @@ protobufjs@6.11.2, protobufjs@^6.10.0, protobufjs@^6.10.2, protobufjs@^6.11.2: "@types/node" ">=13.7.0" long "^4.0.0" +protobufjs@^6.11.3: + version "6.11.3" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.3.tgz#637a527205a35caa4f3e2a9a4a13ddffe0e7af74" + integrity sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" ">=13.7.0" + long "^4.0.0" + protoc-gen-ts@0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/protoc-gen-ts/-/protoc-gen-ts-0.6.0.tgz#a2c3e117ae43fa7821a7f46dee25fb6d20d12565" @@ -20620,7 +20683,7 @@ qs@6.9.7: resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe" integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== -qs@^6.9.4: +qs@^6.10.3, qs@^6.9.4: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==