|
| 1 | +import test, { Test } from "tape-promise/tape"; |
| 2 | +import { v4 as internalIpV4 } from "internal-ip"; |
| 3 | +import { v4 as uuidv4 } from "uuid"; |
| 4 | +import http from "http"; |
| 5 | +import bodyParser from "body-parser"; |
| 6 | +import express from "express"; |
| 7 | +import { AddressInfo } from "net"; |
| 8 | + |
| 9 | +import { Containers, CordaTestLedger } from "@hyperledger/cactus-test-tooling"; |
| 10 | +import { |
| 11 | + LogLevelDesc, |
| 12 | + IListenOptions, |
| 13 | + Servers, |
| 14 | +} from "@hyperledger/cactus-common"; |
| 15 | +import { |
| 16 | + SampleCordappEnum, |
| 17 | + CordaConnectorContainer, |
| 18 | +} from "@hyperledger/cactus-test-tooling"; |
| 19 | + |
| 20 | +import { |
| 21 | + CordappDeploymentConfig, |
| 22 | + DefaultApi as CordaApi, |
| 23 | + DeployContractJarsV1Request, |
| 24 | + FlowInvocationType, |
| 25 | + InvokeContractV1Request, |
| 26 | + JvmTypeKind, |
| 27 | +} from "../../../main/typescript/generated/openapi/typescript-axios/index"; |
| 28 | +import { Configuration } from "@hyperledger/cactus-core-api"; |
| 29 | + |
| 30 | +import { |
| 31 | + IPluginLedgerConnectorCordaOptions, |
| 32 | + PluginLedgerConnectorCorda, |
| 33 | +} from "../../../main/typescript/plugin-ledger-connector-corda"; |
| 34 | +import { K_CACTUS_CORDA_TOTAL_TX_COUNT } from "../../../main/typescript/prometheus-exporter/metrics"; |
| 35 | + |
| 36 | +const logLevel: LogLevelDesc = "TRACE"; |
| 37 | + |
| 38 | +test("Tests are passing on the JVM side", async (t: Test) => { |
| 39 | + test.onFailure(async () => { |
| 40 | + await Containers.logDiagnostics({ logLevel }); |
| 41 | + }); |
| 42 | + |
| 43 | + const ledger = new CordaTestLedger({ |
| 44 | + imageName: "ghcr.io/hyperledger/cactus-corda-4-7-all-in-one-obligation", |
| 45 | + imageVersion: "2021-08-19--feat-888", |
| 46 | + logLevel, |
| 47 | + }); |
| 48 | + t.ok(ledger, "CordaTestLedger instantaited OK"); |
| 49 | + |
| 50 | + test.onFinish(async () => { |
| 51 | + await ledger.stop(); |
| 52 | + await ledger.destroy(); |
| 53 | + }); |
| 54 | + const ledgerContainer = await ledger.start(); |
| 55 | + t.ok(ledgerContainer, "CordaTestLedger container truthy post-start() OK"); |
| 56 | + |
| 57 | + const corDappsDirPartyA = await ledger.getCorDappsDirPartyA(); |
| 58 | + const corDappsDirPartyB = await ledger.getCorDappsDirPartyB(); |
| 59 | + t.comment(`corDappsDirPartyA=${corDappsDirPartyA}`); |
| 60 | + t.comment(`corDappsDirPartyB=${corDappsDirPartyB}`); |
| 61 | + |
| 62 | + await ledger.logDebugPorts(); |
| 63 | + const partyARpcPort = await ledger.getRpcAPublicPort(); |
| 64 | + const partyBRpcPort = await ledger.getRpcBPublicPort(); |
| 65 | + |
| 66 | + const jarFiles = await ledger.pullCordappJars( |
| 67 | + SampleCordappEnum.BASIC_CORDAPP, |
| 68 | + ); |
| 69 | + t.comment(`Fetched ${jarFiles.length} cordapp jars OK`); |
| 70 | + |
| 71 | + const internalIpOrUndefined = await internalIpV4(); |
| 72 | + t.ok(internalIpOrUndefined, "Determined LAN IPv4 address successfully OK"); |
| 73 | + const internalIp = internalIpOrUndefined as string; |
| 74 | + t.comment(`Internal IP (based on default gateway): ${internalIp}`); |
| 75 | + |
| 76 | + // TODO: parse the gradle build files to extract the credentials? |
| 77 | + const partyARpcUsername = "user1"; |
| 78 | + const partyARpcPassword = "password"; |
| 79 | + const partyBRpcUsername = partyARpcUsername; |
| 80 | + const partyBRpcPassword = partyARpcPassword; |
| 81 | + const springAppConfig = { |
| 82 | + logging: { |
| 83 | + level: { |
| 84 | + root: "INFO", |
| 85 | + "net.corda": "INFO", |
| 86 | + "org.hyperledger.cactus": "DEBUG", |
| 87 | + }, |
| 88 | + }, |
| 89 | + cactus: { |
| 90 | + corda: { |
| 91 | + node: { host: internalIp }, |
| 92 | + rpc: { |
| 93 | + port: partyARpcPort, |
| 94 | + username: partyARpcUsername, |
| 95 | + password: partyARpcPassword, |
| 96 | + }, |
| 97 | + }, |
| 98 | + }, |
| 99 | + }; |
| 100 | + const springApplicationJson = JSON.stringify(springAppConfig); |
| 101 | + const envVarSpringAppJson = `SPRING_APPLICATION_JSON=${springApplicationJson}`; |
| 102 | + t.comment(envVarSpringAppJson); |
| 103 | + |
| 104 | + const connector = new CordaConnectorContainer({ |
| 105 | + logLevel, |
| 106 | + imageName: "ghcr.io/hyperledger/cactus-connector-corda-server", |
| 107 | + imageVersion: "2021-03-25-feat-622", |
| 108 | + envVars: [envVarSpringAppJson], |
| 109 | + }); |
| 110 | + t.ok(CordaConnectorContainer, "CordaConnectorContainer instantiated OK"); |
| 111 | + |
| 112 | + test.onFinish(async () => { |
| 113 | + try { |
| 114 | + await connector.stop(); |
| 115 | + } finally { |
| 116 | + await connector.destroy(); |
| 117 | + } |
| 118 | + }); |
| 119 | + |
| 120 | + const connectorContainer = await connector.start(); |
| 121 | + t.ok(connectorContainer, "CordaConnectorContainer started OK"); |
| 122 | + |
| 123 | + await connector.logDebugPorts(); |
| 124 | + const apiUrl = await connector.getApiLocalhostUrl(); |
| 125 | + |
| 126 | + const config = new Configuration({ basePath: apiUrl }); |
| 127 | + const apiClient = new CordaApi(config); |
| 128 | + |
| 129 | + const flowsRes1 = await apiClient.listFlowsV1(); |
| 130 | + t.ok(flowsRes1.status === 200, "flowsRes1.status === 200 OK"); |
| 131 | + t.ok(flowsRes1.data, "flowsRes1.data truthy OK"); |
| 132 | + t.ok(flowsRes1.data.flowNames, "flowsRes1.data.flowNames truthy OK"); |
| 133 | + t.comment(`apiClient.listFlowsV1() => ${JSON.stringify(flowsRes1.data)}`); |
| 134 | + const flowNamesPreDeploy = flowsRes1.data.flowNames; |
| 135 | + |
| 136 | + const sshConfig = await ledger.getSshConfig(); |
| 137 | + const hostKeyEntry = "not-used-right-now-so-this-does-not-matter... ;-("; |
| 138 | + |
| 139 | + const cdcA: CordappDeploymentConfig = { |
| 140 | + cordappDir: corDappsDirPartyA, |
| 141 | + cordaNodeStartCmd: "supervisorctl start corda-a", |
| 142 | + cordaJarPath: |
| 143 | + "/samples-kotlin/Advanced/obligation-cordapp/build/nodes/ParticipantA/corda.jar", |
| 144 | + nodeBaseDirPath: |
| 145 | + "/samples-kotlin/Advanced/obligation-cordapp/build/nodes/ParticipantA/", |
| 146 | + rpcCredentials: { |
| 147 | + hostname: internalIp, |
| 148 | + port: partyARpcPort, |
| 149 | + username: partyARpcUsername, |
| 150 | + password: partyARpcPassword, |
| 151 | + }, |
| 152 | + sshCredentials: { |
| 153 | + hostKeyEntry, |
| 154 | + hostname: internalIp, |
| 155 | + password: "root", |
| 156 | + port: sshConfig.port as number, |
| 157 | + username: sshConfig.username as string, |
| 158 | + }, |
| 159 | + }; |
| 160 | + |
| 161 | + const cdcB: CordappDeploymentConfig = { |
| 162 | + cordappDir: corDappsDirPartyB, |
| 163 | + cordaNodeStartCmd: "supervisorctl start corda-b", |
| 164 | + cordaJarPath: |
| 165 | + "/samples-kotlin/Advanced/obligation-cordapp/build/nodes/ParticipantB/corda.jar", |
| 166 | + nodeBaseDirPath: |
| 167 | + "/samples-kotlin/Advanced/obligation-cordapp/build/nodes/ParticipantB/", |
| 168 | + rpcCredentials: { |
| 169 | + hostname: internalIp, |
| 170 | + port: partyBRpcPort, |
| 171 | + username: partyBRpcUsername, |
| 172 | + password: partyBRpcPassword, |
| 173 | + }, |
| 174 | + sshCredentials: { |
| 175 | + hostKeyEntry, |
| 176 | + hostname: internalIp, |
| 177 | + password: "root", |
| 178 | + port: sshConfig.port as number, |
| 179 | + username: sshConfig.username as string, |
| 180 | + }, |
| 181 | + }; |
| 182 | + |
| 183 | + const cordappDeploymentConfigs: CordappDeploymentConfig[] = [cdcA, cdcB]; |
| 184 | + const depReq: DeployContractJarsV1Request = { |
| 185 | + jarFiles, |
| 186 | + cordappDeploymentConfigs, |
| 187 | + }; |
| 188 | + const depRes = await apiClient.deployContractJarsV1(depReq); |
| 189 | + t.ok(depRes, "Jar deployment response truthy OK"); |
| 190 | + t.equal(depRes.status, 200, "Jar deployment status code === 200 OK"); |
| 191 | + t.ok(depRes.data, "Jar deployment response body truthy OK"); |
| 192 | + t.ok(depRes.data.deployedJarFiles, "Jar deployment body deployedJarFiles OK"); |
| 193 | + t.equal( |
| 194 | + depRes.data.deployedJarFiles.length, |
| 195 | + jarFiles.length, |
| 196 | + "Deployed jar file count equals count in request OK", |
| 197 | + ); |
| 198 | + |
| 199 | + const flowsRes2 = await apiClient.listFlowsV1(); |
| 200 | + t.ok(flowsRes2.status === 200, "flowsRes2.status === 200 OK"); |
| 201 | + t.comment(`apiClient.listFlowsV1() => ${JSON.stringify(flowsRes2.data)}`); |
| 202 | + t.ok(flowsRes2.data, "flowsRes2.data truthy OK"); |
| 203 | + t.ok(flowsRes2.data.flowNames, "flowsRes2.data.flowNames truthy OK"); |
| 204 | + const flowNamesPostDeploy = flowsRes2.data.flowNames; |
| 205 | + t.notDeepLooseEqual( |
| 206 | + flowNamesPostDeploy, |
| 207 | + flowNamesPreDeploy, |
| 208 | + "New flows detected post Cordapp Jar deployment OK", |
| 209 | + ); |
| 210 | + |
| 211 | + // let's see if this makes a difference and if yes, then we know that the issue |
| 212 | + // is a race condition for sure |
| 213 | + // await new Promise((r) => setTimeout(r, 120000)); |
| 214 | + t.comment("Fetching network map for Corda network..."); |
| 215 | + const networkMapRes = await apiClient.networkMapV1(); |
| 216 | + t.ok(networkMapRes, "networkMapRes truthy OK"); |
| 217 | + t.ok(networkMapRes.status, "networkMapRes.status truthy OK"); |
| 218 | + t.ok(networkMapRes.data, "networkMapRes.data truthy OK"); |
| 219 | + t.true(Array.isArray(networkMapRes.data), "networkMapRes.data isArray OK"); |
| 220 | + t.true(networkMapRes.data.length > 0, "networkMapRes.data not empty OK"); |
| 221 | + |
| 222 | + const partyB = networkMapRes.data.find((it) => |
| 223 | + it.legalIdentities.some((it2) => it2.name.organisation === "ParticipantB"), |
| 224 | + ); |
| 225 | + const partyBPublicKey = partyB?.legalIdentities[0].owningKey; |
| 226 | + |
| 227 | + const req: InvokeContractV1Request = ({ |
| 228 | + timeoutMs: 60000, |
| 229 | + flowFullClassName: "net.corda.samples.example.flows.ExampleFlow$Initiator", |
| 230 | + flowInvocationType: FlowInvocationType.FlowDynamic, |
| 231 | + params: [ |
| 232 | + { |
| 233 | + jvmTypeKind: JvmTypeKind.Primitive, |
| 234 | + jvmType: { |
| 235 | + fqClassName: "java.lang.Integer", |
| 236 | + }, |
| 237 | + primitiveValue: 42, |
| 238 | + }, |
| 239 | + { |
| 240 | + jvmTypeKind: JvmTypeKind.Reference, |
| 241 | + jvmType: { |
| 242 | + fqClassName: "net.corda.core.identity.Party", |
| 243 | + }, |
| 244 | + jvmCtorArgs: [ |
| 245 | + { |
| 246 | + jvmTypeKind: JvmTypeKind.Reference, |
| 247 | + jvmType: { |
| 248 | + fqClassName: "net.corda.core.identity.CordaX500Name", |
| 249 | + }, |
| 250 | + jvmCtorArgs: [ |
| 251 | + { |
| 252 | + jvmTypeKind: JvmTypeKind.Primitive, |
| 253 | + jvmType: { |
| 254 | + fqClassName: "java.lang.String", |
| 255 | + }, |
| 256 | + primitiveValue: "ParticipantB", |
| 257 | + }, |
| 258 | + { |
| 259 | + jvmTypeKind: JvmTypeKind.Primitive, |
| 260 | + jvmType: { |
| 261 | + fqClassName: "java.lang.String", |
| 262 | + }, |
| 263 | + primitiveValue: "New York", |
| 264 | + }, |
| 265 | + { |
| 266 | + jvmTypeKind: JvmTypeKind.Primitive, |
| 267 | + jvmType: { |
| 268 | + fqClassName: "java.lang.String", |
| 269 | + }, |
| 270 | + primitiveValue: "US", |
| 271 | + }, |
| 272 | + ], |
| 273 | + }, |
| 274 | + { |
| 275 | + jvmTypeKind: JvmTypeKind.Reference, |
| 276 | + jvmType: { |
| 277 | + fqClassName: |
| 278 | + "org.hyperledger.cactus.plugin.ledger.connector.corda.server.impl.PublicKeyImpl", |
| 279 | + }, |
| 280 | + jvmCtorArgs: [ |
| 281 | + { |
| 282 | + jvmTypeKind: JvmTypeKind.Primitive, |
| 283 | + jvmType: { |
| 284 | + fqClassName: "java.lang.String", |
| 285 | + }, |
| 286 | + primitiveValue: partyBPublicKey?.algorithm, |
| 287 | + }, |
| 288 | + { |
| 289 | + jvmTypeKind: JvmTypeKind.Primitive, |
| 290 | + jvmType: { |
| 291 | + fqClassName: "java.lang.String", |
| 292 | + }, |
| 293 | + primitiveValue: partyBPublicKey?.format, |
| 294 | + }, |
| 295 | + { |
| 296 | + jvmTypeKind: JvmTypeKind.Primitive, |
| 297 | + jvmType: { |
| 298 | + fqClassName: "java.lang.String", |
| 299 | + }, |
| 300 | + primitiveValue: partyBPublicKey?.encoded, |
| 301 | + }, |
| 302 | + ], |
| 303 | + }, |
| 304 | + ], |
| 305 | + }, |
| 306 | + ], |
| 307 | + } as unknown) as InvokeContractV1Request; |
| 308 | + |
| 309 | + const res = await apiClient.invokeContractV1(req); |
| 310 | + t.ok(res, "InvokeContractV1Request truthy OK"); |
| 311 | + t.equal(res.status, 200, "InvokeContractV1Request status code === 200 OK"); |
| 312 | + |
| 313 | + const pluginOptions: IPluginLedgerConnectorCordaOptions = { |
| 314 | + instanceId: uuidv4(), |
| 315 | + corDappsDir: corDappsDirPartyA, |
| 316 | + sshConfigAdminShell: sshConfig, |
| 317 | + }; |
| 318 | + |
| 319 | + const plugin = new PluginLedgerConnectorCorda(pluginOptions); |
| 320 | + |
| 321 | + const expressApp = express(); |
| 322 | + expressApp.use(bodyParser.json({ limit: "250mb" })); |
| 323 | + const server = http.createServer(expressApp); |
| 324 | + const listenOptions: IListenOptions = { |
| 325 | + hostname: "0.0.0.0", |
| 326 | + port: 0, |
| 327 | + server, |
| 328 | + }; |
| 329 | + const addressInfo = (await Servers.listen(listenOptions)) as AddressInfo; |
| 330 | + test.onFinish(async () => await Servers.shutdown(server)); |
| 331 | + const { address, port } = addressInfo; |
| 332 | + const apiHost = `http://${address}:${port}`; |
| 333 | + t.comment( |
| 334 | + `Metrics URL: ${apiHost}/api/v1/plugins/@hyperledger/cactus-plugin-ledger-connector-corda/get-prometheus-exporter-metrics`, |
| 335 | + ); |
| 336 | + |
| 337 | + const apiConfig = new Configuration({ basePath: apiHost }); |
| 338 | + const apiClient1 = new CordaApi(apiConfig); |
| 339 | + |
| 340 | + await plugin.getOrCreateWebServices(); |
| 341 | + await plugin.registerWebServices(expressApp); |
| 342 | + |
| 343 | + { |
| 344 | + plugin.transact(); |
| 345 | + const promRes = await apiClient1.getPrometheusMetricsV1(); |
| 346 | + const promMetricsOutput = |
| 347 | + "# HELP " + |
| 348 | + K_CACTUS_CORDA_TOTAL_TX_COUNT + |
| 349 | + " Total transactions executed\n" + |
| 350 | + "# TYPE " + |
| 351 | + K_CACTUS_CORDA_TOTAL_TX_COUNT + |
| 352 | + " gauge\n" + |
| 353 | + K_CACTUS_CORDA_TOTAL_TX_COUNT + |
| 354 | + '{type="' + |
| 355 | + K_CACTUS_CORDA_TOTAL_TX_COUNT + |
| 356 | + '"} 1'; |
| 357 | + t.ok(promRes); |
| 358 | + t.ok(promRes.data); |
| 359 | + t.equal(promRes.status, 200); |
| 360 | + t.true( |
| 361 | + promRes.data.includes(promMetricsOutput), |
| 362 | + "Total Transaction Count of 1 recorded as expected. RESULT OK", |
| 363 | + ); |
| 364 | + |
| 365 | + // Executing transaction to increment the Total transaction count metrics |
| 366 | + plugin.transact(); |
| 367 | + |
| 368 | + const promRes1 = await apiClient1.getPrometheusMetricsV1(); |
| 369 | + const promMetricsOutput1 = |
| 370 | + "# HELP " + |
| 371 | + K_CACTUS_CORDA_TOTAL_TX_COUNT + |
| 372 | + " Total transactions executed\n" + |
| 373 | + "# TYPE " + |
| 374 | + K_CACTUS_CORDA_TOTAL_TX_COUNT + |
| 375 | + " gauge\n" + |
| 376 | + K_CACTUS_CORDA_TOTAL_TX_COUNT + |
| 377 | + '{type="' + |
| 378 | + K_CACTUS_CORDA_TOTAL_TX_COUNT + |
| 379 | + '"} 2'; |
| 380 | + t.ok(promRes1); |
| 381 | + t.ok(promRes1.data); |
| 382 | + t.equal(promRes1.status, 200); |
| 383 | + t.true( |
| 384 | + promRes1.data.includes(promMetricsOutput1), |
| 385 | + "Total Transaction Count of 2 recorded as expected. RESULT OK", |
| 386 | + ); |
| 387 | + } |
| 388 | + |
| 389 | + t.end(); |
| 390 | +}); |
0 commit comments