diff --git a/.idea/runConfigurations/BankOfCordaDriverKt___Issue_Web.xml b/.idea/runConfigurations/BankOfCordaDriverKt___Issue_Web.xml deleted file mode 100644 index 321d3d2d06..0000000000 --- a/.idea/runConfigurations/BankOfCordaDriverKt___Issue_Web.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/BankOfCordaDriverKt___Run_Stack.xml b/.idea/runConfigurations/BankOfCordaDriverKt___Run_Stack.xml deleted file mode 100644 index ea61b6ef8d..0000000000 --- a/.idea/runConfigurations/BankOfCordaDriverKt___Run_Stack.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - \ No newline at end of file diff --git a/build.gradle b/build.gradle index d71ffe58cc..e72f20a51a 100644 --- a/build.gradle +++ b/build.gradle @@ -48,6 +48,7 @@ buildscript { ext.commons_collections_version = '4.1' ext.beanutils_version = '1.9.3' ext.crash_version = 'faba68332800f21278c5b600bf14ad55cef5989e' + ext.jsr305_version = constants.getProperty("jsr305Version") // Update 121 is required for ObjectInputFilter and at time of writing 131 was latest: ext.java8_minUpdateVersion = '131' diff --git a/constants.properties b/constants.properties index 211f29e563..7ee707fd23 100644 --- a/constants.properties +++ b/constants.properties @@ -1,6 +1,7 @@ -gradlePluginsVersion=2.0.9 +gradlePluginsVersion=3.0.0 kotlinVersion=1.1.60 platformVersion=2 guavaVersion=21.0 bouncycastleVersion=1.57 -typesafeConfigVersion=1.3.1 \ No newline at end of file +typesafeConfigVersion=1.3.1 +jsr305Version=3.0.2 diff --git a/core/build.gradle b/core/build.gradle index ab67d23248..bd443598d7 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -78,7 +78,7 @@ dependencies { compileOnly "co.paralleluniverse:quasar-core:$quasar_version:jdk8" // Thread safety annotations - compile "com.google.code.findbugs:jsr305:3.0.1" + compile "com.google.code.findbugs:jsr305:$jsr305_version" // Log4J: logging framework (ONLY explicitly referenced by net.corda.core.utilities.Logging.kt) compile "org.apache.logging.log4j:log4j-core:${log4j_version}" diff --git a/gradle-plugins/build.gradle b/gradle-plugins/build.gradle index 88169c215c..bfab124a56 100644 --- a/gradle-plugins/build.gradle +++ b/gradle-plugins/build.gradle @@ -10,6 +10,7 @@ buildscript { ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion") ext.bouncycastle_version = constants.getProperty("bouncycastleVersion") ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion") + ext.jsr305_version = constants.getProperty("jsr305Version") ext.kotlin_version = constants.getProperty("kotlinVersion") repositories { diff --git a/gradle-plugins/cordform-common/build.gradle b/gradle-plugins/cordform-common/build.gradle index 82274e0d09..2423bcd773 100644 --- a/gradle-plugins/cordform-common/build.gradle +++ b/gradle-plugins/cordform-common/build.gradle @@ -11,6 +11,9 @@ version gradle_plugins_version group 'net.corda.plugins' dependencies { + // JSR 305: Nullability annotations + compile "com.google.code.findbugs:jsr305:$jsr305_version" + // TypeSafe Config: for simple and human friendly config files. compile "com.typesafe:config:$typesafe_config_version" diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java index 047d7e6289..fc62b1bbee 100644 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java +++ b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java @@ -1,24 +1,40 @@ package net.corda.cordform; +import javax.annotation.Nonnull; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.List; import java.util.function.Consumer; public abstract class CordformDefinition { - public final Path driverDirectory; - public final ArrayList> nodeConfigurers = new ArrayList<>(); + private Path nodesDirectory = Paths.get("build", "nodes"); + private final List> nodeConfigurers = new ArrayList<>(); + private final List cordappPackages = new ArrayList<>(); - public CordformDefinition(Path driverDirectory) { - this.driverDirectory = driverDirectory; + public Path getNodesDirectory() { + return nodesDirectory; } - public void addNode(Consumer configurer) { + public void setNodesDirectory(Path nodesDirectory) { + this.nodesDirectory = nodesDirectory; + } + + public List> getNodeConfigurers() { + return nodeConfigurers; + } + + public void addNode(Consumer configurer) { nodeConfigurers.add(configurer); } + public List getCordappPackages() { + return cordappPackages; + } + /** * Make arbitrary changes to the node directories before they are started. * @param context Lookup of node directory by node name. */ - public abstract void setup(CordformContext context); + public abstract void setup(@Nonnull CordformContext context); } diff --git a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java index f8c4248f99..33b433a506 100644 --- a/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java +++ b/gradle-plugins/cordform-common/src/main/java/net/corda/cordform/CordformNode.java @@ -2,6 +2,9 @@ package net.corda.cordform; import static java.util.Collections.emptyList; import com.typesafe.config.*; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import java.util.Collections; import java.util.List; import java.util.Map; @@ -54,7 +57,7 @@ public class CordformNode implements NodeDefinition { */ public void name(String name) { this.name = name; - config = config.withValue("myLegalName", ConfigValueFactory.fromAnyRef(name)); + setValue("myLegalName", name); } /** @@ -62,6 +65,7 @@ public class CordformNode implements NodeDefinition { * * @return This node's P2P address. */ + @Nonnull public String getP2pAddress() { return config.getString("p2pAddress"); } @@ -71,8 +75,8 @@ public class CordformNode implements NodeDefinition { * * @param p2pPort The Artemis messaging queue port. */ - public void p2pPort(Integer p2pPort) { - config = config.withValue("p2pAddress", ConfigValueFactory.fromAnyRef(DEFAULT_HOST + ':' + p2pPort)); + public void p2pPort(int p2pPort) { + p2pAddress(DEFAULT_HOST + ':' + p2pPort); } /** @@ -81,7 +85,15 @@ public class CordformNode implements NodeDefinition { * @param p2pAddress The Artemis messaging queue host and port. */ public void p2pAddress(String p2pAddress) { - config = config.withValue("p2pAddress", ConfigValueFactory.fromAnyRef(p2pAddress)); + setValue("p2pAddress", p2pAddress); + } + + /** + * Returns the RPC address for this node, or null if one hasn't been specified. + */ + @Nullable + public String getRpcAddress() { + return getOptionalString("rpcAddress"); } /** @@ -89,8 +101,8 @@ public class CordformNode implements NodeDefinition { * * @param rpcPort The Artemis RPC queue port. */ - public void rpcPort(Integer rpcPort) { - config = config.withValue("rpcAddress", ConfigValueFactory.fromAnyRef(DEFAULT_HOST + ':' + rpcPort)); + public void rpcPort(int rpcPort) { + rpcAddress(DEFAULT_HOST + ':' + rpcPort); } /** @@ -99,7 +111,31 @@ public class CordformNode implements NodeDefinition { * @param rpcAddress The Artemis RPC queue host and port. */ public void rpcAddress(String rpcAddress) { - config = config.withValue("rpcAddress", ConfigValueFactory.fromAnyRef(rpcAddress)); + setValue("rpcAddress", rpcAddress); + } + + /** + * Returns the address of the web server that will connect to the node, or null if one hasn't been specified. + */ + @Nullable + public String getWebAddress() { + return getOptionalString("webAddress"); + } + + /** + * Configure a webserver to connect to the node via RPC. This port will specify the port it will listen on. The node + * must have an RPC address configured. + */ + public void webPort(int webPort) { + webAddress(DEFAULT_HOST + ':' + webPort); + } + + /** + * Configure a webserver to connect to the node via RPC. This address will specify the port it will listen on. The node + * must have an RPC address configured. + */ + public void webAddress(String webAddress) { + setValue("webAddress", webAddress); } /** @@ -108,6 +144,14 @@ public class CordformNode implements NodeDefinition { * @param configFile The file path. */ public void configFile(String configFile) { - config = config.withValue("configFile", ConfigValueFactory.fromAnyRef(configFile)); + setValue("configFile", configFile); + } + + private String getOptionalString(String path) { + return config.hasPath(path) ? config.getString(path) : null; + } + + private void setValue(String path, Object value) { + config = config.withValue(path, ConfigValueFactory.fromAnyRef(value)); } } diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt index 057b1861b5..f43d39f6f4 100644 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Cordform.kt @@ -11,10 +11,10 @@ import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME import org.gradle.api.tasks.TaskAction import java.io.File import java.net.URLClassLoader -import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths import java.util.concurrent.TimeUnit +import java.util.jar.JarInputStream /** * Creates nodes based on the configuration of this task in the gradle configuration DSL. @@ -23,12 +23,16 @@ import java.util.concurrent.TimeUnit */ @Suppress("unused") open class Cordform : DefaultTask() { + private companion object { + private val defaultDirectory: Path = Paths.get("build", "nodes") + } + /** * Optionally the name of a CordformDefinition subclass to which all configuration will be delegated. */ @Suppress("MemberVisibilityCanPrivate") var definitionClass: String? = null - private var directory = Paths.get("build", "nodes") + private var directory = defaultDirectory private val nodes = mutableListOf() /** @@ -116,7 +120,6 @@ open class Cordform : DefaultTask() { /** * This task action will create and install the nodes based on the node configurations added. */ - @Suppress("unused") @TaskAction fun build() { project.logger.info("Running Cordform task") @@ -129,10 +132,18 @@ open class Cordform : DefaultTask() { private fun initializeConfiguration() { if (definitionClass != null) { val cd = loadCordformDefinition() + // If the user has specified their own directory (even if it's the same default path) then let them know + // it's not used and should just rely on the one in CordformDefinition + require(directory === defaultDirectory) { + "'directory' cannot be used when 'definitionClass' is specified. Use CordformDefinition.nodesDirectory instead." + } + directory = cd.nodesDirectory + val cordapps = cd.getMatchingCordapps() cd.nodeConfigurers.forEach { val node = node { } it.accept(node) node.rootDir(directory) + node.installCordapps(cordapps) } cd.setup { nodeName -> project.projectDir.toPath().resolve(getNodeByName(nodeName)!!.nodeDir.toPath()) } } else { @@ -142,6 +153,30 @@ open class Cordform : DefaultTask() { } } + private fun CordformDefinition.getMatchingCordapps(): List { + val cordappJars = project.configuration("cordapp").files + return cordappPackages.map { `package` -> + val cordappsWithPackage = cordappJars.filter { it.containsPackage(`package`) } + when (cordappsWithPackage.size) { + 0 -> throw IllegalArgumentException("There are no cordapp dependencies containing the package $`package`") + 1 -> cordappsWithPackage[0] + else -> throw IllegalArgumentException("More than one cordapp dependency contains the package $`package`: $cordappsWithPackage") + } + } + } + + private fun File.containsPackage(`package`: String): Boolean { + JarInputStream(inputStream()).use { + while (true) { + val name = it.nextJarEntry?.name ?: break + if (name.endsWith(".class") && name.replace('/', '.').startsWith(`package`)) { + return true + } + } + return false + } + } + private fun generateAndInstallNodeInfos() { generateNodeInfos() installNodeInfos() @@ -149,7 +184,7 @@ open class Cordform : DefaultTask() { private fun generateNodeInfos() { project.logger.info("Generating node infos") - var nodeProcesses = buildNodeProcesses() + val nodeProcesses = buildNodeProcesses() try { validateNodeProcessess(nodeProcesses) } finally { @@ -177,7 +212,7 @@ open class Cordform : DefaultTask() { private fun buildNodeProcess(node: Node): Pair { node.makeLogDirectory() - var process = ProcessBuilder(generateNodeInfoCommand()) + val process = ProcessBuilder(generateNodeInfoCommand()) .directory(node.fullPath().toFile()) .redirectErrorStream(true) // InheritIO causes hangs on windows due the gradle buffer also not being flushed. @@ -224,6 +259,8 @@ open class Cordform : DefaultTask() { } } } + private fun Node.logFile(): Path = this.logDirectory().resolve("generate-info.log") + private fun ProcessBuilder.addEnvironment(key: String, value: String) = this.apply { environment().put(key, value) } } diff --git a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt index 9358a293b5..90b1364126 100644 --- a/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt +++ b/gradle-plugins/cordformation/src/main/kotlin/net/corda/plugins/Node.kt @@ -3,7 +3,6 @@ package net.corda.plugins import com.typesafe.config.* import net.corda.cordform.CordformNode import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x500.RDN import org.bouncycastle.asn1.x500.style.BCStyle import org.gradle.api.Project import java.io.File @@ -39,6 +38,7 @@ class Node(private val project: Project) : CordformNode() { private val releaseVersion = project.rootProject.ext("corda_release_version") internal lateinit var nodeDir: File + private set /** * Sets whether this node will use HTTPS communication. @@ -60,26 +60,6 @@ class Node(private val project: Project) : CordformNode() { config = config.withValue("useTestClock", ConfigValueFactory.fromAnyRef(useTestClock)) } - /** - * Set the HTTP web server port for this node. Will use localhost as the address. - * - * @param webPort The web port number for this node. - */ - fun webPort(webPort: Int) { - config = config.withValue("webAddress", - ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$webPort")) - } - - /** - * Set the HTTP web server address and port for this node. - * - * @param webAddress The web address for this node. - */ - fun webAddress(webAddress: String) { - config = config.withValue("webAddress", - ConfigValueFactory.fromAnyRef(webAddress)) - } - /** * Set the network map address for this node. * @@ -104,7 +84,6 @@ class Node(private val project: Project) : CordformNode() { config = config.withValue("sshd.port", ConfigValueFactory.fromAnyRef(sshdPort)) } - internal fun build() { configureProperties() installCordaJar() @@ -118,19 +97,15 @@ class Node(private val project: Project) : CordformNode() { } internal fun rootDir(rootDir: Path) { - if(name == null) { + if (name == null) { project.logger.error("Node has a null name - cannot create node") throw IllegalStateException("Node has a null name - cannot create node") } val dirName = try { val o = X500Name(name).getRDNs(BCStyle.O) - if (o.size > 0) { - o.first().first.value.toString() - } else { - name - } - } catch(_ : IllegalArgumentException) { + if (o.isNotEmpty()) o.first().first.value.toString() else name + } catch (_ : IllegalArgumentException) { // Can't parse as an X500 name, use the full string name } @@ -192,9 +167,8 @@ class Node(private val project: Project) : CordformNode() { /** * Installs other cordapps to this node's cordapps directory. */ - private fun installCordapps() { + internal fun installCordapps(cordapps: Collection = getCordappList()) { val cordappsDir = File(nodeDir, "cordapps") - val cordapps = getCordappList() project.copy { it.apply { from(cordapps) @@ -280,7 +254,7 @@ class Node(private val project: Project) : CordformNode() { throw RuntimeException("No Corda Webserver JAR found. Have you deployed the Corda project to Maven? Looked for \"corda-webserver-$releaseVersion.jar\"") } else { val jar = maybeJar.singleFile - assert(jar.isFile) + require(jar.isFile) return jar } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 92165eede8..10cb7bb37b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Sat Nov 25 22:21:50 GMT 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.3-all.zip diff --git a/node/build.gradle b/node/build.gradle index 7e9adec300..07a4248964 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -54,8 +54,6 @@ dependencies { compile project(':client:rpc') compile "net.corda.plugins:cordform-common:$gradle_plugins_version" - compile "com.google.code.findbugs:jsr305:3.0.1" - // Log4J: logging framework (with SLF4J bindings) compile "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}" compile "org.apache.logging.log4j:log4j-web:${log4j_version}" diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index e3a915f6b1..86124da7fc 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -29,6 +29,7 @@ interface NodeConfiguration : NodeSSLConfiguration { val notary: NotaryConfig? val activeMQServer: ActiveMqServerConfiguration val additionalNodeInfoPollingFrequencyMsec: Long + // TODO Remove as this is only used by the driver val useHTTPS: Boolean val p2pAddress: NetworkHostAndPort val rpcAddress: NetworkHostAndPort? diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle index 4d291f727a..b496155694 100644 --- a/samples/bank-of-corda-demo/build.gradle +++ b/samples/bank-of-corda-demo/build.gradle @@ -48,51 +48,8 @@ dependencies { } task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { - directory "./build/nodes" - // This name "Notary" is hard-coded into BankOfCordaClientApi so if you change it here, change it there too. - node { - name "O=Notary Service,L=Zurich,C=CH" - notary = [validating : true] - p2pPort 10002 - rpcPort 10003 - cordapps = ["$project.group:finance:$corda_release_version"] - } - node { - name "O=BankOfCorda,L=London,C=GB" - extraConfig = [issuableCurrencies : ["USD"]] - p2pPort 10005 - rpcPort 10006 - webPort 10007 - cordapps = ["$project.group:finance:$corda_release_version"] - rpcUsers = [ - ['username' : "bankUser", - 'password' : "test", - 'permissions': ["StartFlow.net.corda.finance.flows.CashPaymentFlow", - "StartFlow.net.corda.finance.flows.CashConfigDataFlow", - "StartFlow.net.corda.finance.flows.CashExitFlow", - "StartFlow.net.corda.finance.flows.CashIssueAndPaymentFlow", - "StartFlow.net.corda.finance.flows.CashConfigDataFlow", - "InvokeRpc.waitUntilNetworkReady", - "InvokeRpc.wellKnownPartyFromX500Name", - "InvokeRpc.notaryIdentities"]] - ] - } - node { - name "O=BigCorporation,L=New York,C=US" - p2pPort 10008 - rpcPort 10009 - webPort 10010 - cordapps = ["$project.group:finance:$corda_release_version"] - rpcUsers = [ - ['username' : "bigCorpUser", - 'password' : "test", - 'permissions': ["StartFlow.net.corda.finance.flows.CashPaymentFlow", - "StartFlow.net.corda.finance.flows.CashConfigDataFlow", - "InvokeRpc.waitUntilNetworkReady", - "InvokeRpc.wellKnownPartyFromX500Name", - "InvokeRpc.notaryIdentities"]] - ] - } + // CordformationDefinition is an experimental feature + definitionClass = 'net.corda.bank.BankOfCordaCordform' } task integrationTest(type: Test, dependsOn: []) { @@ -119,16 +76,9 @@ publishing { } } -task runIssuer(type: JavaExec) { - classpath = sourceSets.main.runtimeClasspath - main = 'net.corda.bank.BankOfCordaDriverKt' - args '--role' - args 'ISSUER' -} - task runRPCCashIssue(type: JavaExec) { classpath = sourceSets.main.runtimeClasspath - main = 'net.corda.bank.BankOfCordaDriverKt' + main = 'net.corda.bank.IssueCash' args '--role' args 'ISSUE_CASH_RPC' args '--quantity' @@ -139,7 +89,7 @@ task runRPCCashIssue(type: JavaExec) { task runWebCashIssue(type: JavaExec) { classpath = sourceSets.main.runtimeClasspath - main = 'net.corda.bank.BankOfCordaDriverKt' + main = 'net.corda.bank.IssueCash' args '--role' args 'ISSUE_CASH_WEB' args '--quantity' diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaCordformTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaCordformTest.kt new file mode 100644 index 0000000000..6890793aba --- /dev/null +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaCordformTest.kt @@ -0,0 +1,16 @@ +package net.corda.bank + +import net.corda.finance.DOLLARS +import net.corda.finance.POUNDS +import net.corda.testing.internal.demorun.deployNodesThen +import org.junit.Test + +class BankOfCordaCordformTest { + @Test + fun `run demo`() { + BankOfCordaCordform().deployNodesThen { + IssueCash.requestWebIssue(30000.POUNDS) + IssueCash.requestRpcIssue(20000.DOLLARS) + } + } +} diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt deleted file mode 100644 index 0f37be7cbc..0000000000 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package net.corda.bank - -import net.corda.bank.api.BankOfCordaClientApi -import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams -import net.corda.core.utilities.getOrThrow -import net.corda.testing.BOC -import net.corda.testing.driver.driver -import org.junit.Test -import kotlin.test.assertTrue - -class BankOfCordaHttpAPITest { - @Test - fun `issuer flow via Http`() { - driver(extraCordappPackagesToScan = listOf("net.corda.finance"), isDebug = true) { - val (bocNode) = listOf( - startNode(providedName = BOC.name), - startNode(providedName = BIGCORP_LEGAL_NAME) - ).map { it.getOrThrow() } - val bocApiAddress = startWebserver(bocNode).getOrThrow().listenAddress - val issueRequestParams = IssueRequestParams(1000, "USD", BIGCORP_LEGAL_NAME, "1", BOC.name, defaultNotaryIdentity.name) - assertTrue(BankOfCordaClientApi(bocApiAddress).requestWebIssue(issueRequestParams)) - } - } -} diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt index 4b2e6c2580..229d74ed4b 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt @@ -29,7 +29,7 @@ class BankOfCordaRPCClientTest { val bigCorpCFO = User("bigCorpCFO", "password2", permissions = emptySet() + commonPermissions) val (nodeBankOfCorda, nodeBigCorporation) = listOf( startNode(providedName = BOC.name, rpcUsers = listOf(bocManager)), - startNode(providedName = BIGCORP_LEGAL_NAME, rpcUsers = listOf(bigCorpCFO)) + startNode(providedName = BIGCORP_NAME, rpcUsers = listOf(bigCorpCFO)) ).map { it.getOrThrow() } // Bank of Corda RPC Client @@ -47,7 +47,7 @@ class BankOfCordaRPCClientTest { // Register for Big Corporation Vault updates val vaultUpdatesBigCorp = bigCorpProxy.vaultTrackByCriteria(Cash.State::class.java, criteria).updates - val bigCorporation = bigCorpProxy.wellKnownPartyFromX500Name(BIGCORP_LEGAL_NAME)!! + val bigCorporation = bigCorpProxy.wellKnownPartyFromX500Name(BIGCORP_NAME)!! // Kick-off actual Issuer Flow val anonymous = true diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt new file mode 100644 index 0000000000..24dda81869 --- /dev/null +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaCordform.kt @@ -0,0 +1,123 @@ +package net.corda.bank + +import joptsimple.OptionParser +import net.corda.bank.api.BankOfCordaClientApi +import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams +import net.corda.cordform.CordformContext +import net.corda.cordform.CordformDefinition +import net.corda.core.contracts.Amount +import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.VisibleForTesting +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.node.services.Permissions.Companion.all +import net.corda.node.services.config.NotaryConfig +import net.corda.node.services.transactions.ValidatingNotaryService +import net.corda.nodeapi.User +import net.corda.testing.BOC +import net.corda.testing.internal.demorun.* +import java.util.* +import kotlin.system.exitProcess + +val BIGCORP_NAME = CordaX500Name(organisation = "BigCorporation", locality = "New York", country = "US") +private val NOTARY_NAME = CordaX500Name(organisation = "Notary Service", locality = "Zurich", country = "CH") +private val BOC_RPC_PORT = 10006 +private val BOC_WEB_PORT = 10007 + +class BankOfCordaCordform : CordformDefinition() { + init { + cordappPackages += "net.corda.finance" + node { + name(NOTARY_NAME) + notary(NotaryConfig(validating = true)) + p2pPort(10002) + rpcPort(10003) + } + node { + name(CordaX500Name(organisation = "BankOfCorda", locality = "London", country = "GB")) + extraConfig = mapOf("issuableCurrencies" to listOf("USD")) + p2pPort(10005) + rpcPort(BOC_RPC_PORT) + webPort(BOC_WEB_PORT) + rpcUsers(User("bankUser", "test", setOf(all()))) + } + node { + name(BIGCORP_NAME) + p2pPort(10008) + rpcPort(10009) + webPort(10010) + rpcUsers(User("bigCorpUser", "test", setOf(all()))) + } + } + + override fun setup(context: CordformContext) = Unit +} + +object DeployNodes { + @JvmStatic + fun main(args: Array) { + BankOfCordaCordform().deployNodes() + } +} + +object IssueCash { + @JvmStatic + fun main(args: Array) { + val parser = OptionParser() + val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).describedAs("[ISSUER|ISSUE_CASH_RPC|ISSUE_CASH_WEB]") + val quantity = parser.accepts("quantity").withOptionalArg().ofType(Long::class.java) + val currency = parser.accepts("currency").withOptionalArg().ofType(String::class.java).describedAs("[GBP|USD|CHF|EUR]") + val options = try { + parser.parse(*args) + } catch (e: Exception) { + println(e.message) + printHelp(parser) + exitProcess(1) + } + + val role = options.valueOf(roleArg)!! + val amount = Amount(options.valueOf(quantity), Currency.getInstance(options.valueOf(currency))) + when (role) { + Role.ISSUE_CASH_RPC -> { + println("Requesting Cash via RPC ...") + val result = requestRpcIssue(amount) + println("Success!! Your transaction receipt is ${result.tx.id}") + } + Role.ISSUE_CASH_WEB -> { + println("Requesting Cash via Web ...") + requestWebIssue(amount) + println("Successfully processed Cash Issue request") + } + } + } + + @VisibleForTesting + fun requestRpcIssue(amount: Amount): SignedTransaction { + return BankOfCordaClientApi.requestRPCIssue(NetworkHostAndPort("localhost", BOC_RPC_PORT), createParams(amount, NOTARY_NAME)) + } + + @VisibleForTesting + fun requestWebIssue(amount: Amount) { + BankOfCordaClientApi.requestWebIssue(NetworkHostAndPort("localhost", BOC_WEB_PORT), createParams(amount, NOTARY_NAME)) + } + + private fun createParams(amount: Amount, notaryName: CordaX500Name): IssueRequestParams { + return IssueRequestParams(amount, BIGCORP_NAME, "1", BOC.name, notaryName.copy(commonName = ValidatingNotaryService.id)) + } + + private fun printHelp(parser: OptionParser) { + println(""" + Usage: bank-of-corda --role ISSUER + bank-of-corda --role (ISSUE_CASH_RPC|ISSUE_CASH_WEB) --quantity --currency + + Please refer to the documentation in docs/build/index.html for more info. + + """.trimIndent()) + parser.printHelpOn(System.out) + } + + enum class Role { + ISSUE_CASH_RPC, + ISSUE_CASH_WEB, + } +} diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt deleted file mode 100644 index 00ada75a26..0000000000 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt +++ /dev/null @@ -1,122 +0,0 @@ -package net.corda.bank - -import joptsimple.OptionParser -import net.corda.bank.api.BankOfCordaClientApi -import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams -import net.corda.core.identity.CordaX500Name -import net.corda.core.utilities.NetworkHostAndPort -import net.corda.finance.flows.CashConfigDataFlow -import net.corda.finance.flows.CashExitFlow -import net.corda.finance.flows.CashIssueAndPaymentFlow -import net.corda.finance.flows.CashPaymentFlow -import net.corda.node.services.Permissions.Companion.all -import net.corda.node.services.Permissions.Companion.startFlow -import net.corda.nodeapi.User -import net.corda.testing.BOC -import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.driver.driver -import kotlin.system.exitProcess - -/** - * This entry point allows for command line running of the Bank of Corda functions on nodes started by BankOfCordaDriver.kt. - */ -fun main(args: Array) { - BankOfCordaDriver().main(args) -} - -const val BANK_USERNAME = "bankUser" -const val BIGCORP_USERNAME = "bigCorpUser" - -val BIGCORP_LEGAL_NAME = CordaX500Name(organisation = "BigCorporation", locality = "New York", country = "US") - -private class BankOfCordaDriver { - enum class Role { - ISSUE_CASH_RPC, - ISSUE_CASH_WEB, - ISSUER - } - - fun main(args: Array) { - val parser = OptionParser() - val roleArg = parser.accepts("role").withRequiredArg().ofType(Role::class.java).describedAs("[ISSUER|ISSUE_CASH_RPC|ISSUE_CASH_WEB]") - val quantity = parser.accepts("quantity").withOptionalArg().ofType(Long::class.java) - val currency = parser.accepts("currency").withOptionalArg().ofType(String::class.java).describedAs("[GBP|USD|CHF|EUR]") - val options = try { - parser.parse(*args) - } catch (e: Exception) { - println(e.message) - printHelp(parser) - exitProcess(1) - } - - // What happens next depends on the role. - // The ISSUER will launch a Bank of Corda node - // The ISSUE_CASH will request some Cash from the ISSUER on behalf of Big Corporation node - val role = options.valueOf(roleArg)!! - - try { - when (role) { - Role.ISSUER -> { - driver(isDebug = true, extraCordappPackagesToScan = listOf("net.corda.finance.contracts.asset"), waitForAllNodesToFinish = true) { - val bankUser = User( - BANK_USERNAME, - "test", - permissions = setOf( - startFlow(), - startFlow(), - startFlow(), - startFlow(), - startFlow(), - all() - )) - val bankOfCorda = startNode( - providedName = BOC.name, - rpcUsers = listOf(bankUser)) - val bigCorpUser = User(BIGCORP_USERNAME, "test", - permissions = setOf( - startFlow(), - startFlow(), - all())) - startNode(providedName = BIGCORP_LEGAL_NAME, rpcUsers = listOf(bigCorpUser)) - startWebserver(bankOfCorda.get()) - } - } - else -> { - val requestParams = IssueRequestParams(options.valueOf(quantity), options.valueOf(currency), BIGCORP_LEGAL_NAME, - "1", BOC.name, DUMMY_NOTARY.name) - when(role) { - Role.ISSUE_CASH_RPC -> { - println("Requesting Cash via RPC ...") - val result = BankOfCordaClientApi(NetworkHostAndPort("localhost", 10004)).requestRPCIssue(requestParams) - println("Success!! You transaction receipt is ${result.tx.id}") - } - Role.ISSUE_CASH_WEB -> { - println("Requesting Cash via Web ...") - val result = BankOfCordaClientApi(NetworkHostAndPort("localhost", 10005)).requestWebIssue(requestParams) - if (result) - println("Successfully processed Cash Issue request") - } - else -> { - throw IllegalArgumentException("Unrecognized role: " + role) - } - } - } - } - } catch (e: Exception) { - println("Exception occurred: $e \n ${e.printStackTrace()}") - exitProcess(1) - } - } - - fun printHelp(parser: OptionParser) { - println(""" - Usage: bank-of-corda --role ISSUER - bank-of-corda --role (ISSUE_CASH_RPC|ISSUE_CASH_WEB) --quantity --currency - - Please refer to the documentation in docs/build/index.html for more info. - - """.trimIndent()) - parser.printHelpOn(System.out) - } -} - diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt index a50e99b727..ac0a136f7c 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaClientApi.kt @@ -15,15 +15,14 @@ import java.util.* /** * Interface for communicating with Bank of Corda node */ -class BankOfCordaClientApi(val hostAndPort: NetworkHostAndPort) { - private val apiRoot = "api/bank" +object BankOfCordaClientApi { /** * HTTP API */ // TODO: security controls required - fun requestWebIssue(params: IssueRequestParams): Boolean { - val api = HttpApi.fromHostAndPort(hostAndPort, apiRoot) - return api.postJson("issue-asset-request", params) + fun requestWebIssue(webAddress: NetworkHostAndPort, params: IssueRequestParams) { + val api = HttpApi.fromHostAndPort(webAddress, "api/bank") + api.postJson("issue-asset-request", params) } /** @@ -31,8 +30,8 @@ class BankOfCordaClientApi(val hostAndPort: NetworkHostAndPort) { * * @return a pair of the issuing and payment transactions. */ - fun requestRPCIssue(params: IssueRequestParams): SignedTransaction { - val client = CordaRPCClient(hostAndPort) + fun requestRPCIssue(rpcAddress: NetworkHostAndPort, params: IssueRequestParams): SignedTransaction { + val client = CordaRPCClient(rpcAddress) // TODO: privileged security controls required client.start("bankUser", "test").use { connection -> val rpc = connection.proxy @@ -44,11 +43,10 @@ class BankOfCordaClientApi(val hostAndPort: NetworkHostAndPort) { val notaryLegalIdentity = rpc.notaryIdentities().firstOrNull { it.name == params.notaryName } ?: throw IllegalStateException("Couldn't locate notary ${params.notaryName} in NetworkMapCache") - val amount = Amount(params.amount, Currency.getInstance(params.currency)) val anonymous = true val issuerBankPartyRef = OpaqueBytes.of(params.issuerBankPartyRef.toByte()) - return rpc.startFlow(::CashIssueAndPaymentFlow, amount, issuerBankPartyRef, issueToParty, anonymous, notaryLegalIdentity) + return rpc.startFlow(::CashIssueAndPaymentFlow, params.amount, issuerBankPartyRef, issueToParty, anonymous, notaryLegalIdentity) .returnValue.getOrThrow().stx } } diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt index 30614d0cdf..25daa53476 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/api/BankOfCordaWebApi.kt @@ -16,11 +16,14 @@ import javax.ws.rs.core.Response // API is accessible from /api/bank. All paths specified below are relative to it. @Path("bank") -class BankOfCordaWebApi(val rpc: CordaRPCOps) { - data class IssueRequestParams(val amount: Long, val currency: String, - val issueToPartyName: CordaX500Name, val issuerBankPartyRef: String, - val issuerBankName: CordaX500Name, - val notaryName: CordaX500Name) +class BankOfCordaWebApi(private val rpc: CordaRPCOps) { + data class IssueRequestParams( + val amount: Amount, + val issueToPartyName: CordaX500Name, + val issuerBankPartyRef: String, + val issuerBankName: CordaX500Name, + val notaryName: CordaX500Name + ) private companion object { private val logger = contextLogger() @@ -47,14 +50,13 @@ class BankOfCordaWebApi(val rpc: CordaRPCOps) { val notaryParty = rpc.notaryIdentities().firstOrNull { it.name == params.notaryName } ?: return Response.status(Response.Status.FORBIDDEN).entity("Unable to locate notary ${params.notaryName} in network map").build() - val amount = Amount(params.amount, Currency.getInstance(params.currency)) val anonymous = true val issuerBankPartyRef = OpaqueBytes.of(params.issuerBankPartyRef.toByte()) // invoke client side of Issuer Flow: IssuanceRequester // The line below blocks and waits for the future to resolve. return try { - rpc.startFlow(::CashIssueAndPaymentFlow, amount, issuerBankPartyRef, issueToParty, anonymous, notaryParty).returnValue.getOrThrow() + rpc.startFlow(::CashIssueAndPaymentFlow, params.amount, issuerBankPartyRef, issueToParty, anonymous, notaryParty).returnValue.getOrThrow() logger.info("Issue and payment request completed successfully: $params") Response.status(Response.Status.CREATED).build() } catch (e: Exception) { diff --git a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt index d924e41287..f47c16a623 100644 --- a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt +++ b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt @@ -117,7 +117,7 @@ class IRSDemoTest { log.info("Running trade against ${nodeApi.root}") val fileContents = loadResourceFile("net/corda/irs/web/simulation/example-irs-trade.json") val tradeFile = fileContents.replace("tradeXXX", "trade1").replace("oracleXXX", oracle.name.toString()) - assertThat(nodeApi.postJson("deals", tradeFile)).isTrue() + nodeApi.postJson("deals", tradeFile) } private fun runUploadRates(nodeApi: HttpApi) { diff --git a/samples/irs-demo/web/src/test/kotlin/net/corda/irs/web/demo/IrsDemoClientApi.kt b/samples/irs-demo/web/src/test/kotlin/net/corda/irs/web/demo/IrsDemoClientApi.kt index 5c434cdb19..82e81de047 100644 --- a/samples/irs-demo/web/src/test/kotlin/net/corda/irs/web/demo/IrsDemoClientApi.kt +++ b/samples/irs-demo/web/src/test/kotlin/net/corda/irs/web/demo/IrsDemoClientApi.kt @@ -11,10 +11,10 @@ import org.apache.commons.io.IOUtils class IRSDemoClientApi(private val hostAndPort: NetworkHostAndPort) { private val api = HttpApi.fromHostAndPort(hostAndPort, apiRoot) - fun runTrade(tradeId: String, oracleName: CordaX500Name): Boolean { + fun runTrade(tradeId: String, oracleName: CordaX500Name) { val fileContents = IOUtils.toString(javaClass.classLoader.getResourceAsStream("net/corda/irs/simulation/example-irs-trade.json"), Charsets.UTF_8.name()) val tradeFile = fileContents.replace("tradeXXX", tradeId).replace("oracleXXX", oracleName.toString()) - return api.postJson("deals", tradeFile) + api.postJson("deals", tradeFile) } fun runDateChange(newDate: String): Boolean { diff --git a/samples/notary-demo/build.gradle b/samples/notary-demo/build.gradle index 4d3f97f065..84ba21d268 100644 --- a/samples/notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -49,22 +49,18 @@ publishing { task deployNodes(dependsOn: ['deployNodesSingle', 'deployNodesRaft', 'deployNodesBFT', 'deployNodesCustom']) task deployNodesSingle(type: Cordform, dependsOn: 'jar') { - directory "./build/nodes/nodesSingle" definitionClass = 'net.corda.notarydemo.SingleNotaryCordform' } task deployNodesCustom(type: Cordform, dependsOn: 'jar') { - directory "./build/nodes/nodesCustom" definitionClass = 'net.corda.notarydemo.CustomNotaryCordform' } task deployNodesRaft(type: Cordform, dependsOn: 'jar') { - directory "./build/nodes/nodesRaft" definitionClass = 'net.corda.notarydemo.RaftNotaryCordform' } task deployNodesBFT(type: Cordform, dependsOn: 'jar') { - directory "./build/nodes/nodesBFT" definitionClass = 'net.corda.notarydemo.BFTNotaryCordform' } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt index e0919a43ab..99e158b723 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt @@ -4,7 +4,6 @@ import net.corda.cordform.CordformContext import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformNode import net.corda.core.identity.CordaX500Name -import net.corda.core.internal.div import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.services.config.BFTSMaRtConfiguration import net.corda.node.services.config.NotaryConfig @@ -14,18 +13,20 @@ import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.testing.ALICE import net.corda.testing.BOB import net.corda.testing.internal.demorun.* +import java.nio.file.Paths -fun main(args: Array) = BFTNotaryCordform().runNodes() +fun main(args: Array) = BFTNotaryCordform().deployNodes() private val clusterSize = 4 // Minimum size that tolerates a faulty replica. private val notaryNames = createNotaryNames(clusterSize) // This is not the intended final design for how to use CordformDefinition, please treat this as experimental and DO // NOT use this as a design to copy. -class BFTNotaryCordform : CordformDefinition("build" / "notary-demo-nodes") { +class BFTNotaryCordform : CordformDefinition() { private val clusterName = CordaX500Name(BFTNonValidatingNotaryService.id, "BFT", "Zurich", "CH") init { + nodesDirectory = Paths.get("build", "nodes", "nodesBFT") node { name(ALICE.name) p2pPort(10002) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt index 3cd43d7a0e..8117ef9a60 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/CustomNotaryCordform.kt @@ -2,17 +2,18 @@ package net.corda.notarydemo import net.corda.cordform.CordformContext import net.corda.cordform.CordformDefinition -import net.corda.core.internal.div import net.corda.node.services.config.NotaryConfig import net.corda.testing.ALICE import net.corda.testing.BOB import net.corda.testing.DUMMY_NOTARY import net.corda.testing.internal.demorun.* +import java.nio.file.Paths -fun main(args: Array) = CustomNotaryCordform().runNodes() +fun main(args: Array) = CustomNotaryCordform().deployNodes() -class CustomNotaryCordform : CordformDefinition("build" / "notary-demo-nodes") { +class CustomNotaryCordform : CordformDefinition() { init { + nodesDirectory = Paths.get("build", "nodes", "nodesCustom") node { name(ALICE.name) p2pPort(10002) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt index ff128c6edf..e6c6f54595 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt @@ -13,8 +13,9 @@ import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.testing.ALICE import net.corda.testing.BOB import net.corda.testing.internal.demorun.* +import java.nio.file.Paths -fun main(args: Array) = RaftNotaryCordform().runNodes() +fun main(args: Array) = RaftNotaryCordform().deployNodes() internal fun createNotaryNames(clusterSize: Int) = (0 until clusterSize).map { CordaX500Name("Notary Service $it", "Zurich", "CH") } @@ -22,10 +23,11 @@ private val notaryNames = createNotaryNames(3) // This is not the intended final design for how to use CordformDefinition, please treat this as experimental and DO // NOT use this as a design to copy. -class RaftNotaryCordform : CordformDefinition("build" / "notary-demo-nodes") { +class RaftNotaryCordform : CordformDefinition() { private val clusterName = CordaX500Name(RaftValidatingNotaryService.id, "Raft", "Zurich", "CH") init { + nodesDirectory = Paths.get("build", "nodes", "nodesRaft") node { name(ALICE.name) p2pPort(10002) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt index 97168834c0..23381b72d1 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt @@ -2,29 +2,24 @@ package net.corda.notarydemo import net.corda.cordform.CordformContext import net.corda.cordform.CordformDefinition -import net.corda.core.internal.div import net.corda.node.services.Permissions.Companion.all import net.corda.node.services.config.NotaryConfig import net.corda.nodeapi.User -import net.corda.notarydemo.flows.DummyIssueAndMove -import net.corda.notarydemo.flows.RPCStartableNotaryFlowClient import net.corda.testing.ALICE import net.corda.testing.BOB import net.corda.testing.DUMMY_NOTARY -import net.corda.testing.internal.demorun.name -import net.corda.testing.internal.demorun.node -import net.corda.testing.internal.demorun.notary -import net.corda.testing.internal.demorun.rpcUsers -import net.corda.testing.internal.demorun.runNodes +import net.corda.testing.internal.demorun.* +import java.nio.file.Paths -fun main(args: Array) = SingleNotaryCordform().runNodes() +fun main(args: Array) = SingleNotaryCordform().deployNodes() val notaryDemoUser = User("demou", "demop", setOf(all())) // This is not the intended final design for how to use CordformDefinition, please treat this as experimental and DO // NOT use this as a design to copy. -class SingleNotaryCordform : CordformDefinition("build" / "notary-demo-nodes") { +class SingleNotaryCordform : CordformDefinition() { init { + nodesDirectory = Paths.get("build", "nodes", "nodesSingle") node { name(ALICE.name) p2pPort(10002) diff --git a/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt b/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt index d09d8f5a8c..2b74779a1c 100644 --- a/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt +++ b/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt @@ -41,7 +41,7 @@ class SimmValuationTest { assertThat(createTradeBetween(nodeAApi, nodeBParty, testTradeId)).isTrue() assertTradeExists(nodeBApi, nodeAParty, testTradeId) assertTradeExists(nodeAApi, nodeBParty, testTradeId) - assertThat(runValuationsBetween(nodeAApi, nodeBParty)).isTrue() + runValuationsBetween(nodeAApi, nodeBParty) assertValuationExists(nodeBApi, nodeAParty) assertValuationExists(nodeAApi, nodeBParty) } @@ -66,8 +66,8 @@ class SimmValuationTest { assertThat(trades).filteredOn { it.id == tradeId }.isNotEmpty() } - private fun runValuationsBetween(partyApi: HttpApi, counterparty: PortfolioApi.ApiParty): Boolean { - return partyApi.postJson("${counterparty.id}/portfolio/valuations/calculate", PortfolioApi.ValuationCreationParams(valuationDate)) + private fun runValuationsBetween(partyApi: HttpApi, counterparty: PortfolioApi.ApiParty) { + partyApi.postJson("${counterparty.id}/portfolio/valuations/calculate", PortfolioApi.ValuationCreationParams(valuationDate)) } private fun assertValuationExists(partyApi: HttpApi, counterparty: PortfolioApi.ApiParty) { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index d062cc9163..eab1ebd159 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -161,12 +161,6 @@ interface DriverDSLExposedInterface : CordformContext { */ fun startNode(parameters: NodeParameters): CordaFuture = startNode(defaultParameters = parameters) - fun startNodes( - nodes: List, - startInSameProcess: Boolean? = null, - maximumHeapSize: String = "200m" - ): List> - /** Call [startWebserver] with a default maximumHeapSize. */ fun startWebserver(handle: NodeHandle): CordaFuture = startWebserver(handle, "200m") @@ -672,22 +666,21 @@ class DriverDSL( return startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize) } - override fun startNodes(nodes: List, startInSameProcess: Boolean?, maximumHeapSize: String): List> { - return nodes.map { node -> - portAllocation.nextHostAndPort() // rpcAddress - val webAddress = portAllocation.nextHostAndPort() - val name = CordaX500Name.parse(node.name) - val rpcUsers = node.rpcUsers - val notary = if (node.notary != null) mapOf("notary" to node.notary) else emptyMap() - val config = ConfigHelper.loadConfig( - baseDirectory = baseDirectory(name), - allowMissingConfig = true, - configOverrides = node.config + notary + mapOf( - "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers - ) - ) - startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize) - } + internal fun startCordformNode(cordform: CordformNode): CordaFuture { + val name = CordaX500Name.parse(cordform.name) + // TODO We shouldn't have to allocate an RPC or web address if they're not specified. We're having to do this because of startNodeInternal + val rpcAddress = if (cordform.rpcAddress == null) mapOf("rpcAddress" to portAllocation.nextHostAndPort().toString()) else emptyMap() + val webAddress = cordform.webAddress?.let { NetworkHostAndPort.parse(it) } ?: portAllocation.nextHostAndPort() + val notary = if (cordform.notary != null) mapOf("notary" to cordform.notary) else emptyMap() + val rpcUsers = cordform.rpcUsers + val config = ConfigHelper.loadConfig( + baseDirectory = baseDirectory(name), + allowMissingConfig = true, + configOverrides = cordform.config + rpcAddress + notary + mapOf( + "rpcUsers" to if (rpcUsers.isEmpty()) defaultRpcUserList else rpcUsers + ) + ) + return startNodeInternal(config, webAddress, null, "200m") } private fun queryWebserver(handle: NodeHandle, process: Process): WebserverHandle { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt index 188b6a30de..5155a31850 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/internal/demorun/DemoRunner.kt @@ -4,20 +4,63 @@ package net.corda.testing.internal.demorun import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformNode +import net.corda.core.internal.concurrent.flatMap +import net.corda.core.internal.concurrent.transpose +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.getOrThrow +import net.corda.testing.driver.DriverDSL import net.corda.testing.driver.PortAllocation import net.corda.testing.driver.driver fun CordformDefinition.clean() { - System.err.println("Deleting: $driverDirectory") - driverDirectory.toFile().deleteRecursively() + System.err.println("Deleting: $nodesDirectory") + nodesDirectory.toFile().deleteRecursively() } /** - * Creates and starts all nodes required for the demo. + * Deploy the nodes specified in the given [CordformDefinition]. This will block until all the nodes and webservers + * have terminated. */ -fun CordformDefinition.runNodes() { - driver(isDebug = true, driverDirectory = driverDirectory, portAllocation = PortAllocation.Incremental(10001), waitForAllNodesToFinish = true) { +fun CordformDefinition.deployNodes() { + runNodes(waitForAllNodesToFinish = true) { } +} + +/** + * Deploy the nodes specified in the given [CordformDefinition] and then execute the given [block] once all the nodes + * and webservers are up. After execution all these processes will be terminated. + */ +fun CordformDefinition.deployNodesThen(block: () -> Unit) { + runNodes(waitForAllNodesToFinish = false, block = block) +} + +private fun CordformDefinition.runNodes(waitForAllNodesToFinish: Boolean, block: () -> Unit) { + val nodes = nodeConfigurers.map { configurer -> CordformNode().also { configurer.accept(it) } } + val maxPort = nodes + .flatMap { listOf(it.p2pAddress, it.rpcAddress, it.webAddress) } + .mapNotNull { address -> address?.let { NetworkHostAndPort.parse(it).port } } + .max()!! + driver( + isDebug = true, + driverDirectory = nodesDirectory, + extraCordappPackagesToScan = cordappPackages, + // Notaries are manually specified in Cordform so we don't want the driver automatically starting any + notarySpecs = emptyList(), + // Start from after the largest port used to prevent port clash + portAllocation = PortAllocation.Incremental(maxPort + 1), + waitForAllNodesToFinish = waitForAllNodesToFinish + ) { setup(this) - startNodes(nodeConfigurers.map { configurer -> CordformNode().also { configurer.accept(it) } }) + this as DriverDSL // startCordformNode is an internal API + nodes.map { + val startedNode = startCordformNode(it) + if (it.webAddress != null) { + // Start a webserver if an address for it was specified + startedNode.flatMap { startWebserver(it) } + } else { + startedNode + } + }.transpose().getOrThrow() // Only proceed once everything is up and running + println("All nodes and webservers are ready...") + block() } } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt index 327f85193b..cf1af9b9ab 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt @@ -6,6 +6,7 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody import org.slf4j.LoggerFactory +import java.io.IOException import java.net.URL import java.util.concurrent.TimeUnit @@ -14,11 +15,13 @@ import java.util.concurrent.TimeUnit */ object HttpUtils { private val logger = LoggerFactory.getLogger(javaClass) + private val client by lazy { OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS).build() } + val defaultMapper: ObjectMapper by lazy { net.corda.client.jackson.JacksonSupport.createNonRpcMapper() } @@ -28,9 +31,9 @@ object HttpUtils { return makeRequest(Request.Builder().url(url).header("Content-Type", "application/json").put(body).build()) } - fun postJson(url: URL, data: String): Boolean { + fun postJson(url: URL, data: String) { val body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), data) - return makeRequest(Request.Builder().url(url).header("Content-Type", "application/json").post(body).build()) + makeExceptionalRequest(Request.Builder().url(url).header("Content-Type", "application/json").post(body).build()) } fun postPlain(url: URL, data: String): Boolean { @@ -44,6 +47,14 @@ object HttpUtils { return mapper.readValue(parameterisedUrl, T::class.java) } + // TODO Move everything to use this instead of makeRequest + private fun makeExceptionalRequest(request: Request) { + val response = client.newCall(request).execute() + if (!response.isSuccessful) { + throw IOException("${request.method()} to ${request.url()} returned a ${response.code()}: ${response.body().string()}") + } + } + private fun makeRequest(request: Request): Boolean { val response = client.newCall(request).execute()