Initial commit of the java template - includes only concverted code

This commit is contained in:
Chris Barratt 2022-10-12 23:04:51 +01:00
commit a15efd43a1
23 changed files with 1474 additions and 0 deletions

View File

@ -0,0 +1,15 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="DebugCorDapp" type="Remote">
<option name="USE_SOCKET_TRANSPORT" value="true" />
<option name="SERVER_MODE" value="false" />
<option name="SHMEM_ADDRESS" />
<option name="HOST" value="localhost" />
<option name="PORT" value="5005" />
<option name="AUTO_RESTART" value="false" />
<RunnerSettings RunnerId="Debug">
<option name="DEBUG_PORT" value="5005" />
<option name="LOCAL" value="false" />
</RunnerSettings>
<method v="2" />
</configuration>
</component>

142
build.gradle Normal file
View File

@ -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 components
// 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
}
}
}

21
buildSrc/build.gradle Normal file
View File

@ -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"
}

View File

@ -0,0 +1,3 @@
jacksonVersion = 2.13.3
unirestVersion=3.13.10
cordaApiVersion=5.0.0.190-DevPreview-2

1
buildSrc/settings.gradle Normal file
View File

@ -0,0 +1 @@
// File intentionally left blank

View File

@ -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<String> myArgs = new LinkedList<String>()
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()
}
}

View File

@ -0,0 +1,7 @@
package com.r3.csde;
public class CsdeException extends Exception {
public CsdeException(String message){
super(message);
}
}

View File

@ -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<String> getConfigX500Ids() throws IOException {
LinkedList<String> 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<kong.unirest.JsonNode> 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<kong.unirest.JsonNode> getVNodeInfo() {
Unirest.config().verifySsl(false);
return Unirest.get(baseURL + "/api/v1/virtualnode/")
.basicAuth(rpcUser, rpcPasswd)
.asJson();
}
public void listVNodesVerbose() {
kong.unirest.HttpResponse<kong.unirest.JsonNode> vnodeResponse = getVNodeInfo();
out.println("VNodes:\n" + vnodeResponse.getBody().toPrettyString());
}
// X500Name, cpiname, shorthash,
public void listVNodes() {
kong.unirest.HttpResponse<kong.unirest.JsonNode> 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<kong.unirest.JsonNode> getCpiInfo() {
Unirest.config().verifySsl(false);
return Unirest.get(baseURL + "/api/v1/cpi/")
.basicAuth(rpcUser, rpcPasswd)
.asJson();
}
public void listCPIs() {
kong.unirest.HttpResponse<kong.unirest.JsonNode> 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<kong.unirest.JsonNode> 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<kong.unirest.JsonNode> 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<kong.unirest.JsonNode> 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<kong.unirest.JsonNode> 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<kong.unirest.JsonNode> uploadStatus(String requestId) {
kong.unirest.HttpResponse<kong.unirest.JsonNode> 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<kong.unirest.JsonNode> 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<kong.unirest.JsonNode> 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<kong.unirest.JsonNode> 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<String> x500Ids = getConfigX500Ids();
LinkedList<String> OKHoldingShortIds = new LinkedList<>();
// For each identity check that it already exists.
Set<MemberX500Name> existingX500 = new HashSet<>();
kong.unirest.HttpResponse<kong.unirest.JsonNode> 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<kong.unirest.JsonNode> 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<kong.unirest.JsonNode> 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?");
}
}
}

View File

@ -0,0 +1,7 @@
package com.r3.csde;
public class NoPidFile extends Exception {
public NoPidFile(String message){
super(message);
}
}

View File

@ -0,0 +1,4 @@
import com.r3.csde.CsdeRpcInterface;
public class CsdeRpcInterfaceTests {
}

7
config/dev-net.json Normal file
View File

@ -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"
]
}

View File

@ -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-----

35
gradle.properties Normal file
View File

@ -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

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -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

185
gradlew vendored Executable file
View File

@ -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" "$@"

89
gradlew.bat vendored Normal file
View File

@ -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

27
settings.gradle Normal file
View File

@ -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'

View File

@ -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;
}

View File

@ -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"
}
}
*/

View File

@ -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"
}
}
*/

View File

@ -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;
}
}

View File

@ -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"));
}
}