diff --git a/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/plugin-ledger-connector-corda.ts b/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/plugin-ledger-connector-corda.ts index bc95d15685..5796a889a0 100644 --- a/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/plugin-ledger-connector-corda.ts +++ b/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/plugin-ledger-connector-corda.ts @@ -32,6 +32,18 @@ import { IInvokeContractEndpointV1Options, InvokeContractEndpointV1, } from "./web-services/invoke-contract-endpoint-v1"; +import { + IListFlowsEndpointV1Options, + ListFlowsEndpointV1, +} from "./web-services/list-flows-endpoint-v1"; +import { + INetworkMapEndpointV1Options, + NetworkMapEndpointV1, +} from "./web-services/network-map-endpoint-v1"; +import { + IDiagnoseNodeEndpointV1Options, + DiagnoseNodeEndpointV1, +} from "./web-services/diagnose-node-endpoint-v1"; export interface IPluginLedgerConnectorCordaOptions extends ICactusPluginOptions { @@ -41,6 +53,7 @@ export interface IPluginLedgerConnectorCordaOptions prometheusExporter?: PrometheusExporter; cordaStartCmd?: string; cordaStopCmd?: string; + apiUrl?: string; } export class PluginLedgerConnectorCorda @@ -147,6 +160,7 @@ export class PluginLedgerConnectorCorda corDappsDir: this.options.corDappsDir, cordaStartCmd: this.options.cordaStartCmd, cordaStopCmd: this.options.cordaStopCmd, + apiUrl: this.options.apiUrl, }); endpoints.push(endpoint); @@ -154,7 +168,7 @@ export class PluginLedgerConnectorCorda { const opts: IInvokeContractEndpointV1Options = { - connector: this, + apiUrl: this.options.apiUrl, logLevel: this.options.logLevel, }; const endpoint = new InvokeContractEndpointV1(opts); @@ -169,6 +183,34 @@ export class PluginLedgerConnectorCorda const endpoint = new GetPrometheusExporterMetricsEndpointV1(opts); endpoints.push(endpoint); } + + { + const opts: IListFlowsEndpointV1Options = { + apiUrl: this.options.apiUrl, + logLevel: this.options.logLevel, + }; + const endpoint = new ListFlowsEndpointV1(opts); + endpoints.push(endpoint); + } + + { + const opts: INetworkMapEndpointV1Options = { + apiUrl: this.options.apiUrl, + logLevel: this.options.logLevel, + }; + const endpoint = new NetworkMapEndpointV1(opts); + endpoints.push(endpoint); + } + + { + const opts: IDiagnoseNodeEndpointV1Options = { + apiUrl: this.options.apiUrl, + logLevel: this.options.logLevel, + }; + const endpoint = new DiagnoseNodeEndpointV1(opts); + endpoints.push(endpoint); + } + this.log.info(`Instantiated endpoints of ${pkgName}`); return endpoints; } diff --git a/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/web-services/deploy-contract-jars-endpoint.ts b/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/web-services/deploy-contract-jars-endpoint.ts index 5a20a7479c..be6c3a40ed 100644 --- a/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/web-services/deploy-contract-jars-endpoint.ts +++ b/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/web-services/deploy-contract-jars-endpoint.ts @@ -1,13 +1,10 @@ -import fs from "fs"; -import path from "path"; - import { Express, Request, Response } from "express"; -import temp from "temp"; -import { NodeSSH, Config as SshConfig } from "node-ssh"; +import { Config as SshConfig } from "node-ssh"; import { IWebServiceEndpoint, IExpressRequestHandler, + Configuration, } from "@hyperledger/cactus-core-api"; import { @@ -26,6 +23,7 @@ import { import { IEndpointAuthzOptions } from "@hyperledger/cactus-core-api"; import { + DefaultApi, DeployContractJarsSuccessV1Response, DeployContractJarsV1Request, } from "../generated/openapi/typescript-axios/api"; @@ -39,6 +37,7 @@ export interface IDeployContractEndpointOptions { cordaStartCmd?: string; cordaStopCmd?: string; authorizationOptionsProvider?: AuthorizationOptionsProvider; + apiUrl?: string; } const K_DEFAULT_AUTHORIZATION_OPTIONS: IEndpointAuthzOptions = { @@ -51,6 +50,7 @@ export class DeployContractJarsEndpoint implements IWebServiceEndpoint { private readonly log: Logger; private readonly authorizationOptionsProvider: AuthorizationOptionsProvider; + private readonly apiUrl?: string; public get className(): string { return DeployContractJarsEndpoint.CLASS_NAME; @@ -71,6 +71,7 @@ export class DeployContractJarsEndpoint implements IWebServiceEndpoint { AuthorizationOptionsProvider.of(K_DEFAULT_AUTHORIZATION_OPTIONS, level); this.log.debug(`Instantiated ${this.className} OK`); + this.apiUrl = options.apiUrl; } getAuthorizationOptionsProvider(): IAsyncProvider { @@ -126,7 +127,8 @@ export class DeployContractJarsEndpoint implements IWebServiceEndpoint { this.log.debug(`${verb} ${thePath} handleRequest()`); try { - const body = await this.doDeploy(req.body); + if (this.apiUrl === undefined) throw "apiUrl option is necessary"; + const body = await this.callInternalContainer(req.body); res.status(200); res.json(body); } catch (ex) { @@ -140,75 +142,12 @@ export class DeployContractJarsEndpoint implements IWebServiceEndpoint { } } - private async doDeploy( - reqBody: DeployContractJarsV1Request, + async callInternalContainer( + req: DeployContractJarsV1Request, ): Promise { - const fnTag = `${this.className}#doDeploy()`; - this.log.debug(`ENTER doDeploy()`); - - if (!Array.isArray(reqBody.jarFiles)) { - throw new TypeError(`${fnTag} expected req.files to be an array`); - } - - const { sshConfigAdminShell, corDappsDir: cordappDir } = this.options; - const ssh = new NodeSSH(); - try { - const resBody: DeployContractJarsSuccessV1Response = { - deployedJarFiles: [], - }; - - temp.track(); - const prefix = `hyperledger-cactus-${this.className}`; - const tmpDirPath = temp.mkdirSync(prefix); - - await ssh.connect(sshConfigAdminShell); - - await this.stopCordaNode(ssh); - - for (const aJarFile of reqBody.jarFiles) { - const localFilePath = path.join(tmpDirPath, aJarFile.filename); - const remoteFilePath = path.join(cordappDir, aJarFile.filename); - - fs.writeFileSync(localFilePath, aJarFile.contentBase64, "base64"); - - this.log.debug(`SCP from/to %o => %o`, localFilePath, remoteFilePath); - await ssh.putFile(localFilePath, remoteFilePath); - this.log.debug(`SCP OK %o`, remoteFilePath); - } - - await this.startCordaNode(ssh); - - fs.rmdirSync(tmpDirPath, { recursive: true }); - - this.log.debug(`EXIT doDeploy()`); - return resBody; - } finally { - ssh.dispose(); - temp.cleanup(); - } - } - - private async stopCordaNode(ssh: NodeSSH): Promise { - const fnTag = `${this.className}#stopCordaNode()`; - Checks.truthy(ssh.isConnected, `${fnTag} ssh.isConnected`); - const cmd = this.options.cordaStopCmd || "sudo systemctl stop corda"; - try { - const response = await ssh.execCommand(cmd); - this.log.debug(`${fnTag} stopped Corda node OK `, response); - } catch (ex) { - this.log.error(`${fnTag} stopping of Corda node failed`, ex); - } - } - - private async startCordaNode(ssh: NodeSSH): Promise { - const fnTag = `${this.className}#startCordaNode()`; - Checks.truthy(ssh.isConnected, `${fnTag} ssh.isConnected`); - const cmd = this.options.cordaStartCmd || "sudo systemctl start corda"; - try { - const response = await ssh.execCommand(cmd); - this.log.debug(`${fnTag} started Corda node OK `, response); - } catch (ex) { - this.log.error(`${fnTag} starting of Corda node failed`, ex); - } + const apiConfig = new Configuration({ basePath: this.apiUrl }); + const apiClient = new DefaultApi(apiConfig); + const res = await apiClient.deployContractJarsV1(req); + return res.data; } } diff --git a/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/web-services/diagnose-node-endpoint-v1.ts b/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/web-services/diagnose-node-endpoint-v1.ts new file mode 100644 index 0000000000..d46c6c4c9b --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/web-services/diagnose-node-endpoint-v1.ts @@ -0,0 +1,114 @@ +import { Express, Request, Response } from "express"; + +import { + Logger, + LoggerProvider, + LogLevelDesc, + Checks, + IAsyncProvider, +} from "@hyperledger/cactus-common"; + +import { + IWebServiceEndpoint, + IExpressRequestHandler, + IEndpointAuthzOptions, + Configuration, +} from "@hyperledger/cactus-core-api"; + +import OAS from "../../json/openapi.json"; + +import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; +import { + DefaultApi, + DiagnoseNodeV1Request, + DiagnoseNodeV1Response, +} from "../generated/openapi/typescript-axios"; + +export interface IDiagnoseNodeEndpointV1Options { + logLevel?: LogLevelDesc; + apiUrl?: string; +} + +export class DiagnoseNodeEndpointV1 implements IWebServiceEndpoint { + private readonly log: Logger; + private readonly apiUrl?: string; + + constructor(public readonly opts: IDiagnoseNodeEndpointV1Options) { + const fnTag = "NetworkMapEndpointV1#constructor()"; + + Checks.truthy(opts, `${fnTag} options`); + + this.log = LoggerProvider.getOrCreate({ + label: "diagnose-node-endpoint-v1", + level: opts.logLevel || "INFO", + }); + + this.apiUrl = opts.apiUrl; + } + + getAuthorizationOptionsProvider(): IAsyncProvider { + // TODO: make this an injectable dependency in the constructor + return { + get: async () => ({ + isProtected: true, + requiredRoles: [], + }), + }; + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public get oasPath(): typeof OAS.paths["/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-corda/diagnose-node"] { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-corda/diagnose-node" + ]; + } + + public getPath(): string { + return this.oasPath.post["x-hyperledger-cactus"].http.path; + } + + public getVerbLowerCase(): string { + return this.oasPath.post["x-hyperledger-cactus"].http.verbLowerCase; + } + + public getOperationId(): string { + return this.oasPath.post.operationId; + } + + public async registerExpress( + expressApp: Express, + ): Promise { + await registerWebServiceEndpoint(expressApp, this); + return this; + } + + async handleRequest(req: Request, res: Response): Promise { + const fnTag = "DiagnoseNodeEndpointV1#handleRequest()"; + const verbUpper = this.getVerbLowerCase().toUpperCase(); + this.log.debug(`${verbUpper} ${this.getPath()}`); + + try { + if (this.apiUrl === undefined) throw "apiUrl option is necessary"; + const resBody = await this.callInternalContainer(req.body); + res.status(200); + res.send(resBody); + } catch (ex) { + this.log.error(`${fnTag} failed to serve request`, ex); + res.status(500); + res.statusMessage = ex.message; + res.json({ error: ex.stack }); + } + } + + async callInternalContainer( + req: DiagnoseNodeV1Request, + ): Promise { + const apiConfig = new Configuration({ basePath: this.apiUrl }); + const apiClient = new DefaultApi(apiConfig); + const res = await apiClient.diagnoseNodeV1(req); + return res.data; + } +} diff --git a/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/web-services/invoke-contract-endpoint-v1.ts b/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/web-services/invoke-contract-endpoint-v1.ts index 24c5b3f77c..419c30b28f 100644 --- a/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/web-services/invoke-contract-endpoint-v1.ts +++ b/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/web-services/invoke-contract-endpoint-v1.ts @@ -12,31 +12,38 @@ import { IWebServiceEndpoint, IExpressRequestHandler, IEndpointAuthzOptions, + Configuration, } from "@hyperledger/cactus-core-api"; import OAS from "../../json/openapi.json"; import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; -import { PluginLedgerConnectorCorda } from "../plugin-ledger-connector-corda"; +import { + DefaultApi, + InvokeContractV1Request, + InvokeContractV1Response, +} from "../generated/openapi/typescript-axios"; export interface IInvokeContractEndpointV1Options { logLevel?: LogLevelDesc; - connector: PluginLedgerConnectorCorda; + apiUrl?: string; } export class InvokeContractEndpointV1 implements IWebServiceEndpoint { private readonly log: Logger; + private readonly apiUrl?: string; constructor(public readonly opts: IInvokeContractEndpointV1Options) { const fnTag = "InvokeContractEndpointV1#constructor()"; Checks.truthy(opts, `${fnTag} options`); - Checks.truthy(opts.connector, `${fnTag} options.connector`); this.log = LoggerProvider.getOrCreate({ label: "invoke-contract-endpoint-v1", level: opts.logLevel || "INFO", }); + + this.apiUrl = opts.apiUrl; } getAuthorizationOptionsProvider(): IAsyncProvider { @@ -84,8 +91,9 @@ export class InvokeContractEndpointV1 implements IWebServiceEndpoint { this.log.debug(`${verbUpper} ${this.getPath()}`); try { - const resBody = "NOT_IMPLEMENTED"; - res.status(501); + if (this.apiUrl === undefined) throw "apiUrl option is necessary"; + const resBody = await this.callInternalContainer(req.body); + res.status(200); res.send(resBody); } catch (ex) { this.log.error(`${fnTag} failed to serve request`, ex); @@ -94,4 +102,13 @@ export class InvokeContractEndpointV1 implements IWebServiceEndpoint { res.json({ error: ex.stack }); } } + + async callInternalContainer( + req: InvokeContractV1Request, + ): Promise { + const apiConfig = new Configuration({ basePath: this.apiUrl }); + const apiClient = new DefaultApi(apiConfig); + const res = await apiClient.invokeContractV1(req); + return res.data; + } } diff --git a/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/web-services/list-flows-endpoint-v1.ts b/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/web-services/list-flows-endpoint-v1.ts new file mode 100644 index 0000000000..b3173766ed --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/web-services/list-flows-endpoint-v1.ts @@ -0,0 +1,119 @@ +import { Express, Request, Response } from "express"; + +import { + Logger, + LoggerProvider, + LogLevelDesc, + Checks, + IAsyncProvider, +} from "@hyperledger/cactus-common"; + +import { + IWebServiceEndpoint, + IExpressRequestHandler, + IEndpointAuthzOptions, + Configuration, +} from "@hyperledger/cactus-core-api"; + +import OAS from "../../json/openapi.json"; + +import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; +import { + DefaultApi, + ListFlowsV1Request, + ListFlowsV1Response, +} from "../generated/openapi/typescript-axios"; + +export interface IListFlowsEndpointV1Options { + logLevel?: LogLevelDesc; + apiUrl?: string; +} + +export class ListFlowsEndpointV1 implements IWebServiceEndpoint { + public static readonly CLASS_NAME = "ListFlowsEndpointV1"; + + private readonly log: Logger; + private readonly apiUrl?: string; + + public get className(): string { + return ListFlowsEndpointV1.CLASS_NAME; + } + + constructor(public readonly options: IListFlowsEndpointV1Options) { + const fnTag = `${this.className}#constructor()`; + + Checks.truthy(options, `${fnTag} options`); + + this.log = LoggerProvider.getOrCreate({ + label: "list-flows-endpoint-v1", + level: options.logLevel || "INFO", + }); + + this.apiUrl = options.apiUrl; + } + + getAuthorizationOptionsProvider(): IAsyncProvider { + // TODO: make this an injectable dependency in the constructor + return { + get: async () => ({ + isProtected: true, + requiredRoles: [], + }), + }; + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public get oasPath(): typeof OAS.paths["/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-corda/list-flows"] { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-corda/list-flows" + ]; + } + + public getPath(): string { + return this.oasPath.post["x-hyperledger-cactus"].http.path; + } + + public getVerbLowerCase(): string { + return this.oasPath.post["x-hyperledger-cactus"].http.verbLowerCase; + } + + public getOperationId(): string { + return this.oasPath.post.operationId; + } + + public async registerExpress( + expressApp: Express, + ): Promise { + await registerWebServiceEndpoint(expressApp, this); + return this; + } + + async handleRequest(req: Request, res: Response): Promise { + const fnTag = "ListFlowsEndpointV1#handleRequest()"; + const verbUpper = this.getVerbLowerCase().toUpperCase(); + this.log.debug(`${verbUpper} ${this.getPath()}`); + try { + if (this.apiUrl === undefined) throw "apiUrl option is necessary"; + const resBody = await this.callInternalContainer(req.body); + res.status(200); + res.send(resBody); + } catch (ex) { + this.log.error(`${fnTag} failed to serve request`, ex); + res.status(500); + res.statusMessage = ex.message; + res.json({ error: ex.stack }); + } + } + + async callInternalContainer( + req: ListFlowsV1Request, + ): Promise { + const apiConfig = new Configuration({ basePath: this.apiUrl }); + const apiClient = new DefaultApi(apiConfig); + const res = await apiClient.listFlowsV1(req); + return res.data; + } +} diff --git a/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/web-services/network-map-endpoint-v1.ts b/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/web-services/network-map-endpoint-v1.ts new file mode 100644 index 0000000000..6ee35bb0fe --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-corda/src/main/typescript/web-services/network-map-endpoint-v1.ts @@ -0,0 +1,108 @@ +import { Express, Request, Response } from "express"; + +import { + Logger, + LoggerProvider, + LogLevelDesc, + Checks, + IAsyncProvider, +} from "@hyperledger/cactus-common"; + +import { + IWebServiceEndpoint, + IExpressRequestHandler, + IEndpointAuthzOptions, + Configuration, +} from "@hyperledger/cactus-core-api"; + +import OAS from "../../json/openapi.json"; + +import { registerWebServiceEndpoint } from "@hyperledger/cactus-core"; +import { DefaultApi, NodeInfo } from "../generated/openapi/typescript-axios"; + +export interface INetworkMapEndpointV1Options { + logLevel?: LogLevelDesc; + apiUrl?: string; +} + +export class NetworkMapEndpointV1 implements IWebServiceEndpoint { + private readonly log: Logger; + private readonly apiUrl?: string; + + constructor(public readonly opts: INetworkMapEndpointV1Options) { + const fnTag = "NetworkMapEndpointV1#constructor()"; + + Checks.truthy(opts, `${fnTag} options`); + + this.log = LoggerProvider.getOrCreate({ + label: "network-map-endpoint-v1", + level: opts.logLevel || "INFO", + }); + + this.apiUrl = opts.apiUrl; + } + + getAuthorizationOptionsProvider(): IAsyncProvider { + // TODO: make this an injectable dependency in the constructor + return { + get: async () => ({ + isProtected: true, + requiredRoles: [], + }), + }; + } + + public getExpressRequestHandler(): IExpressRequestHandler { + return this.handleRequest.bind(this); + } + + public get oasPath(): typeof OAS.paths["/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-corda/network-map"] { + return OAS.paths[ + "/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-corda/network-map" + ]; + } + + public getPath(): string { + return this.oasPath.post["x-hyperledger-cactus"].http.path; + } + + public getVerbLowerCase(): string { + return this.oasPath.post["x-hyperledger-cactus"].http.verbLowerCase; + } + + public getOperationId(): string { + return this.oasPath.post.operationId; + } + + public async registerExpress( + expressApp: Express, + ): Promise { + await registerWebServiceEndpoint(expressApp, this); + return this; + } + + async handleRequest(req: Request, res: Response): Promise { + const fnTag = "NetworkMapEndpointV1#handleRequest()"; + const verbUpper = this.getVerbLowerCase().toUpperCase(); + this.log.debug(`${verbUpper} ${this.getPath()}`); + + try { + if (this.apiUrl === undefined) throw "apiUrl option is necessary"; + const resBody = await this.callInternalContainer(); + res.status(200); + res.send(resBody); + } catch (ex) { + this.log.error(`${fnTag} failed to serve request`, ex); + res.status(500); + res.statusMessage = ex.message; + res.json({ error: ex.stack }); + } + } + + async callInternalContainer(): Promise { + const apiConfig = new Configuration({ basePath: this.apiUrl }); + const apiClient = new DefaultApi(apiConfig); + const res = await apiClient.networkMapV1(); + return res.data; + } +} diff --git a/packages/cactus-plugin-ledger-connector-corda/src/test/typescript/integration/deploy-cordapp-jars-to-nodes-v4.8-express.test.ts b/packages/cactus-plugin-ledger-connector-corda/src/test/typescript/integration/deploy-cordapp-jars-to-nodes-v4.8-express.test.ts new file mode 100644 index 0000000000..f1e8d5ecbb --- /dev/null +++ b/packages/cactus-plugin-ledger-connector-corda/src/test/typescript/integration/deploy-cordapp-jars-to-nodes-v4.8-express.test.ts @@ -0,0 +1,397 @@ +import test, { Test } from "tape-promise/tape"; +import { v4 as internalIpV4 } from "internal-ip"; +import { v4 as uuidv4 } from "uuid"; +import http from "http"; +import bodyParser from "body-parser"; +import express from "express"; +import { AddressInfo } from "net"; + +import { Containers, CordaTestLedger } from "@hyperledger/cactus-test-tooling"; +import { + LogLevelDesc, + IListenOptions, + Servers, +} from "@hyperledger/cactus-common"; +import { + SampleCordappEnum, + CordaConnectorContainer, +} from "@hyperledger/cactus-test-tooling"; + +import { + CordappDeploymentConfig, + DefaultApi as CordaApi, + DeployContractJarsV1Request, + FlowInvocationType, + InvokeContractV1Request, + JvmTypeKind, +} from "../../../main/typescript/generated/openapi/typescript-axios/index"; +import { Configuration } from "@hyperledger/cactus-core-api"; + +import { + IPluginLedgerConnectorCordaOptions, + PluginLedgerConnectorCorda, +} from "../../../main/typescript/plugin-ledger-connector-corda"; +import { K_CACTUS_CORDA_TOTAL_TX_COUNT } from "../../../main/typescript/prometheus-exporter/metrics"; + +const logLevel: LogLevelDesc = "TRACE"; + +test("Tests are passing on the JVM side", async (t: Test) => { + test.onFailure(async () => { + await Containers.logDiagnostics({ logLevel }); + }); + + const ledger = new CordaTestLedger({ + imageName: "ghcr.io/hyperledger/cactus-corda-4-8-all-in-one-obligation", + imageVersion: "2021-08-31--feat-889", + logLevel, + }); + t.ok(ledger, "CordaTestLedger v4.8 instantaited OK"); + + test.onFinish(async () => { + await ledger.stop(); + await ledger.destroy(); + }); + const ledgerContainer = await ledger.start(); + t.ok( + ledgerContainer, + "CordaTestLedger v4.8 container truthy post-start() OK", + ); + + const corDappsDirPartyA = await ledger.getCorDappsDirPartyA(); + const corDappsDirPartyB = await ledger.getCorDappsDirPartyB(); + t.comment(`corDappsDirPartyA=${corDappsDirPartyA}`); + t.comment(`corDappsDirPartyB=${corDappsDirPartyB}`); + + await ledger.logDebugPorts(); + const partyARpcPort = await ledger.getRpcAPublicPort(); + const partyBRpcPort = await ledger.getRpcBPublicPort(); + + const sshConfig = await ledger.getSshConfig(); + + const jarFiles = await ledger.pullCordappJars( + SampleCordappEnum.BASIC_CORDAPP, + ); + t.comment(`Fetched ${jarFiles.length} cordapp jars OK`); + + const internalIpOrUndefined = await internalIpV4(); + t.ok(internalIpOrUndefined, "Determined LAN IPv4 address successfully OK"); + const internalIp = internalIpOrUndefined as string; + t.comment(`Internal IP (based on default gateway): ${internalIp}`); + + // TODO: parse the gradle build files to extract the credentials? + const partyARpcUsername = "user1"; + const partyARpcPassword = "password"; + const partyBRpcUsername = partyARpcUsername; + const partyBRpcPassword = partyARpcPassword; + const springAppConfig = { + logging: { + level: { + root: "INFO", + "net.corda": "INFO", + "org.hyperledger.cactus": "DEBUG", + }, + }, + cactus: { + corda: { + node: { host: internalIp }, + rpc: { + port: partyARpcPort, + username: partyARpcUsername, + password: partyARpcPassword, + }, + }, + }, + }; + const springApplicationJson = JSON.stringify(springAppConfig); + const envVarSpringAppJson = `SPRING_APPLICATION_JSON=${springApplicationJson}`; + t.comment(envVarSpringAppJson); + + const connector = new CordaConnectorContainer({ + logLevel, + imageName: "ghcr.io/hyperledger/cactus-connector-corda-server", + imageVersion: "2021-03-25-feat-622", + envVars: [envVarSpringAppJson], + }); + t.ok(CordaConnectorContainer, "CordaConnectorContainer instantiated OK"); + + test.onFinish(async () => { + try { + await connector.stop(); + } finally { + await connector.destroy(); + } + }); + + const connectorContainer = await connector.start(); + t.ok(connectorContainer, "CordaConnectorContainer started OK"); + + await connector.logDebugPorts(); + + const apiUrl = await connector.getApiLocalhostUrl(); + const pluginOptions: IPluginLedgerConnectorCordaOptions = { + instanceId: uuidv4(), + corDappsDir: corDappsDirPartyA, + sshConfigAdminShell: sshConfig, + apiUrl, + }; + + const plugin = new PluginLedgerConnectorCorda(pluginOptions); + + const expressApp = express(); + expressApp.use(bodyParser.json({ limit: "250mb" })); + const server = http.createServer(expressApp); + const listenOptions: IListenOptions = { + hostname: "0.0.0.0", + port: 0, + server, + }; + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; + test.onFinish(async () => await Servers.shutdown(server)); + const { address, port } = addressInfo; + const apiHost = `http://${address}:${port}`; + t.comment( + `Metrics URL: ${apiHost}/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-corda/get-prometheus-exporter-metrics`, + ); + + const apiConfig = new Configuration({ basePath: apiHost }); + const apiClient = new CordaApi(apiConfig); + + await plugin.getOrCreateWebServices(); + await plugin.registerWebServices(expressApp); + + // const apiUrl = await connector.getApiLocalhostUrl(); + + // const config = new Configuration({ basePath: apiUrl }); + // const apiClient = new CordaApi(config); + + const flowsRes1 = await apiClient.listFlowsV1(); + t.ok(flowsRes1.status === 200, "flowsRes1.status === 200 OK"); + t.ok(flowsRes1.data, "flowsRes1.data truthy OK"); + t.ok(flowsRes1.data.flowNames, "flowsRes1.data.flowNames truthy OK"); + t.comment(`apiClient.listFlowsV1() => ${JSON.stringify(flowsRes1.data)}`); + const flowNamesPreDeploy = flowsRes1.data.flowNames; + + const hostKeyEntry = "not-used-right-now-so-this-does-not-matter... ;-("; + + const cdcA: CordappDeploymentConfig = { + cordappDir: corDappsDirPartyA, + cordaNodeStartCmd: "supervisorctl start corda-a", + cordaJarPath: + "/samples-kotlin/Advanced/obligation-cordapp/build/nodes/ParticipantA/corda.jar", + nodeBaseDirPath: + "/samples-kotlin/Advanced/obligation-cordapp/build/nodes/ParticipantA/", + rpcCredentials: { + hostname: internalIp, + port: partyARpcPort, + username: partyARpcUsername, + password: partyARpcPassword, + }, + sshCredentials: { + hostKeyEntry, + hostname: internalIp, + password: "root", + port: sshConfig.port as number, + username: sshConfig.username as string, + }, + }; + + const cdcB: CordappDeploymentConfig = { + cordappDir: corDappsDirPartyB, + cordaNodeStartCmd: "supervisorctl start corda-b", + cordaJarPath: + "/samples-kotlin/Advanced/obligation-cordapp/build/nodes/ParticipantB/corda.jar", + nodeBaseDirPath: + "/samples-kotlin/Advanced/obligation-cordapp/build/nodes/ParticipantB/", + rpcCredentials: { + hostname: internalIp, + port: partyBRpcPort, + username: partyBRpcUsername, + password: partyBRpcPassword, + }, + sshCredentials: { + hostKeyEntry, + hostname: internalIp, + password: "root", + port: sshConfig.port as number, + username: sshConfig.username as string, + }, + }; + + const cordappDeploymentConfigs: CordappDeploymentConfig[] = [cdcA, cdcB]; + const depReq: DeployContractJarsV1Request = { + jarFiles, + cordappDeploymentConfigs, + }; + const depRes = await apiClient.deployContractJarsV1(depReq); + t.ok(depRes, "Jar deployment response truthy OK"); + t.equal(depRes.status, 200, "Jar deployment status code === 200 OK"); + t.ok(depRes.data, "Jar deployment response body truthy OK"); + t.ok(depRes.data.deployedJarFiles, "Jar deployment body deployedJarFiles OK"); + t.equal( + depRes.data.deployedJarFiles.length, + jarFiles.length, + "Deployed jar file count equals count in request OK", + ); + + const flowsRes2 = await apiClient.listFlowsV1(); + t.ok(flowsRes2.status === 200, "flowsRes2.status === 200 OK"); + t.comment(`apiClient.listFlowsV1() => ${JSON.stringify(flowsRes2.data)}`); + t.ok(flowsRes2.data, "flowsRes2.data truthy OK"); + t.ok(flowsRes2.data.flowNames, "flowsRes2.data.flowNames truthy OK"); + const flowNamesPostDeploy = flowsRes2.data.flowNames; + t.notDeepLooseEqual( + flowNamesPostDeploy, + flowNamesPreDeploy, + "New flows detected post Cordapp Jar deployment OK", + ); + + // let's see if this makes a difference and if yes, then we know that the issue + // is a race condition for sure + // await new Promise((r) => setTimeout(r, 120000)); + t.comment("Fetching network map for Corda network..."); + const networkMapRes = await apiClient.networkMapV1(); + t.ok(networkMapRes, "networkMapRes truthy OK"); + t.ok(networkMapRes.status, "networkMapRes.status truthy OK"); + t.ok(networkMapRes.data, "networkMapRes.data truthy OK"); + t.true(Array.isArray(networkMapRes.data), "networkMapRes.data isArray OK"); + t.true(networkMapRes.data.length > 0, "networkMapRes.data not empty OK"); + + const partyB = networkMapRes.data.find((it) => + it.legalIdentities.some((it2) => it2.name.organisation === "ParticipantB"), + ); + const partyBPublicKey = partyB?.legalIdentities[0].owningKey; + + const req: InvokeContractV1Request = ({ + timeoutMs: 600000, + flowFullClassName: "net.corda.samples.example.flows.ExampleFlow$Initiator", + flowInvocationType: FlowInvocationType.FlowDynamic, + params: [ + { + jvmTypeKind: JvmTypeKind.Primitive, + jvmType: { + fqClassName: "java.lang.Integer", + }, + primitiveValue: 42, + }, + { + jvmTypeKind: JvmTypeKind.Reference, + jvmType: { + fqClassName: "net.corda.core.identity.Party", + }, + jvmCtorArgs: [ + { + jvmTypeKind: JvmTypeKind.Reference, + jvmType: { + fqClassName: "net.corda.core.identity.CordaX500Name", + }, + jvmCtorArgs: [ + { + jvmTypeKind: JvmTypeKind.Primitive, + jvmType: { + fqClassName: "java.lang.String", + }, + primitiveValue: "ParticipantB", + }, + { + jvmTypeKind: JvmTypeKind.Primitive, + jvmType: { + fqClassName: "java.lang.String", + }, + primitiveValue: "New York", + }, + { + jvmTypeKind: JvmTypeKind.Primitive, + jvmType: { + fqClassName: "java.lang.String", + }, + primitiveValue: "US", + }, + ], + }, + { + jvmTypeKind: JvmTypeKind.Reference, + jvmType: { + fqClassName: + "org.hyperledger.cactus.plugin.ledger.connector.corda.server.impl.PublicKeyImpl", + }, + jvmCtorArgs: [ + { + jvmTypeKind: JvmTypeKind.Primitive, + jvmType: { + fqClassName: "java.lang.String", + }, + primitiveValue: partyBPublicKey?.algorithm, + }, + { + jvmTypeKind: JvmTypeKind.Primitive, + jvmType: { + fqClassName: "java.lang.String", + }, + primitiveValue: partyBPublicKey?.format, + }, + { + jvmTypeKind: JvmTypeKind.Primitive, + jvmType: { + fqClassName: "java.lang.String", + }, + primitiveValue: partyBPublicKey?.encoded, + }, + ], + }, + ], + }, + ], + } as unknown) as InvokeContractV1Request; + + const res = await apiClient.invokeContractV1(req); + t.ok(res, "InvokeContractV1Request truthy OK"); + t.equal(res.status, 200, "InvokeContractV1Request status code === 200 OK"); + + { + plugin.transact(); + const promRes = await apiClient.getPrometheusMetricsV1(); + const promMetricsOutput = + "# HELP " + + K_CACTUS_CORDA_TOTAL_TX_COUNT + + " Total transactions executed\n" + + "# TYPE " + + K_CACTUS_CORDA_TOTAL_TX_COUNT + + " gauge\n" + + K_CACTUS_CORDA_TOTAL_TX_COUNT + + '{type="' + + K_CACTUS_CORDA_TOTAL_TX_COUNT + + '"} 1'; + t.ok(promRes); + t.ok(promRes.data); + t.equal(promRes.status, 200); + t.true( + promRes.data.includes(promMetricsOutput), + "Total Transaction Count of 1 recorded as expected. RESULT OK", + ); + + // Executing transaction to increment the Total transaction count metrics + plugin.transact(); + + const promRes1 = await apiClient.getPrometheusMetricsV1(); + const promMetricsOutput1 = + "# HELP " + + K_CACTUS_CORDA_TOTAL_TX_COUNT + + " Total transactions executed\n" + + "# TYPE " + + K_CACTUS_CORDA_TOTAL_TX_COUNT + + " gauge\n" + + K_CACTUS_CORDA_TOTAL_TX_COUNT + + '{type="' + + K_CACTUS_CORDA_TOTAL_TX_COUNT + + '"} 2'; + t.ok(promRes1); + t.ok(promRes1.data); + t.equal(promRes1.status, 200); + t.true( + promRes1.data.includes(promMetricsOutput1), + "Total Transaction Count of 2 recorded as expected. RESULT OK", + ); + } + + t.end(); +});