commit a15efd43a121c18e15915ed5b0974a6eb7c452c3 Author: Chris Barratt Date: Wed Oct 12 23:04:51 2022 +0100 Initial commit of the java template - includes only concverted code diff --git a/.run/runConfigurations/DebugCorDapp.run.xml b/.run/runConfigurations/DebugCorDapp.run.xml new file mode 100644 index 0000000..1d8da82 --- /dev/null +++ b/.run/runConfigurations/DebugCorDapp.run.xml @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..defe174 --- /dev/null +++ b/build.gradle @@ -0,0 +1,142 @@ +import static org.gradle.api.JavaVersion.VERSION_11 + +plugins { + id 'org.jetbrains.kotlin.jvm' + + // Include the cordapp-cpb plugin. This automatically includes the cordapp-cpk plugin as well. + // These extend existing build environment so that CPB and CPK files can be built. + // This includes a CorDapp DSL that allows the developer to supply metadata for the CorDapp + // required by Corda. + id 'net.corda.plugins.cordapp-cpb2' + id 'net.corda.cordapp.cordapp-configuration' + + id 'org.jetbrains.kotlin.plugin.jpa' + + id 'java' + id 'maven-publish' + + id 'csde' +} + +group 'com.r3.hellocorda' +version '1.0-SNAPSHOT' + +def javaVersion = VERSION_11 + +// The CordApp section. +// This is part of the DSL provided by the corda plugins to define metadata for our CorDapp. +// Each component of the CorDapp would get its own CorDapp section in the build.gradle file for the component’s +// subproject. +// This is required by the corda plugins to build the CorDapp. +cordapp { + // "targetPlatformVersion" and "minimumPlatformVersion" are intended to specify the preferred + // and earliest versions of the Corda platform that the CorDapp will run on respectively. + // Enforced versioning has not implemented yet so we need to pass in a dummy value for now. + // The platform version will correspond to and be roughly equivalent to the Corda API version. + targetPlatformVersion platformVersion.toInteger() + minimumPlatformVersion platformVersion.toInteger() + + // The cordapp section contains either a workflow or contract subsection depending on the type of component. + // Declares the type and metadata of the CPK (this CPB has one CPK). + workflow { + name "ModuleNameHere" + versionId 1 + vendor "VendorNameHere" + } +} + +// Declare the set of Kotlin compiler options we need to build a CorDapp. +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + kotlinOptions { + allWarningsAsErrors = false + + // Specify the version of Kotlin that we are that we will be developing. + languageVersion = '1.7' + // Specify the Kotlin libraries that code is compatible with + apiVersion = '1.7' + // Note that we Need to use a version of Kotlin that will be compatible with the Corda API. + // Currently that is developed in Kotlin 1.7 so picking the same version ensures compatibility with that. + + // Specify the version of Java to target. + jvmTarget = javaVersion + + // Needed for reflection to work correctly. + javaParameters = true + + // -Xjvm-default determines how Kotlin supports default methods. + // JetBrains currently recommends developers use -Xjvm-default=all + // https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-jvm-default/ + freeCompilerArgs += [ + "-Xjvm-default=all" + ] + } +} + +repositories { + // All dependencies are held in Maven Central + mavenCentral() +} + +// Declare dependencies for the modules we will use. +// A cordaProvided declaration is required for anything that we use that the Corda API provides. +// This is required to allow us to build CorDapp modules as OSGi bundles that CPI and CPB files are built on. +dependencies { + // We need a version of kotlin-stdlib-jdk8 built as an OSGi bundle, this is "kotlin-stdlib-jdk8-osgi". + // R3 builds kotlin-stdlib-jdk8-osgi from Kotlin's kotlin-stdlib-jdk8. + // NB: + // Kotlin's kotlin-osgi-bundle does not provide all of the Kotlin API that is required, + // There is no kotlin-stdlib-jdk11, but one is not needed even though we are targetting Java 11. + cordaProvided 'net.corda.kotlin:kotlin-stdlib-jdk8-osgi' + + // Declare a "platform" so that we use the correct set of dependency versions for the version of the + // Corda API specified. + cordaProvided platform("net.corda:corda-api:$cordaApiVersion") + + // If using transistive dependencies this will provide most of Corda-API: + // cordaProvided 'net.corda:corda-application' + + // Alternatively we can explicitly specify all our Corda-API dependencies: + cordaProvided 'net.corda:corda-base' + cordaProvided 'net.corda:corda-application' + cordaProvided 'net.corda:corda-crypto' + cordaProvided 'net.corda:corda-membership' + // cordaProvided 'net.corda:corda-persistence' + cordaProvided 'net.corda:corda-serialization' + + // Not yet fully implemented: + // cordaProvided 'net.corda:corda-ledger' + + // The CorDapp uses the slf4j logging framework. Corda-API provides this so we need a 'cordaProvided' declaration. + cordaProvided 'org.slf4j:slf4j-api' + + // Dependencies Required By Test Tooling + testImplementation "net.corda:corda-simulator-api:$combinedWorkerVersion" + testRuntimeOnly "net.corda:corda-simulator-runtime:$combinedWorkerVersion" + + // 3rd party libraries + // Required + testImplementation "org.slf4j:slf4j-simple:2.0.0" + testImplementation "org.junit.jupiter:junit-jupiter:$junitVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + // Optional but used by exmaple tests. + testImplementation "org.mockito:mockito-core:$mockitoVersion" + testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion" + testImplementation "org.hamcrest:hamcrest-library:$hamcrestVersion" + +} + + +test { + useJUnitPlatform() +} + +publishing { + publications { + maven(MavenPublication) { + artifactId "corda-CSDE-kotlin-sample" + groupId project.group + artifact jar + } + } +} diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 0000000..750b4c5 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,21 @@ + +plugins { + id 'groovy-gradle-plugin' + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' + + implementation "com.konghq:unirest-java:$unirestVersion" + implementation "com.konghq:unirest-objectmapper-jackson:$unirestVersion" + implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" + implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jacksonVersion" + + implementation "net.corda:corda-base:$cordaApiVersion" +} diff --git a/buildSrc/gradle.properties b/buildSrc/gradle.properties new file mode 100644 index 0000000..e892a48 --- /dev/null +++ b/buildSrc/gradle.properties @@ -0,0 +1,3 @@ +jacksonVersion = 2.13.3 +unirestVersion=3.13.10 +cordaApiVersion=5.0.0.190-DevPreview-2 diff --git a/buildSrc/settings.gradle b/buildSrc/settings.gradle new file mode 100644 index 0000000..86ac012 --- /dev/null +++ b/buildSrc/settings.gradle @@ -0,0 +1 @@ +// File intentionally left blank diff --git a/buildSrc/src/main/groovy/csde.gradle b/buildSrc/src/main/groovy/csde.gradle new file mode 100644 index 0000000..bbdf48d --- /dev/null +++ b/buildSrc/src/main/groovy/csde.gradle @@ -0,0 +1,252 @@ +import com.r3.csde.CsdeRpcInterface + +plugins { + id 'java-library' + id 'checkstyle' + id 'groovy' + id 'java' +} + + +configurations { + combinedWorker{ + canBeConsumed = false + canBeResolved= true + } + + myPostgresJDBC { + canBeConsumed = false + canBeResolved = true + } + +} + +// Dependencies for supporting tools +dependencies { + combinedWorker "net.corda:corda-combined-worker:$combinedWorkerVersion" + myPostgresJDBC 'org.postgresql:postgresql:42.4.1' + implementation "org.codehaus.groovy:groovy-json:3.0.9" +} + + +def pluginGroupName = "CSDE" +def pluginImplGroupName = "other" +def cordaBinDir= System.getProperty('user.home') + "/.corda/corda5" +def cordaCliBinDir = System.getProperty('user.home') + "/.corda/cli" +def cordaJDBCDir = cordaBinDir + "/jdbcDrivers" +def signingCertAlias="gradle-plugin-default-key" +// Get error if this is not a autotyped object +// def signingCertFName = "$rootDir/config/gradle-plugin-default-key.pem" +def signingCertFName = rootDir.toString() + "/csde-config/gradle-plugin-default-key.pem" +def keystoreAlias = "my-signing-key" +def keystoreFName = devEnvWorkspace + "/signingkeys.pfx" +def keystoreCertFName = devEnvWorkspace + "/signingkey1.pem" +def combiWorkerPidCacheFile = devEnvWorkspace + "/CordaPID.dat" + + +// Need to read things from cordapp plugin +def cpiName = 'cpi name' + +def csdeHelper = new CsdeRpcInterface(project, + cordaClusterURL.toString(), + cordaRpcUser, + cordaRpcPasswd, + devEnvWorkspace, + new String("${System.getProperty("java.home")}/bin"), + dbContainerName, + cordaJDBCDir, + combiWorkerPidCacheFile +) + + +tasks.register("getPostgresJDBC") { + group = pluginImplGroupName + doLast { + copy { + from configurations.myPostgresJDBC + into "$cordaJDBCDir" + } + } +} + +tasks.register('projInit') { + group = pluginImplGroupName + doLast { + mkdir devEnvWorkspace + } +} + +tasks.register("createGroupPolicy") { + group = pluginImplGroupName + dependsOn('projInit') + doLast { + + def groupPolicyFName = new String("${devEnvWorkspace}/GroupPolicy.json") + def devnetFName = new String("$rootDir/csde-config/dev-net.json") + File groupPolicyFile = new File(groupPolicyFName) + File devnetFile = new File(devnetFName) + if (!groupPolicyFile.exists() || groupPolicyFile.lastModified() < devnetFile.lastModified()) { + def configX500Ids = csdeHelper.getConfigX500Ids() + + println("createGroupPolicy: Creating a GroupPolicy") + + javaexec { + classpath = files("$cordaCliBinDir/corda-cli.jar") + jvmArgs = ["-Dpf4j.pluginsDir=$cordaCliBinDir/plugins/"] + standardOutput = new FileOutputStream(groupPolicyFName) + LinkedList myArgs = new LinkedList() + myArgs.add("mgm") + myArgs.add("groupPolicy") + configX500Ids.forEach { + myArgs.add("--name") + myArgs.add("$it") + } + + myArgs.add("--endpoint-protocol=1") + myArgs.add("--endpoint=http://localhost:1080") + args = myArgs + } + + } else { + println("createPolicyTask: everything up to date; nothing to do.") + } + + } +} + +tasks.register("getDevCordaLite", Copy) { + group = pluginImplGroupName + from configurations.combinedWorker + into cordaBinDir +} + +tasks.register('createKeystore') { + group = pluginImplGroupName + dependsOn('projInit') + doLast { + File keystoreFile = new File(keystoreFName) + if(!keystoreFile.exists()) { + println('createKeystore: Create a keystore') + exec { + commandLine "keytool", "-genkeypair", + "-alias", keystoreAlias, + "-keystore", keystoreFName, + "-storepass", "keystore password", + "-dname", "CN=CPI Example - My Signing Key, O=CorpOrgCorp, L=London, C=GB", + "-keyalg", "RSA", + "-storetype", "pkcs12", + "-validity", "4000" + } + // Need to add the default signing key to the keystore + exec { + commandLine "keytool", "-importcert", + "-keystore", keystoreFName, + "-storepass", "keystore password", + "-noprompt", + "-alias", signingCertAlias, + "-file", signingCertFName + } + // keytool -exportcert -rfc -alias "signing key 1" -keystore signingkeys.pfx -storepass "keystore password" -file signingkey1.pem + exec { + commandLine "keytool", + "-exportcert", "-rfc", "-alias", keystoreAlias, + "-keystore", keystoreFName, + "-storepass", "keystore password", + "-file", keystoreCertFName + } + } + else { + println('createKeystore: keystore already created; nothing to do.') + } + + } +} + + +tasks.register('buildCPI') { + group = pluginGroupName + dependsOn('build', 'createGroupPolicy', 'createKeystore') + + doLast{ + def cpiFile= buildDir.toString() + "/" + project.archivesBaseName + "-" + project.version + ".cpi" + delete { delete cpiFile } + File srcDir + srcDir = file('build/libs') + + // Create a file collection using a closure + def collection = layout.files { srcDir.listFiles() } + def cpbs = collection.filter { it.getName().endsWith(".cpb") } + + javaexec { + classpath = files("$cordaCliBinDir/corda-cli.jar") + jvmArgs = ["-Dpf4j.pluginsDir=$cordaCliBinDir/plugins/"] + args = ['package', 'create-cpi', + '--cpb', cpbs.singleFile.absolutePath, + '--group-policy', "${devEnvWorkspace}/GroupPolicy.json", + '--cpi-name', cpiName, + '--cpi-version', project.version, + '--file', cpiFile, + '--keystore', "${devEnvWorkspace}/signingkeys.pfx", + '--storepass', 'keystore password', + '--key', 'my-signing-key' ] + } + } + +} + +tasks.register("deployCPI") { + group = pluginImplGroupName + dependsOn('buildCPI') + doLast { + csdeHelper.uploadCertificate(signingCertAlias, signingCertFName) + csdeHelper.uploadCertificate(keystoreAlias, keystoreCertFName) + csdeHelper.deployCPI("${buildDir}/${project.archivesBaseName}-${project.version}.cpi", cpiName, project.version) + } +} + +tasks.register("createAndRegVNodes") { + group = pluginImplGroupName + dependsOn('deployCPI') + doLast { + csdeHelper.createAndRegVNodes() + } +} + +tasks.register('listVNodes') { + group = pluginGroupName + doLast { + csdeHelper.listVNodes() + } +} + +tasks.register('listCPIs') { + group = pluginImplGroupName + doLast { + csdeHelper.listCPIs() + } +} + +// Empty task, just acts as the Task user entry point task. +tasks.register('deployCordapp') { + group = pluginGroupName + dependsOn("createAndRegVNodes") +} + +tasks.register("startCorda") { + group = pluginGroupName + dependsOn('getDevCordaLite', 'getPostgresJDBC') + doLast { + mkdir devEnvWorkspace + csdeHelper.startCorda() + } +} + +tasks.register("stopCorda") { + group = pluginGroupName + doLast { + csdeHelper.stopCorda() + } +} + + + diff --git a/buildSrc/src/main/java/com/r3/csde/CsdeException.java b/buildSrc/src/main/java/com/r3/csde/CsdeException.java new file mode 100644 index 0000000..163d49b --- /dev/null +++ b/buildSrc/src/main/java/com/r3/csde/CsdeException.java @@ -0,0 +1,7 @@ +package com.r3.csde; + +public class CsdeException extends Exception { + public CsdeException(String message){ + super(message); + } +} \ No newline at end of file diff --git a/buildSrc/src/main/java/com/r3/csde/CsdeRpcInterface.java b/buildSrc/src/main/java/com/r3/csde/CsdeRpcInterface.java new file mode 100644 index 0000000..eef8c7e --- /dev/null +++ b/buildSrc/src/main/java/com/r3/csde/CsdeRpcInterface.java @@ -0,0 +1,415 @@ +package com.r3.csde; + +import kong.unirest.json.JSONArray; +import org.gradle.api.Project; +import net.corda.v5.base.types.MemberX500Name; +import kong.unirest.Unirest; +import kong.unirest.json.JSONObject; +import org.jetbrains.annotations.NotNull; +import java.io.*; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Scanner; +import java.util.Set; +import static java.lang.Thread.sleep; + +public class CsdeRpcInterface { + private Project project; + private String baseURL = "https://localhost:8888"; + private String rpcUser = "admin"; + private String rpcPasswd = "admin"; + private String workspaceDir = "workspace"; + static private int retryWaitMs = 1000; + static PrintStream out = System.out; + static private String CPIUploadStatusBaseName = "CPIFileStatus.json"; + static private String CPIUploadStatusFName; + static private String X500ConfigFile = "config/dev-net.json"; + static private String javaBinDir; + static private String cordaPidCache = "CordaPIDCache.dat"; + static private String dbContainerName; + private String JDBCDir; + private String combinedWorkerBinRe; + + public CsdeRpcInterface() { + } + + public CsdeRpcInterface (Project inProject, + String inBaseUrl, + String inRpcUser, + String inRpcPasswd, + String inWorkspaceDir, + String inJavaBinDir, + String inDbContainerName, + String inJDBCDir, + String inCordaPidCache + ) { + project = inProject; + baseURL = inBaseUrl; + rpcUser = inRpcUser; + rpcPasswd = inRpcPasswd; + workspaceDir = inWorkspaceDir; + javaBinDir = inJavaBinDir; + cordaPidCache = inCordaPidCache; + dbContainerName = inDbContainerName; + JDBCDir = inJDBCDir; + CPIUploadStatusFName = workspaceDir +"/"+ CPIUploadStatusBaseName; + + } + + + static private void rpcWait(int millis) { + try { + sleep(millis); + } + catch(InterruptedException e) { + throw new UnsupportedOperationException("Interrupts not supported.", e); + } + } + + static private void rpcWait() { + rpcWait(retryWaitMs); + } + + public LinkedList getConfigX500Ids() throws IOException { + LinkedList x500Ids = new LinkedList<>(); + com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); + + FileInputStream in = new FileInputStream(X500ConfigFile); + com.fasterxml.jackson.databind.JsonNode jsonNode = mapper.readTree(in); + for( com.fasterxml.jackson.databind.JsonNode identity: jsonNode.get("identities")) { + String idAsString = identity.toString(); + x500Ids.add(idAsString.substring(1,idAsString.length()-1)); + } + return x500Ids; + } + + static public String getLastCPIUploadChkSum(@NotNull String CPIUploadStatusFName) throws IOException, NullPointerException { + + com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); + FileInputStream in = new FileInputStream(CPIUploadStatusFName); + com.fasterxml.jackson.databind.JsonNode jsonNode = mapper.readTree(in); + + + String checksum = jsonNode.get("cpiFileChecksum").toString(); + if(checksum == null || checksum.equals("null")) { + throw new NullPointerException("Missing cpiFileChecksum in file " + CPIUploadStatusFName+ " with contents:" + jsonNode); + } + return checksum; + } + + + public void reportError(@NotNull kong.unirest.HttpResponse response) throws CsdeException { + + out.println("*** *** ***"); + out.println("Unexpected response from Corda"); + out.println("Status="+ response.getStatus()); + out.println("*** Headers ***\n"+ response.getHeaders()); + out.println("*** Body ***\n"+ response.getBody()); + out.println("*** *** ***"); + throw new CsdeException("Error: unexpected response from Corda."); + } + + public void downloadFile(String url, String targetPath) { + Unirest.get(url) + .asFile(targetPath) + .getBody(); + } + + public kong.unirest.HttpResponse getVNodeInfo() { + Unirest.config().verifySsl(false); + return Unirest.get(baseURL + "/api/v1/virtualnode/") + .basicAuth(rpcUser, rpcPasswd) + .asJson(); + } + + public void listVNodesVerbose() { + kong.unirest.HttpResponse vnodeResponse = getVNodeInfo(); + out.println("VNodes:\n" + vnodeResponse.getBody().toPrettyString()); + } + + // X500Name, cpiname, shorthash, + public void listVNodes() { + kong.unirest.HttpResponse vnodeResponse = getVNodeInfo(); + + kong.unirest.json.JSONArray virtualNodesJson = (JSONArray) vnodeResponse.getBody().getObject().get("virtualNodes"); + out.println("X500 Name\tHolding identity short hash"); + for(Object o: virtualNodesJson){ + if(o instanceof kong.unirest.json.JSONObject) { + kong.unirest.json.JSONObject idObj = ((kong.unirest.json.JSONObject) o).getJSONObject("holdingIdentity"); + out.print("\"" + idObj.get("x500Name") + "\""); + out.println("\t\"" + idObj.get("shortHash") + "\""); + } + } + } + + public kong.unirest.HttpResponse getCpiInfo() { + Unirest.config().verifySsl(false); + return Unirest.get(baseURL + "/api/v1/cpi/") + .basicAuth(rpcUser, rpcPasswd) + .asJson(); + + } + + public void listCPIs() { + kong.unirest.HttpResponse cpiResponse = getCpiInfo(); + kong.unirest.json.JSONArray jArray = (JSONArray) cpiResponse.getBody().getObject().get("cpis"); + + for(Object o: jArray){ + if(o instanceof kong.unirest.json.JSONObject) { + kong.unirest.json.JSONObject idObj = ((kong.unirest.json.JSONObject) o).getJSONObject("id"); + out.print("cpiName=" + idObj.get("cpiName")); + out.println(", cpiVersion=" + idObj.get("cpiVersion")); + } + } + } + + public void uploadCertificate(String certAlias, String certFName) { + Unirest.config().verifySsl(false); + kong.unirest.HttpResponse uploadResponse = Unirest.put(baseURL + "/api/v1/certificates/codesigner/") + .field("alias", certAlias) + .field("certificate", new File(certFName)) + .basicAuth(rpcUser, rpcPasswd) + .asJson(); + out.println("Certificate/key upload, alias "+certAlias+" certificate/key file "+certFName); + out.println(uploadResponse.getBody().toPrettyString()); + } + + public void forceuploadCPI(String cpiFName) throws FileNotFoundException, CsdeException { + Unirest.config().verifySsl(false); + kong.unirest.HttpResponse jsonResponse = Unirest.post(baseURL + "/api/v1/maintenance/virtualnode/forcecpiupload/") + .field("upload", new File(cpiFName)) + .basicAuth(rpcUser, rpcPasswd) + .asJson(); + + if(jsonResponse.getStatus() == 200) { + String id = (String) jsonResponse.getBody().getObject().get("id"); + out.println("get id:\n" +id); + kong.unirest.HttpResponse statusResponse = uploadStatus(id); + + if (statusResponse.getStatus() == 200) { + PrintStream cpiUploadStatus = new PrintStream(new FileOutputStream(CPIUploadStatusFName)); + cpiUploadStatus.print(statusResponse.getBody()); + out.println("Caching CPI file upload status:\n" + statusResponse.getBody()); + } else { + reportError(statusResponse); + } + } + else { + reportError(jsonResponse); + } + } + + private boolean uploadStatusRetry(kong.unirest.HttpResponse response) { + int status = response.getStatus(); + kong.unirest.JsonNode body = response.getBody(); + // Do not retry on success + if(status == 200) { + // Keep retrying until we get "OK" may move through "Validateing upload", "Persisting CPI" + return !(body.getObject().get("status").equals("OK")); + } + else if (status == 400){ + JSONObject details = response.getBody().getObject().getJSONObject("details"); + if( details != null ){ + String code = (String) details.getString("code"); + return !code.equals("BAD_REQUEST"); + } + else { + // 400 otherwise means some transient problem + return true; + } + } + return false; + } + + public kong.unirest.HttpResponse uploadStatus(String requestId) { + kong.unirest.HttpResponse statusResponse = null; + do { + rpcWait(1000); + statusResponse = Unirest + .get(baseURL + "/api/v1/cpi/status/" + requestId + "/") + .basicAuth(rpcUser, rpcPasswd) + .asJson(); + out.println("Upload status="+statusResponse.getStatus()+", status query response:\n"+statusResponse.getBody().toPrettyString()); + } + while(uploadStatusRetry(statusResponse)); + + return statusResponse; + } + + public void deployCPI(String cpiFName, String cpiName, String cpiVersion) throws FileNotFoundException, CsdeException { + Unirest.config().verifySsl(false); + + kong.unirest.HttpResponse cpiResponse = getCpiInfo(); + kong.unirest.json.JSONArray jArray = (JSONArray) cpiResponse.getBody().getObject().get("cpis"); + + + int matches = 0; + for(Object o: jArray.toList() ) { + if(o instanceof JSONObject) { + JSONObject idObj = ((JSONObject) o).getJSONObject("id"); + if((idObj.get("cpiName").toString().equals(cpiName) + && idObj.get("cpiVersion").toString().equals(cpiVersion))) { + matches++; + } + } + } + out.println("Matching CPIS="+matches); + + + if(matches == 0) { + kong.unirest.HttpResponse uploadResponse = Unirest.post(baseURL + "/api/v1/cpi/") + .field("upload", new File(cpiFName)) + .basicAuth(rpcUser, rpcPasswd) + .asJson(); + + kong.unirest.JsonNode body = uploadResponse.getBody(); + + int status = uploadResponse.getStatus(); + + out.println("Upload Status:" + status); + out.println("Pretty print the body\n" + body.toPrettyString()); + + // We expect the id field to be a string. + if (status == 200) { + String id = (String) body.getObject().get("id"); + out.println("get id:\n" + id); + + kong.unirest.HttpResponse statusResponse = uploadStatus(id); + if (statusResponse.getStatus() == 200) { + PrintStream cpiUploadStatus = new PrintStream(new FileOutputStream(CPIUploadStatusFName)); + cpiUploadStatus.print(statusResponse.getBody()); + out.println("Caching CPI file upload status:\n" + statusResponse.getBody()); + } else { + reportError(statusResponse); + } + } else { + reportError(uploadResponse); + } + } + else { + out.println("CPI already uploaded doing a 'force' upload."); + forceuploadCPI(cpiFName); + } + } + + public void createAndRegVNodes() throws IOException, CsdeException{ + Unirest.config().verifySsl(false); + String cpiCheckSum = getLastCPIUploadChkSum( CPIUploadStatusFName ); + + LinkedList x500Ids = getConfigX500Ids(); + LinkedList OKHoldingShortIds = new LinkedList<>(); + + // For each identity check that it already exists. + Set existingX500 = new HashSet<>(); + kong.unirest.HttpResponse vnodeListResponse = getVNodeInfo(); + + kong.unirest.json.JSONArray virtualNodesJson = (JSONArray) vnodeListResponse.getBody().getObject().get("virtualNodes"); + for(Object o: virtualNodesJson){ + if(o instanceof kong.unirest.json.JSONObject) { + kong.unirest.json.JSONObject idObj = ((kong.unirest.json.JSONObject) o).getJSONObject("holdingIdentity"); + String x500id = (String) idObj.get("x500Name"); + existingX500.add(MemberX500Name.parse( x500id) ); + } + } + + // Create the VNodes + for(String x500id: x500Ids) { + if(!existingX500.contains(MemberX500Name.parse(x500id) )) { + out.println("Creating VNode for x500id=\"" + x500id + "\" cpi checksum=" + cpiCheckSum); + kong.unirest.HttpResponse jsonNode = Unirest.post(baseURL + "/api/v1/virtualnode") + .body("{ \"request\" : { \"cpiFileChecksum\": " + cpiCheckSum + ", \"x500Name\": \"" + x500id + "\" } }") + .basicAuth(rpcUser, rpcPasswd) + .asJson(); + // Logging. + + // need to check this and report errors. + // 200 - OK + // 409 - Vnode already exists + if (jsonNode.getStatus() != 409) { + if (jsonNode.getStatus() != 200) { + reportError(jsonNode); + } else { + JSONObject thing = jsonNode.getBody().getObject().getJSONObject("holdingIdentity"); + String shortHash = (String) thing.get("shortHash"); + OKHoldingShortIds.add(shortHash); + } + } + } + else { + out.println("Not creating a vnode for \"" + x500id + "\", vnode already exists."); + } + } + + // Register the VNodes + for(String shortHoldingIdHash: OKHoldingShortIds) { + kong.unirest.HttpResponse vnodeResponse = Unirest.post(baseURL + "/api/v1/membership/" + shortHoldingIdHash) + .body("{ \"memberRegistrationRequest\": { \"action\": \"requestJoin\", \"context\": { \"corda.key.scheme\" : \"CORDA.ECDSA.SECP256R1\" } } }") + .basicAuth(rpcUser, rpcPasswd) + .asJson(); + + out.println("Vnode membership submission:\n" + vnodeResponse.getBody().toPrettyString()); + } + + } + + public void startCorda() throws IOException { + PrintStream pidStore = new PrintStream(new FileOutputStream(cordaPidCache)); + File combinedWorkerJar = project.getConfigurations().getByName("combinedWorker").getSingleFile(); + + new ProcessBuilder( + "docker", + "run", "-d", "--rm", + "-p", "5432:5432", + "--name", dbContainerName, + "-e", "POSTGRES_DB=cordacluster", + "-e", "POSTGRES_USER=postgres", + "-e", "POSTGRES_PASSWORD=password", + "postgres:latest").start(); + rpcWait(10000); + + ProcessBuilder procBuild = new ProcessBuilder(javaBinDir + "/java", + "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", + "-Dco.paralleluniverse.fibers.verifyInstrumentation=true", + "-jar", + combinedWorkerJar.toString(), + "--instanceId=0", + "-mbus.busType=DATABASE", + "-spassphrase=password", + "-ssalt=salt", + "-spassphrase=password", + "-ssalt=salt", + "-ddatabase.user=user", + "-ddatabase.pass=password", + "-ddatabase.jdbc.url=jdbc:postgresql://localhost:5432/cordacluster", + "-ddatabase.jdbc.directory="+JDBCDir); + + + procBuild.redirectErrorStream(true); + Process proc = procBuild.start(); + pidStore.print(proc.pid()); + out.println("Corda Process-id="+proc.pid()); + } + + public void stopCorda() throws IOException, NoPidFile { + File cordaPIDFile = new File(cordaPidCache); + if(cordaPIDFile.exists()) { + Scanner sc = new Scanner(cordaPIDFile); + long pid = sc.nextLong(); + out.println("pid to kill=" + pid); + + if (System.getProperty("os.name").toLowerCase().contains("windows")) { + new ProcessBuilder("Powershell", "-Command", "Stop-Process", "-Id", Long.toString(pid), "-PassThru").start(); + } else { + new ProcessBuilder("kill", "-9", Long.toString(pid)).start(); + } + + Process proc = new ProcessBuilder("docker", "stop", dbContainerName).start(); + + cordaPIDFile.delete(); + } + else { + throw new NoPidFile("Cannot stop the Combined worker\nCached process ID file " + cordaPidCache + " missing.\nWas the combined worker not started?"); + } + } + +} diff --git a/buildSrc/src/main/java/com/r3/csde/NoPidFile.java b/buildSrc/src/main/java/com/r3/csde/NoPidFile.java new file mode 100644 index 0000000..d7bb3dc --- /dev/null +++ b/buildSrc/src/main/java/com/r3/csde/NoPidFile.java @@ -0,0 +1,7 @@ +package com.r3.csde; + +public class NoPidFile extends Exception { + public NoPidFile(String message){ + super(message); + } +} \ No newline at end of file diff --git a/buildSrc/src/test/java/CsdeRpcInterfaceTests.java b/buildSrc/src/test/java/CsdeRpcInterfaceTests.java new file mode 100644 index 0000000..dcf57d3 --- /dev/null +++ b/buildSrc/src/test/java/CsdeRpcInterfaceTests.java @@ -0,0 +1,4 @@ +import com.r3.csde.CsdeRpcInterface; + +public class CsdeRpcInterfaceTests { +} diff --git a/config/dev-net.json b/config/dev-net.json new file mode 100644 index 0000000..0d5aa28 --- /dev/null +++ b/config/dev-net.json @@ -0,0 +1,7 @@ +{ +"identities" : [ + "CN=Alice, OU=Test Dept, O=R3, L=London, C=GB", + "CN=Bob, OU=Test Dept, O=R3, L=London, C=GB", + "CN=Charlie, OU=Test Dept, O=R3, L=London, C=GB" + ] +} diff --git a/config/gradle-plugin-default-key.pem b/config/gradle-plugin-default-key.pem new file mode 100644 index 0000000..5294bbd --- /dev/null +++ b/config/gradle-plugin-default-key.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB7zCCAZOgAwIBAgIEFyV7dzAMBggqhkjOPQQDAgUAMFsxCzAJBgNVBAYTAkdC +MQ8wDQYDVQQHDAZMb25kb24xDjAMBgNVBAoMBUNvcmRhMQswCQYDVQQLDAJSMzEe +MBwGA1UEAwwVQ29yZGEgRGV2IENvZGUgU2lnbmVyMB4XDTIwMDYyNTE4NTI1NFoX +DTMwMDYyMzE4NTI1NFowWzELMAkGA1UEBhMCR0IxDzANBgNVBAcTBkxvbmRvbjEO +MAwGA1UEChMFQ29yZGExCzAJBgNVBAsTAlIzMR4wHAYDVQQDExVDb3JkYSBEZXYg +Q29kZSBTaWduZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQDjSJtzQ+ldDFt +pHiqdSJebOGPZcvZbmC/PIJRsZZUF1bl3PfMqyG3EmAe0CeFAfLzPQtf2qTAnmJj +lGTkkQhxo0MwQTATBgNVHSUEDDAKBggrBgEFBQcDAzALBgNVHQ8EBAMCB4AwHQYD +VR0OBBYEFLMkL2nlYRLvgZZq7GIIqbe4df4pMAwGCCqGSM49BAMCBQADSAAwRQIh +ALB0ipx6EplT1fbUKqgc7rjH+pV1RQ4oKF+TkfjPdxnAAiArBdAI15uI70wf+xlL +zU+Rc5yMtcOY4/moZUq36r0Ilg== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..c6aac50 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,35 @@ +kotlin.code.style=official + +# Specify the version of the Corda-API to use. +# This needs to match the version supported by the Corda Cluster the CorDapp will run on. +cordaApiVersion=5.0.0.190-DevPreview-2 + +# Specify the version of the cordapp-cpb and cordapp-cpk plugins +cordaPluginsVersion=7.0.0-DevPreview-2 + +# For the time being this just needs to be set to a dummy value. +platformVersion = 999 + +# Version of Kotlin to use. +# We recommend using a version close to that used by Corda-API. +kotlinVersion = 1.7.10 + +# Do not use default dependencies. +kotlin.stdlib.default.dependency=false + +# Test Tooling Dependency Versions +junitVersion = 5.8.2 +mockitoKotlinVersion=4.0.0 +mockitoVersion=4.6.1 +hamcrestVersion=2.2 + +# Settings For Development Utilities +testUtilsVersion=5.0.0.0-DevPreview-2 +combinedWorkerVersion=5.0.0.0-DevPreview-2 + +cordaClusterURL=https://localhost:8888 +cordaRpcUser=admin +cordaRpcPasswd=admin +devEnvWorkspace=workspace +dbContainerName=CSDEpostgresql + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..41d9927 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..5ec4b8e --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..744e882 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..3a9ccef --- /dev/null +++ b/settings.gradle @@ -0,0 +1,27 @@ +pluginManagement { + // Declare the repositories where plugins are stored. + repositories { + gradlePluginPortal() + mavenCentral { + content { + includeGroupByRegex 'net\\.corda(\\..*)?' + } + } + } + + // The plugin dependencies with versions of the plugins congruent with the specified CorDapp plugin version, + // Corda API version, and Kotlin version. + plugins { + id 'net.corda.plugins.cordapp-cpk2' version cordaPluginsVersion + id 'net.corda.plugins.cordapp-cpb2' version cordaPluginsVersion + id 'net.corda.cordapp.cordapp-configuration' version cordaApiVersion + id 'org.jetbrains.kotlin.jvm' version kotlinVersion + id 'org.jetbrains.kotlin.plugin.jpa' version kotlinVersion + id 'org.jetbrains.kotlin.plugin.allopen' version kotlinVersion + } +} + +// Root project name, used in naming the project as a whole and used in naming objects built by the project. +rootProject.name = 'csde-cordapp-template-java' + + diff --git a/src/main/java/com/r3/developers/csdetemplate/Message.java b/src/main/java/com/r3/developers/csdetemplate/Message.java new file mode 100644 index 0000000..a603f3c --- /dev/null +++ b/src/main/java/com/r3/developers/csdetemplate/Message.java @@ -0,0 +1,16 @@ +package com.r3.developers.csdetemplate; + +import net.corda.v5.base.annotations.CordaSerializable; +import net.corda.v5.base.types.MemberX500Name; + +// // A class which will contain a message, It must be marked with @CordaSerializable for Corda +//// to be able to send from one virtual node to another. +@CordaSerializable +public class Message { + Message(MemberX500Name _sender, String _message) { + sender = _sender; + message = _message; + } + public MemberX500Name sender; + public String message; +} diff --git a/src/main/java/com/r3/developers/csdetemplate/MyFirstFlow.java b/src/main/java/com/r3/developers/csdetemplate/MyFirstFlow.java new file mode 100644 index 0000000..77e10e6 --- /dev/null +++ b/src/main/java/com/r3/developers/csdetemplate/MyFirstFlow.java @@ -0,0 +1,98 @@ +package com.r3.developers.csdetemplate; + +import net.corda.v5.application.flows.*; +import net.corda.v5.application.marshalling.JsonMarshallingService; +import net.corda.v5.application.membership.MemberLookup; +import net.corda.v5.application.messaging.FlowMessaging; +import net.corda.v5.application.messaging.FlowSession; +import net.corda.v5.base.annotations.Suspendable; +import net.corda.v5.base.types.MemberX500Name; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +// MyFirstFlow is an initiating flow, it's corresponding responder flow is called MyFirstFlowResponder (defined below) +// to link the two sides of the flow together they need to have the same protocol. +@InitiatingFlow(protocol = "another-flow") +// MyFirstFlow should inherit from RPCStartableFlow, which tells Corda it can be started via an RPC call +class MyFirstFlow implements RPCStartableFlow { + + // It is useful to be able to log messages from the flows for debugging. + private final Logger log = LoggerFactory.getLogger(MyFirstFlow.class); + + // Corda has a set of injectable services which are injected into the flow at runtime. + // Flows declare them with @CordaInjectable, then the flows have access to their services. + + // JsonMarshallingService provides a Service for manipulating json + @CordaInject + JsonMarshallingService jsonMarshallingService; + + // FlowMessaging provides a service for establishing flow sessions between Virtual Nodes and + // sending and receiving payloads between them + @CordaInject + FlowMessaging flowMessaging; + + // MemberLookup provides a service for looking up information about members of the Virtual Network which + // this CorDapp is operating in. + @CordaInject + MemberLookup memberLookup; + + public MyFirstFlow() {} + + // When a flow is invoked it's call() method is called. + // call() methods must be marked as @Suspendable, this allows Corda to pause mid-execution to wait + // for a response from the other flows and services + @Suspendable + @Override + public String call(@NotNull RPCRequestData requestBody) { + + // Useful logging to follow what's happening in the console or logs + log.info("MFF: MyFirstFlow.call() called"); + + // Show the requestBody in the logs - this can be used to help establish the format for starting a flow on corda + log.info("MFF: requestBody: " + requestBody.getRequestBody()); + + // Deserialize the Json requestBody into the MyfirstFlowStartArgs class using the JsonSerialisation Service + MyFirstFlowStartArgs flowArgs = requestBody.getRequestBodyAs(jsonMarshallingService, MyFirstFlowStartArgs.class); + + // Obtain the MemberX500Name of counterparty + MemberX500Name otherMember = flowArgs.othermember; + + // Get our identity from the MemberLookup service. + MemberX500Name ourIdentity = memberLookup.myInfo().getName(); + + // Create the message payload using the MessageClass we defined. + Message message = new Message(otherMember, "Hello from " + ourIdentity + "."); + + // Log the message to be sent. + log.info("MFF: message.message: " + message.message); + + // Start a flow session with the otherMember using the FlowMessaging service + // The otherMember's Virtual Node will run the corresponding MyFirstFlowResponder responder flow + FlowSession session = flowMessaging.initiateFlow(otherMember); + + // Send the Payload using the send method on the session to the MyFirstFlowResponder Responder flow + session.send(message); + + // Receive a response from the Responder flow + Message response = session.receive(Message.class); + + // The return value of a RPCStartableFlow must always be a String, this string will be passed + // back as the REST RPC response when the status of the flow is queried on Corda, or as the return + // value from the flow when testing using the Simulator + return response.message; + } +} + + +/* +RequestBody for triggering the flow via http-rpc: +{ + "clientRequestId": "r1", + "flowClassName": "com.r3.developers.csdetemplate.MyFirstFlow", + "requestData": { + "otherMember":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB" + } +} + */ \ No newline at end of file diff --git a/src/main/java/com/r3/developers/csdetemplate/MyFirstFlowResponder.java b/src/main/java/com/r3/developers/csdetemplate/MyFirstFlowResponder.java new file mode 100644 index 0000000..559d57e --- /dev/null +++ b/src/main/java/com/r3/developers/csdetemplate/MyFirstFlowResponder.java @@ -0,0 +1,72 @@ +package com.r3.developers.csdetemplate; + +import net.corda.v5.application.flows.CordaInject; +import net.corda.v5.application.flows.InitiatedBy; +import net.corda.v5.application.flows.ResponderFlow; +import net.corda.v5.application.membership.MemberLookup; +import net.corda.v5.application.messaging.FlowSession; +import net.corda.v5.base.annotations.Suspendable; +import net.corda.v5.base.types.MemberX500Name; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +// MyFirstFlowResponder is a responder flow, it's corresponding initiating flow is called MyFirstFlow (defined above) +// to link the two sides of the flow together they need to have the same protocol. +@InitiatedBy(protocol = "another-flow") +// Responder flows must inherit from ResponderFlow +class MyFirstFlowResponder implements ResponderFlow { + + // It is useful to be able to log messages from the flows for debugging. + private final Logger log = LoggerFactory.getLogger(MyFirstFlowResponder.class); + + // MemberLookup provides a service for looking up information about members of the Virtual Network which + // this CorDapp is operating in. + @CordaInject + MemberLookup memberLookup; + + public MyFirstFlowResponder() {} + + // Responder flows are invoked when an initiating flow makes a call via a session set up with the Virtual + // node hosting the Responder flow. When a responder flow is invoked it's call() method is called. + // call() methods must be marked as @Suspendable, this allows Corda to pause mid-execution to wait + // for a response from the other flows and services/ + // The Call method has the flow session passed in as a parameter by Corda so the session is available to + // responder flow code, you don't need to inject the FlowMessaging service. + @Suspendable + @Override + public void call(FlowSession session) { + + // Useful logging to follow what's happening in the console or logs + log.info("MFF: MyFirstResponderFlow.call() called"); + + // Receive the payload and deserialize it into a Message class + Message receivedMessage = session.receive(Message.class); + + // Log the message as a proxy for performing some useful operation on it. + log.info("MFF: Message received from " + receivedMessage.sender + ":" + receivedMessage.message); + + // Get our identity from the MemberLookup service. + MemberX500Name ourIdentity = memberLookup.myInfo().getName(); + + // Create a response to greet the sender + Message response = new Message(ourIdentity, + "Hello " + session.getCounterparty().getCommonName() + ", best wishes from " + ourIdentity.getCommonName()); + + // Log the response to be sent. + log.info("MFF: response.message: " + response.message); + + // Send the response via the send method on the flow session + session.send(response); + } +} +/* +RequestBody for triggering the flow via http-rpc: +{ + "clientRequestId": "r1", + "flowClassName": "com.r3.developers.csdetemplate.MyFirstFlow", + "requestData": { + "otherMember":"CN=Bob, OU=Test Dept, O=R3, L=London, C=GB" + } +} + */ \ No newline at end of file diff --git a/src/main/java/com/r3/developers/csdetemplate/MyFirstFlowStartArgs.java b/src/main/java/com/r3/developers/csdetemplate/MyFirstFlowStartArgs.java new file mode 100644 index 0000000..bfb6ae9 --- /dev/null +++ b/src/main/java/com/r3/developers/csdetemplate/MyFirstFlowStartArgs.java @@ -0,0 +1,13 @@ +package com.r3.developers.csdetemplate; + +import net.corda.v5.base.types.MemberX500Name; + +// // A class to hold the arguments required to start the flow +//class MyFirstFlowStartArgs(val otherMember: MemberX500Name) +public class MyFirstFlowStartArgs { + public MemberX500Name othermember; + + public MyFirstFlowStartArgs(MemberX500Name othermember) { + this.othermember = othermember; + } +} diff --git a/src/test/java/com/r3/developers/csdetemplate/MyFirstFlowTest.java b/src/test/java/com/r3/developers/csdetemplate/MyFirstFlowTest.java new file mode 100644 index 0000000..0e9e2c7 --- /dev/null +++ b/src/test/java/com/r3/developers/csdetemplate/MyFirstFlowTest.java @@ -0,0 +1,47 @@ +package com.r3.developers.csdetemplate; + +import net.corda.simulator.HoldingIdentity; +import net.corda.simulator.RequestData; +import net.corda.simulator.SimulatedVirtualNode; +import net.corda.simulator.Simulator; +import net.corda.v5.base.types.MemberX500Name; +import org.junit.jupiter.api.Test; + +class MyFirstFlowTest { + + // Names picked to match the corda network in config/dev-net.json + private MemberX500Name aliceX500 = MemberX500Name.parse("CN=Alice, OU=Test Dept, O=R3, L=London, C=GB"); + private MemberX500Name bobX500 = MemberX500Name.parse("CN=Bob, OU=Test Dept, O=R3, L=London, C=GB"); + + @Test + public void test_that_MyFirstFLow_returns_correct_message() { + + // Instantiate an instance of the Simulator + Simulator simulator = new Simulator(); + + // Create Alice's and Bob HoldingIDs + HoldingIdentity aliceHoldingID = HoldingIdentity.Companion.create(aliceX500); + HoldingIdentity bobHoldingID = HoldingIdentity.Companion.create(bobX500); + + // Create Alice and Bob's virtual nodes, including the Class's of the flows which will be registered on each node. + // We don't assign Bob's virtual node to a val because we don't need it for this particular test. + SimulatedVirtualNode aliceVN = simulator.createVirtualNode(aliceHoldingID, MyFirstFlow.class); + simulator.createVirtualNode(bobHoldingID, MyFirstFlowResponder.class); + + // Create an instance of the MyFirstFlowStartArgs which contains the request arguments for starting the flow + MyFirstFlowStartArgs myFirstFlowStartArgs = new MyFirstFlowStartArgs(bobX500); + + // Create a requestData object + RequestData requestData = RequestData.Companion.create( + "request no 1", // A unique reference for the instance of the flow request + MyFirstFlow.class, // The name of the flow class which is to be started + myFirstFlowStartArgs // The object which contains the start arguments of the flow + ); + + // Call the Flow on Alice's virtual node and capture the response from the flow + String flowResponse = aliceVN.callFlow(requestData); + + // Check that the flow has returned the expected string + assert(flowResponse.equals("Hello Alice, best wishes from Bob")); + } +}