Overhaul of the Bank of Corda demo to fix two problems it had:

1. The runRPCCashIssue and runWebCashIssue gradle tasks didn't work because they were using the wrong ports
2. Notary lookup was failing because the lookup name didn't include the correct CN for the notary name (this slipped through when reverting the network parameters)

The ports change occurred in #1922 which was attempting the fix the runIssuer gradle task. This is actually a misleading and redundant task as all it does is start up the nodes, which is what the documented deployNodes already does. The ports runIssuer allocated to the nodes were different to the ones specified in deployNodes.

To make sure we have integration tests which closely match deployNodes, the BoC demo has been updated to make use of CordformDefinition. This keeps the node definitions in one place, removing the need to have disparate files in sync. runIssuer has been removed.
This commit is contained in:
Shams Asari 2017-11-23 22:27:24 +00:00
parent b45d9e957b
commit 5c53a91785
33 changed files with 395 additions and 363 deletions

View File

@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="BankOfCordaDriverKt - Issue Web" type="JetRunConfigurationType" factoryName="Kotlin">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.bank.BankOfCordaDriverKt" />
<option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="--role ISSUE_CASH_WEB --quantity 100 --currency USD" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="bank-of-corda-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

@ -1,15 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="BankOfCordaDriverKt - Run Stack" type="JetRunConfigurationType" factoryName="Kotlin">
<extension name="coverage" enabled="false" merge="false" sample_coverage="true" runner="idea" />
<option name="MAIN_CLASS_NAME" value="net.corda.bank.BankOfCordaDriverKt" />
<option name="VM_PARAMETERS" value="" />
<option name="PROGRAM_PARAMETERS" value="--role ISSUER" />
<option name="WORKING_DIRECTORY" value="" />
<option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
<option name="ALTERNATIVE_JRE_PATH" />
<option name="PASS_PARENT_ENVS" value="true" />
<module name="bank-of-corda-demo_main" />
<envs />
<method />
</configuration>
</component>

View File

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

View File

@ -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
typesafeConfigVersion=1.3.1
jsr305Version=3.0.2

View File

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

View File

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

View File

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

View File

@ -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<Consumer<? super CordformNode>> nodeConfigurers = new ArrayList<>();
private Path nodesDirectory = Paths.get("build", "nodes");
private final List<Consumer<CordformNode>> nodeConfigurers = new ArrayList<>();
private final List<String> cordappPackages = new ArrayList<>();
public CordformDefinition(Path driverDirectory) {
this.driverDirectory = driverDirectory;
public Path getNodesDirectory() {
return nodesDirectory;
}
public void addNode(Consumer<? super CordformNode> configurer) {
public void setNodesDirectory(Path nodesDirectory) {
this.nodesDirectory = nodesDirectory;
}
public List<Consumer<CordformNode>> getNodeConfigurers() {
return nodeConfigurers;
}
public void addNode(Consumer<CordformNode> configurer) {
nodeConfigurers.add(configurer);
}
public List<String> 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);
}

View File

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

View File

@ -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<Node>()
/**
@ -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<File> {
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, Process> {
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) }
}

View File

@ -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<String>("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<File> = 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
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@ class BankOfCordaRPCClientTest {
val bigCorpCFO = User("bigCorpCFO", "password2", permissions = emptySet<String>() + 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

View File

@ -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<String>) {
BankOfCordaCordform().deployNodes()
}
}
object IssueCash {
@JvmStatic
fun main(args: Array<String>) {
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<Currency>): SignedTransaction {
return BankOfCordaClientApi.requestRPCIssue(NetworkHostAndPort("localhost", BOC_RPC_PORT), createParams(amount, NOTARY_NAME))
}
@VisibleForTesting
fun requestWebIssue(amount: Amount<Currency>) {
BankOfCordaClientApi.requestWebIssue(NetworkHostAndPort("localhost", BOC_WEB_PORT), createParams(amount, NOTARY_NAME))
}
private fun createParams(amount: Amount<Currency>, 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 <quantity> --currency <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,
}
}

View File

@ -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<String>) {
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<String>) {
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<CashPaymentFlow>(),
startFlow<CashConfigDataFlow>(),
startFlow<CashExitFlow>(),
startFlow<CashIssueAndPaymentFlow>(),
startFlow<CashConfigDataFlow>(),
all()
))
val bankOfCorda = startNode(
providedName = BOC.name,
rpcUsers = listOf(bankUser))
val bigCorpUser = User(BIGCORP_USERNAME, "test",
permissions = setOf(
startFlow<CashPaymentFlow>(),
startFlow<CashConfigDataFlow>(),
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 <quantity> --currency <currency>
Please refer to the documentation in docs/build/index.html for more info.
""".trimIndent())
parser.printHelpOn(System.out)
}
}

View File

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

View File

@ -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<Currency>,
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) {

View File

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

View File

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

View File

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

View File

@ -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<String>) = BFTNotaryCordform().runNodes()
fun main(args: Array<String>) = 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)

View File

@ -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<String>) = CustomNotaryCordform().runNodes()
fun main(args: Array<String>) = 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)

View File

@ -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<String>) = RaftNotaryCordform().runNodes()
fun main(args: Array<String>) = 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)

View File

@ -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<String>) = SingleNotaryCordform().runNodes()
fun main(args: Array<String>) = 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)

View File

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

View File

@ -161,12 +161,6 @@ interface DriverDSLExposedInterface : CordformContext {
*/
fun startNode(parameters: NodeParameters): CordaFuture<NodeHandle> = startNode(defaultParameters = parameters)
fun startNodes(
nodes: List<CordformNode>,
startInSameProcess: Boolean? = null,
maximumHeapSize: String = "200m"
): List<CordaFuture<NodeHandle>>
/** Call [startWebserver] with a default maximumHeapSize. */
fun startWebserver(handle: NodeHandle): CordaFuture<WebserverHandle> = startWebserver(handle, "200m")
@ -672,22 +666,21 @@ class DriverDSL(
return startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize)
}
override fun startNodes(nodes: List<CordformNode>, startInSameProcess: Boolean?, maximumHeapSize: String): List<CordaFuture<NodeHandle>> {
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<NodeHandle> {
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 {

View File

@ -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()
}
}

View File

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